figment-0.2.0.0: src/Figment/Provider/Json.hs
-- | JSON. Parsed from aeson's lazy token stream, so the depth and size
-- limits are enforced while parsing (a megabyte of @[[[[...@ is
-- rejected at depth 65 without being materialised) and duplicate keys
-- are reported instead of silently keeping the last.
module Figment.Provider.Json
( json
, parseJson
) where
import qualified Data.Aeson.Decoding.ByteString as Tok
import Data.Aeson.Decoding.Tokens
import qualified Data.Aeson.Key as Key
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BC
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Data.Vector as Vector
import Figment.Error
import Figment.Internal.Budget
import Figment.Limits
import Figment.Path
import Figment.Provider.Format (Format (..))
import Figment.Value
-- | The JSON format.
json :: Format
json = Format "JSON" (\lim bs -> pure (parseJson lim bs))
-- | Parse a JSON document, enforcing the limits.
parseJson :: Limits -> ByteString -> Either ConfigError Value
parseJson lim bs = runBudget lim $ do
(v, rest) <- value 1 root (Tok.bsToTokens bs)
if BC.all (`elem` (" \t\r\n" :: String)) rest
then pure v
else throwB (configError (SyntaxError "unexpected characters after the JSON value"))
syntax :: Path -> String -> Budget a
syntax p e = throwB (atPath p (configError (SyntaxError (Text.pack e))))
value :: forall k. Int -> Path -> Tokens k String -> Budget (Value, k)
value d p tok = do
spend 1 p Nothing
case tok of
TkLit LitNull k -> pure (mkValue Null, k)
TkLit LitTrue k -> pure (mkValue (Bool True), k)
TkLit LitFalse k -> pure (mkValue (Bool False), k)
TkText t k -> pure (mkValue (String t), k)
TkNumber n k -> pure (mkValue (Num (Finite (num n))), k)
TkArrayOpen arr -> enterContainer d p Nothing >> items 0 [] arr
TkRecordOpen rec -> enterContainer d p Nothing >> pairs Map.empty rec
TkErr e -> syntax p e
where
num (NumInteger i) = fromInteger i
num (NumDecimal s) = s
num (NumScientific s) = s
items :: Int -> [Value] -> TkArray k String -> Budget (Value, k)
items i acc = \case
TkItem toks -> do
(v, rest) <- value (d + 1) (p /# i) toks
items (i + 1) (v : acc) rest
TkArrayEnd k -> pure (mkValue (Array (Vector.fromList (reverse acc))), k)
TkArrayErr e -> syntax p e
pairs :: Map.Map Text.Text Value -> TkRecord k String -> Budget (Value, k)
pairs acc = \case
TkPair key toks -> do
let kt = Key.toText key
if Map.member kt acc
then throwB (atPath (p /. kt) (configError (DuplicateKey kt)))
else do
(v, rest) <- value (d + 1) (p /. kt) toks
pairs (Map.insert kt v acc) rest
TkRecordEnd k -> pure (mkValue (Dict acc), k)
TkRecordErr e -> syntax p e