summaryrefslogtreecommitdiff
path: root/src/Conftrack/Source/Aeson.hs
blob: 97353d04ac3588c4fa6635da066ab44b8866aade (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}

module Conftrack.Source.Aeson (JsonSource(..), mkJsonSource, mkJsonSourceWith, mkJsonFileSource) where

import Conftrack.Value (Key (..), ConfigError(..), Value (..), KeyPart)
import Conftrack.Source (SomeSource(..), ConfigSource (..))

import Prelude hiding (readFile)
import qualified Data.Aeson as A
import Control.Monad.State (get, modify, MonadState (..))
import Data.Function ((&))
import Data.Text (Text)
import qualified Data.Aeson.Text as A
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Aeson.Types as A
import qualified Data.Aeson.Key as A
import Data.Aeson.Types (unexpected)
import qualified Data.List.NonEmpty as NonEmpty
import Control.Monad ((>=>))
import Data.Functor ((<&>))
import System.OsPath (OsPath)
import qualified System.OsPath as OS
import System.File.OsPath (readFile)
import qualified Data.Aeson.KeyMap as A
import qualified Data.Text.Encoding as BS

data JsonSource = JsonSource
  { jsonSourceValue :: A.Value
  , jsonSourceDescription :: Text
  } deriving (Show)

mkJsonSource :: A.Value -> SomeSource
mkJsonSource value = mkJsonSourceWith ("JSON string " <> LT.toStrict (A.encodeToLazyText value)) value

mkJsonSourceWith :: Text -> A.Value -> SomeSource
mkJsonSourceWith description value = SomeSource (source, [])
  where source = JsonSource value description

mkJsonFileSource :: OsPath -> IO (Maybe SomeSource)
mkJsonFileSource path = do
  bytes <- readFile path
  pathAsText <- OS.decodeUtf path <&> LT.toStrict . LT.pack
  pure $ A.decode bytes
    <&> mkJsonSourceWith ("JSON file " <> pathAsText)

instance ConfigSource JsonSource where
  type SourceState JsonSource = [Key]

  fetchValue key@(Key parts) JsonSource{..} = do
    case A.parseEither (lookupJsonPath (NonEmpty.toList parts) >=> parseJsonValue) jsonSourceValue of
      Left a -> pure $ Left (ParseError (T.pack a))
      Right val -> do
        modify (key :)
        pure $ Right (val, jsonSourceDescription)

  leftovers JsonSource{..} = do
    used <- get

    allJsonPaths jsonSourceValue
     & filter (`notElem` used)
     & Just
     & pure

-- | this is essentially a FromJSON instance for Value, but not written as one
-- so as to not introduce an orphan
parseJsonValue :: A.Value -> A.Parser Value
parseJsonValue = \case
  (A.String bytes) -> pure $ ConfigString (BS.encodeUtf8 bytes)
  (A.Number num) ->
    A.parseJSON (A.Number num) <&> ConfigInteger
  (A.Bool b) -> pure $ ConfigBool b
  A.Null -> pure ConfigNull
  (A.Object _) -> unexpected "unexpected json object"
  (A.Array _) -> unexpected "unexpected json array"

lookupJsonPath ::  [KeyPart] -> A.Value -> A.Parser A.Value
lookupJsonPath [] value = pure value
lookupJsonPath (part:parts) value = do
  A.withObject "blub" (\obj -> obj A..: A.fromText part) value
  >>= lookupJsonPath parts

allJsonPaths :: A.Value -> [Key]
allJsonPaths = fmap keyToKey . subKeys []
  where
    keyToKey keys = Key (fmap aesonKeyToText keys)
    aesonKeyToText (key :: A.Key) = case A.parseMaybe A.parseJSON (A.toJSON key) of
      Nothing -> error "key not representable as text; this is a bug in conftrack-aeson."
      Just a -> a
    subKeys prefix (A.Object keymap) =
      A.foldMapWithKey (\key v -> subKeys (prefix <> [key]) v) keymap
    subKeys prefix _ = case NonEmpty.nonEmpty prefix of
      Just key -> [key]
      _ -> undefined