aeson-match-qq-1.7.0: src/Aeson/Match/QQ/Internal/Value.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
module Aeson.Match.QQ.Internal.Value
( Matcher(..)
, Box(..)
, Array
, Object
, HoleSig(..)
, Type(..)
, embed
, quote
) where
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as Aeson (toHashMapText)
#endif
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Scientific (Scientific)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Language.Haskell.TH (Q, Exp(..), Lit(..))
import Language.Haskell.TH.Syntax (Lift(..))
import Prelude hiding (any, null)
import qualified Text.PrettyPrint.HughesPJClass as PP (Pretty(..))
import Aeson.Match.QQ.Internal.AesonUtils (toJSONE)
-- | A value constructed using 'qq' that attempts to match
-- a JSON document.
data Matcher ext
= Hole (Maybe HoleSig) (Maybe Text)
-- ^ Optionally typed, optionally named _hole.
-- If a type is provided, the _hole only matches those values
-- that have that type.
-- If a name is provided, the matched value is returned
-- to the user.
| Null
| Bool Bool
| Number Scientific
| String Text
| StringCI (CI Text)
-- ^ Case-insensitive strings
| Array (Array ext)
| ArrayUO (Array ext)
-- ^ Unordered arrays
| Object (Object ext)
| Ext ext
-- ^ External values spliced into a 'Matcher' using the `#{}` syntax
deriving (Show, Eq, Functor)
instance Aeson.ToJSON ext => Aeson.ToJSON (Matcher ext) where
toJSON =
Aeson.object . \case
Hole type_ name ->
[ "type" .= ("hole" :: Text)
, "expected-type" .= type_
, "name" .= name
]
Null ->
[ "type" .= ("null" :: Text)
]
Bool v ->
[ "type" .= ("bool" :: Text)
, "value" .= v
]
Number v ->
[ "type" .= ("number" :: Text)
, "value" .= v
]
String v ->
[ "type" .= ("string" :: Text)
, "value" .= v
]
StringCI v ->
[ "type" .= ("string-ci" :: Text)
, "value" .= CI.original v
]
Array v ->
[ "type" .= ("array" :: Text)
, "value" .= v
]
ArrayUO v ->
[ "type" .= ("array-unordered" :: Text)
, "value" .= v
]
Object v ->
[ "type" .= ("object" :: Text)
, "value" .= v
]
Ext v ->
[ "type" .= ("extension" :: Text)
, "value" .= v
]
-- | A wrapper for those matchers that support the `...` syntax.
data Box a = Box
{ values :: a
, extra :: Bool
-- ^ Are extra, not specifically mentioned by a 'Matcher', values
-- allowed in a 'Value'?
} deriving (Show, Eq, Functor)
instance Aeson.ToJSON a => Aeson.ToJSON (Box a) where
toJSON Box {..} =
Aeson.object
[ "values" .= values
, "extra" .= extra
]
type Array ext = Box (Vector (Matcher ext))
type Object ext = Box (HashMap Text (Matcher ext))
-- | It may be tempting to make this the 'Lift' instance for 'Matcher', but I don't
-- think it would be correct. We can get a lot from re-using 'Lift' machinery: namely,
-- we can cpmpletely bypass manual 'Exp' construction. But, fundamentally, 'Lift' is
-- for "serializing" Haskell values and it is not what we are attempting here.
quote :: Matcher Exp -> Q Exp
quote = \case
Hole type_ name ->
[| Hole type_ name :: Matcher Aeson.Value |]
Null ->
[| Null :: Matcher Aeson.Value |]
Bool b ->
[| Bool b :: Matcher Aeson.Value |]
Number n ->
[| Number n :: Matcher Aeson.Value |]
String str ->
[| String str :: Matcher Aeson.Value |]
StringCI ci -> let
original = CI.original ci
in
[| StringCI (CI.mk original) :: Matcher Aeson.Value |]
Array Box {values, extra} -> do
let
quoted =
fmap ListE (traverse quote (Vector.toList values))
[| Array Box
{ values = Vector.fromList $quoted
, extra
} :: Matcher Aeson.Value |]
ArrayUO Box {values, extra} -> do
let
quoted =
fmap ListE (traverse quote (Vector.toList values))
[| ArrayUO Box
{ values = Vector.fromList $quoted
, extra
} :: Matcher Aeson.Value |]
Object Box {values, extra} -> do
let
quoted =
fmap toExp (traverse (traverse quote) (HashMap.toList values))
toExp =
ListE . map (\(k, v) -> tup2 (LitE (StringL (Text.unpack k)), v))
tup2 (a, b) =
TupE [Just a, Just b]
[| Object Box
{ values = HashMap.fromList $quoted
, extra
} :: Matcher Aeson.Value |]
-- | This is fundamentally type-unsafe as long as we try to splice `Exp` in.
Ext ext ->
[| Ext (toJSONE $(pure ext)) :: Matcher Aeson.Value |]
-- | _hole type signature
data HoleSig = HoleSig
{ type_ :: Type
, nullable :: Bool
} deriving (Show, Eq, Lift)
instance Aeson.ToJSON HoleSig where
toJSON HoleSig {..} =
Aeson.object
[ "type" .= type_
, "nullable" .= nullable
]
-- | _hole type
data Type
= BoolT
-- ^ @_ : bool@
| NumberT
-- ^ @_ : number@
| StringT
-- ^ @_ : string@
| StringCIT
-- ^ @_ : ci-string@
| ArrayT
-- ^ @_ : array@
| ArrayUOT
-- ^ @_ : unordered-array@
| ObjectT
-- ^ @_ : object@
deriving (Show, Eq, Lift)
instance Aeson.ToJSON Type where
toJSON =
Aeson.toJSON . \case
BoolT {} -> "bool" :: Text
NumberT {} -> "number"
StringT {} -> "string"
StringCIT {} -> "ci-string"
ArrayT {} -> "array"
ArrayUOT {} -> "unordered-array"
ObjectT {} -> "object"
instance PP.Pretty Type where
pPrint = \case
BoolT {} -> "bool"
NumberT {} -> "number"
StringT {} -> "string"
StringCIT {} -> "ci-string"
ArrayT {} -> "array"
ArrayUOT {} -> "unordered-array"
ObjectT {} -> "object"
embed :: Aeson.Value -> Matcher ext
embed = \case
Aeson.Null ->
Null
Aeson.Bool b ->
Bool b
Aeson.Number n ->
Number n
Aeson.String n ->
String n
Aeson.Array xs ->
Array Box {values = fmap embed xs, extra = False}
#if MIN_VERSION_aeson(2,0,0)
Aeson.Object (Aeson.toHashMapText -> o) ->
#else
Aeson.Object o ->
#endif
Object Box {values = fmap embed o, extra = False}