packages feed

aeson-match-qq 1.7.0 → 1.8.0

raw patch · 11 files changed

+385/−309 lines, 11 filesdep +attoparsec-aesonPVP ok

version bump matches the API change (PVP)

Dependencies added: attoparsec-aeson

API changes (from Hackage documentation)

- Aeson.Match.QQ: Hole :: Maybe HoleSig -> Maybe Text -> Matcher ext
- Aeson.Match.QQ: HoleSig :: Type -> Bool -> HoleSig
- Aeson.Match.QQ: [$sel:expected:MkTypeMismatch] :: TypeMismatch -> Type
- Aeson.Match.QQ: [$sel:given:MkMismatch] :: Mismatch -> Value
- Aeson.Match.QQ: [$sel:given:MkTypeMismatch] :: TypeMismatch -> Value
- Aeson.Match.QQ: [$sel:matcher:MkMismatch] :: Mismatch -> Matcher Value
- Aeson.Match.QQ: [$sel:matcher:MkTypeMismatch] :: TypeMismatch -> Matcher Value
- Aeson.Match.QQ: [$sel:missing:MkMissingPathElem] :: MissingPathElem -> PathElem
- Aeson.Match.QQ: [$sel:path:MkExtraArrayValues] :: ExtraArrayValues -> Path
- Aeson.Match.QQ: [$sel:path:MkExtraObjectValues] :: ExtraObjectValues -> Path
- Aeson.Match.QQ: [$sel:path:MkMismatch] :: Mismatch -> Path
- Aeson.Match.QQ: [$sel:path:MkMissingPathElem] :: MissingPathElem -> Path
- Aeson.Match.QQ: [$sel:path:MkTypeMismatch] :: TypeMismatch -> Path
- Aeson.Match.QQ: [$sel:unPath:Path] :: Path -> [PathElem]
- Aeson.Match.QQ: [$sel:values:MkExtraArrayValues] :: ExtraArrayValues -> Vector Value
- Aeson.Match.QQ: [$sel:values:MkExtraObjectValues] :: ExtraObjectValues -> HashMap Text Value
- Aeson.Match.QQ: [nullable] :: HoleSig -> Bool
- Aeson.Match.QQ: [type_] :: HoleSig -> Type
- Aeson.Match.QQ: data HoleSig
- Aeson.Match.QQ.Internal.Match: [$sel:expected:MkTypeMismatch] :: TypeMismatch -> Type
- Aeson.Match.QQ.Internal.Match: [$sel:given:MkMismatch] :: Mismatch -> Value
- Aeson.Match.QQ.Internal.Match: [$sel:given:MkTypeMismatch] :: TypeMismatch -> Value
- Aeson.Match.QQ.Internal.Match: [$sel:matcher:MkMismatch] :: Mismatch -> Matcher Value
- Aeson.Match.QQ.Internal.Match: [$sel:matcher:MkTypeMismatch] :: TypeMismatch -> Matcher Value
- Aeson.Match.QQ.Internal.Match: [$sel:missing:MkMissingPathElem] :: MissingPathElem -> PathElem
- Aeson.Match.QQ.Internal.Match: [$sel:path:MkExtraArrayValues] :: ExtraArrayValues -> Path
- Aeson.Match.QQ.Internal.Match: [$sel:path:MkExtraObjectValues] :: ExtraObjectValues -> Path
- Aeson.Match.QQ.Internal.Match: [$sel:path:MkMismatch] :: Mismatch -> Path
- Aeson.Match.QQ.Internal.Match: [$sel:path:MkMissingPathElem] :: MissingPathElem -> Path
- Aeson.Match.QQ.Internal.Match: [$sel:path:MkTypeMismatch] :: TypeMismatch -> Path
- Aeson.Match.QQ.Internal.Match: [$sel:unPath:Path] :: Path -> [PathElem]
- Aeson.Match.QQ.Internal.Match: [$sel:values:MkExtraArrayValues] :: ExtraArrayValues -> Vector Value
- Aeson.Match.QQ.Internal.Match: [$sel:values:MkExtraObjectValues] :: ExtraObjectValues -> HashMap Text Value
- Aeson.Match.QQ.Internal.Match: instance GHC.Exts.IsList Aeson.Match.QQ.Internal.Match.Path
- Aeson.Match.QQ.Internal.Value: Hole :: Maybe HoleSig -> Maybe Text -> Matcher ext
- Aeson.Match.QQ.Internal.Value: HoleSig :: Type -> Bool -> HoleSig
- Aeson.Match.QQ.Internal.Value: [nullable] :: HoleSig -> Bool
- Aeson.Match.QQ.Internal.Value: [type_] :: HoleSig -> Type
- Aeson.Match.QQ.Internal.Value: data HoleSig
- Aeson.Match.QQ.Internal.Value: instance Data.Aeson.Types.ToJSON.ToJSON Aeson.Match.QQ.Internal.Value.HoleSig
- Aeson.Match.QQ.Internal.Value: instance GHC.Classes.Eq Aeson.Match.QQ.Internal.Value.HoleSig
- Aeson.Match.QQ.Internal.Value: instance GHC.Show.Show Aeson.Match.QQ.Internal.Value.HoleSig
- Aeson.Match.QQ.Internal.Value: instance Language.Haskell.TH.Syntax.Lift Aeson.Match.QQ.Internal.Value.HoleSig
+ Aeson.Match.QQ: AnyT :: Type
+ Aeson.Match.QQ: Sig :: Type -> Bool -> Matcher ext -> Matcher ext
+ Aeson.Match.QQ: Var :: Text -> Matcher ext
+ Aeson.Match.QQ: [expected] :: TypeMismatch -> Type
+ Aeson.Match.QQ: [given] :: Mismatch -> Value
+ Aeson.Match.QQ: [matcher] :: Mismatch -> Matcher Value
+ Aeson.Match.QQ: [missing] :: MissingPathElem -> PathElem
+ Aeson.Match.QQ: [path] :: ExtraObjectValues -> Path
+ Aeson.Match.QQ: [unPath] :: Path -> [PathElem]
+ Aeson.Match.QQ.Internal.Match: [expected] :: TypeMismatch -> Type
+ Aeson.Match.QQ.Internal.Match: [given] :: Mismatch -> Value
+ Aeson.Match.QQ.Internal.Match: [matcher] :: Mismatch -> Matcher Value
+ Aeson.Match.QQ.Internal.Match: [missing] :: MissingPathElem -> PathElem
+ Aeson.Match.QQ.Internal.Match: [path] :: ExtraObjectValues -> Path
+ Aeson.Match.QQ.Internal.Match: [unPath] :: Path -> [PathElem]
+ Aeson.Match.QQ.Internal.Match: [values] :: ExtraObjectValues -> HashMap Text Value
+ Aeson.Match.QQ.Internal.Match: instance GHC.IsList.IsList Aeson.Match.QQ.Internal.Match.Path
+ Aeson.Match.QQ.Internal.Value: AnyT :: Type
+ Aeson.Match.QQ.Internal.Value: Sig :: Type -> Bool -> Matcher ext -> Matcher ext
+ Aeson.Match.QQ.Internal.Value: Var :: Text -> Matcher ext
- Aeson.Match.QQ: type Array ext = Box (Vector (Matcher ext))
+ Aeson.Match.QQ: type Array ext = Box Vector Matcher ext
- Aeson.Match.QQ: type Object ext = Box (HashMap Text (Matcher ext))
+ Aeson.Match.QQ: type Object ext = Box HashMap Text NonEmpty Matcher ext
- Aeson.Match.QQ.Internal.Value: type Array ext = Box (Vector (Matcher ext))
+ Aeson.Match.QQ.Internal.Value: type Array ext = Box Vector Matcher ext
- Aeson.Match.QQ.Internal.Value: type Object ext = Box (HashMap Text (Matcher ext))
+ Aeson.Match.QQ.Internal.Value: type Object ext = Box HashMap Text NonEmpty Matcher ext

Files

CHANGELOG.markdown view
@@ -1,3 +1,12 @@+1.8.0+====++  * GHC 9.8 compatibility++  * Added `any` type for consistency.++  * Duplicate keys in objects are handled ~~properly~~ better (https://github.com/supki/aeson-match-qq/pull/40)+ 1.7.0 ==== 
aeson-match-qq.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.2.+-- This file has been generated from package.yaml by hpack version 0.38.0. -- -- see: https://github.com/sol/hpack  name:           aeson-match-qq-version:        1.7.0+version:        1.8.0 synopsis:       Declarative JSON matchers. description:    See README.markdown category:       Web@@ -16,6 +16,9 @@ license:        BSD2 license-file:   LICENSE build-type:     Simple+tested-with:+    GHC==9.6.5+  , GHC==9.8.2 extra-source-files:     README.markdown     CHANGELOG.markdown@@ -36,10 +39,13 @@       Paths_aeson_match_qq   hs-source-dirs:       src-  ghc-options: -funbox-strict-fields -Wall+  default-extensions:+      ImportQualifiedPost+  ghc-options: -funbox-strict-fields -Wall -Wno-incomplete-uni-patterns   build-depends:       aeson     , attoparsec+    , attoparsec-aeson     , base >=4.14 && <5     , bytestring     , case-insensitive@@ -58,12 +64,17 @@   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:+      Aeson.Match.QQ.Internal.ParseSpec       Aeson.Match.QQ.Internal.PrettyPrintSpec       Aeson.Match.QQSpec       Paths_aeson_match_qq   hs-source-dirs:       test-  ghc-options: -Wall -threaded -with-rtsopts=-N+  default-extensions:+      ImportQualifiedPost+  ghc-options: -Wall -Wno-incomplete-uni-patterns -threaded -with-rtsopts=-N+  build-tool-depends:+      hspec-discover:hspec-discover   build-depends:       aeson     , aeson-match-qq
src/Aeson/Match/QQ.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DuplicateRecordFields #-} module Aeson.Match.QQ   ( match   , qq@@ -14,7 +15,6 @@   , Array   , Object   , Box(..)-  , HoleSig(..)   , Type(..)   , Path(..)   , PathElem(..)@@ -22,13 +22,13 @@   , parse   ) where -import           Data.String (IsString(..))-import qualified Data.Text.Encoding as Text-import           Language.Haskell.TH.Quote (QuasiQuoter(..))-import qualified Text.PrettyPrint as PP (render)-import qualified Text.PrettyPrint.HughesPJClass as PP (Pretty(..))+import Data.String (IsString(..))+import Data.Text.Encoding qualified as Text+import Language.Haskell.TH.Quote (QuasiQuoter(..))+import Text.PrettyPrint qualified as PP (render)+import Text.PrettyPrint.HughesPJClass qualified as PP (Pretty(..)) -import           Aeson.Match.QQ.Internal.Match+import Aeson.Match.QQ.Internal.Match   ( match   , Error(..)   , TypeMismatch(..)@@ -39,13 +39,12 @@   , Path(..)   , PathElem(..)   )-import           Aeson.Match.QQ.Internal.Parse (parse)-import           Aeson.Match.QQ.Internal.Value+import Aeson.Match.QQ.Internal.Parse (parse)+import Aeson.Match.QQ.Internal.Value   ( Matcher(..)   , Box(..)   , Array   , Object-  , HoleSig(..)   , Type(..)   , quote   )
src/Aeson/Match/QQ/Internal/AesonUtils.hs view
@@ -6,21 +6,21 @@   , pp   ) where -import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Encoding.Internal as Aeson (encodingToLazyByteString)-import qualified Data.Aeson.KeyMap as Aeson.KeyMap-import           Data.Bool (bool)-import           Data.Foldable (toList)-import qualified Data.List as List-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import           Data.Int (Int64)-import           Data.Scientific (Scientific, floatingOrInteger)-import           Data.String (fromString)-import           Data.Text (Text)-import           Data.Vector (Vector)-import           Text.PrettyPrint ((<+>))-import qualified Text.PrettyPrint as PP+import Data.Aeson qualified as Aeson+import Data.Aeson.Encoding.Internal qualified as Aeson (encodingToLazyByteString)+import Data.Aeson.KeyMap qualified as Aeson.KeyMap+import Data.Bool (bool)+import Data.Foldable (toList)+import Data.List qualified as List+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Int (Int64)+import Data.Scientific (Scientific, floatingOrInteger)+import Data.String (fromString)+import Data.Text (Text)+import Data.Vector (Vector)+import Text.PrettyPrint ((<+>))+import Text.PrettyPrint qualified as PP   -- | This is a round-about way to produce a 'Aeson.Value' from a 'ToJSON' instance.
src/Aeson/Match/QQ/Internal/Match.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -20,35 +19,32 @@   , PathElem(..)   ) where -import           Control.Applicative (liftA2)-import           Control.Monad (unless)-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.Bool (bool)-import qualified Data.CaseInsensitive as CI-import           Data.Either.Validation+import Control.Monad (unless)+import Data.Aeson ((.=))+import Data.Aeson qualified as Aeson+import Data.Aeson.KeyMap qualified as Aeson (toHashMapText)+import Data.Bool (bool)+import Data.CaseInsensitive qualified as CI+import Data.Either.Validation   ( Validation(..)   , eitherToValidation   , validationToEither   )-import           Data.Foldable (for_, toList)-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import qualified Data.List as List-import           Data.List.NonEmpty (NonEmpty, nonEmpty)-import           Data.Maybe (mapMaybe)-import qualified Data.Set as Set-import           Data.String (IsString(..))-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Vector (Vector)-import qualified Data.Vector as Vector-import           GHC.Exts (IsList)-import           Prelude hiding (any, null)-import qualified Text.PrettyPrint as PP+import Data.Foldable (toList)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.List qualified as List+import Data.List.NonEmpty (NonEmpty, nonEmpty)+import Data.Maybe (mapMaybe)+import Data.Set qualified as Set+import Data.String (IsString(..))+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Vector (Vector)+import Data.Vector qualified as Vector+import GHC.Exts (IsList)+import Prelude hiding (any, null)+import Text.PrettyPrint qualified as PP   ( vcat   , hsep   , brackets@@ -56,14 +52,13 @@   , char   , int   )-import qualified Text.PrettyPrint.HughesPJClass as PP (Pretty(..))+import Text.PrettyPrint.HughesPJClass qualified as PP (Pretty(..)) -import qualified Aeson.Match.QQ.Internal.AesonUtils as AesonUtils (pp)-import qualified Aeson.Match.QQ.Internal.PrettyPrint as Matcher (pp)-import           Aeson.Match.QQ.Internal.Value+import Aeson.Match.QQ.Internal.AesonUtils qualified as AesonUtils (pp)+import Aeson.Match.QQ.Internal.PrettyPrint qualified as Matcher (pp)+import Aeson.Match.QQ.Internal.Value   ( Matcher(..)   , Box(..)-  , HoleSig(..)   , Type(..)   , embed   )@@ -86,11 +81,6 @@       mistyped expected =         mistype path expected matcher given     case (matcher, given) of-      (Hole holeTypeO nameO, val) -> do-        for_ holeTypeO $ \holeType ->-          unless (holeTypeMatch holeType val) $-            mistyped (type_ holeType)-        pure (maybe mempty (\name -> HashMap.singleton name val) nameO)       (Null, Aeson.Null) ->         pure mempty       (Null, _) -> do@@ -142,11 +132,7 @@         mistyped ArrayUOT         pure mempty       ( Object Box {values, extra}-#if MIN_VERSION_aeson(2,0,0)         , Aeson.Object (Aeson.toHashMapText -> o)-#else-        , Aeson.Object o-#endif          ) ->         let fold f =@@ -158,26 +144,38 @@             (extra || HashMap.null extraValues)             (extraObjectValues path extraValues) *>           fold-            (\k v -> maybe (missingPathElem path (Key k)) (go (Key k : path) v) (HashMap.lookup k o))+            (\k vs ->+              maybe+                (missingPathElem path (Key k))+                (\ov -> foldr1 (liftA2 (<>)) (fmap (\v -> go (Key k : path) v ov) vs))+                (HashMap.lookup k o))             values       (Object _, _) -> do         mistyped ObjectT         pure mempty+      (Sig type_ nullable x, val) -> -- do -- ApplicativeDo shits the bed here for some reason+        unless (sigTypeMatch type_ nullable val) (mistyped type_) *>+        go path x val+      (Var "", _) ->+        pure mempty+      (Var name, val) ->+        pure (HashMap.singleton name val)       (Ext val, val') ->         go path (embed val) val' -holeTypeMatch :: HoleSig -> Aeson.Value -> Bool-holeTypeMatch type_ val =-  case (type_, val) of-    (HoleSig {nullable = True}, Aeson.Null) -> True-    (HoleSig {type_ = BoolT} , Aeson.Bool {}) -> True-    (HoleSig {type_ = NumberT} , Aeson.Number {}) -> True-    (HoleSig {type_ = StringT} , Aeson.String {}) -> True-    (HoleSig {type_ = StringCIT} , Aeson.String {}) -> True-    (HoleSig {type_ = ArrayT} , Aeson.Array {}) -> True-    (HoleSig {type_ = ArrayUOT} , Aeson.Array {}) -> True-    (HoleSig {type_ = ObjectT} , Aeson.Object {}) -> True-    (_, _) -> False+sigTypeMatch :: Type -> Bool -> Aeson.Value -> Bool+sigTypeMatch type_ nullable val =+  case (type_, nullable, val) of+    (AnyT,      _,    _) -> True+    (_,         True, Aeson.Null) -> True+    (BoolT,     _,    Aeson.Bool {}) -> True+    (NumberT,   _,    Aeson.Number {}) -> True+    (StringT,   _,    Aeson.String {}) -> True+    (StringCIT, _,    Aeson.String {}) -> True+    (ArrayT,    _,    Aeson.Array {}) -> True+    (ArrayUOT,  _,    Aeson.Array {}) -> True+    (ObjectT,   _,    Aeson.Object {}) -> True+    (_,         _,    _) -> False  matchArrayUO   :: Validation (NonEmpty Error) (HashMap Text Aeson.Value)
src/Aeson/Match/QQ/Internal/Parse.hs view
@@ -7,30 +7,29 @@   ( parse   ) where -import           Control.Applicative ((<|>), optional)-import qualified Data.Aeson.Parser as Aeson-import qualified Data.Attoparsec.ByteString as Atto-import qualified Data.ByteString as ByteString+import Control.Applicative ((<|>), optional)+import Data.Aeson.Parser qualified as Aeson+import Data.Attoparsec.ByteString qualified as Atto+import Data.ByteString qualified as ByteString -- cannot use .Text here due to .Aeson parsers being tied to .ByteString-import           Data.ByteString (ByteString)-import qualified Data.CaseInsensitive as CI-import qualified Data.Char as Char-import           Data.Foldable (asum)-import qualified Data.HashMap.Strict as HashMap-import           Data.Maybe (isJust)-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import qualified Data.Vector as Vector-import           Data.Word (Word8)-import           Language.Haskell.Meta.Parse (parseExp)-import           Language.Haskell.TH (Exp(..))-import           Prelude hiding (any, null)+import Data.ByteString (ByteString)+import Data.CaseInsensitive qualified as CI+import Data.Char qualified as Char+import Data.Foldable (asum)+import Data.HashMap.Strict qualified as HashMap+import Data.Maybe (isJust)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Data.Vector qualified as Vector+import Data.Word (Word8)+import Language.Haskell.Meta.Parse (parseExp)+import Language.Haskell.TH (Exp(..))+import Prelude hiding (any, null) -import           Aeson.Match.QQ.Internal.Value+import Aeson.Match.QQ.Internal.Value   ( Matcher(..)   , Box(..)-  , HoleSig(..)   , Type(..)   ) @@ -80,30 +79,30 @@ optimize = \case   -- [...] -> _ : array   Array Box {extra = True, values = (Vector.null -> True)} ->-    Hole (Just (HoleSig ArrayT False)) Nothing+    Sig ArrayT False (Var "")   -- this optimization is probably never going to be used,   -- but I'll include it for completeness:   -- (unordered) [...] -> _ : unordered-array   ArrayUO Box {extra = True, values = (Vector.null -> True)} ->-    Hole (Just (HoleSig ArrayUOT False)) Nothing+    Sig ArrayUOT False (Var "")   -- {...} -> _ : object   Object Box {extra = True, values = (HashMap.null -> True)} ->-    Hole (Just (HoleSig ObjectT False)) Nothing+    Sig ObjectT False (Var "")   val ->     val  any :: Atto.Parser (Matcher Exp) any = do   _ <- Atto.word8 HoleP-  name <- fmap Just key <|> pure Nothing+  name <- key <|> pure ""   spaces   b <- optional Atto.peekWord8'-  expectedType <- case b of+  (type_, nullable) <- case b of     Just ColonP ->-      fmap Just holeSig+      sig     _ ->-      pure Nothing-  pure (Hole expectedType name)+      pure (AnyT, False)+  pure (Sig type_ nullable (Var name))  null :: Atto.Parser (Matcher Exp) null =@@ -208,10 +207,10 @@         sep <- Atto.satisfy (\w -> w == CommaP || w == CloseCurlyBracketP) Atto.<?> "',' or '}'"         case sep of           CommaP ->-            loop ((k, val) : acc)+            loop ((k, pure val) : acc)           CloseCurlyBracketP ->             pure $ Object Box-              { values = HashMap.fromList ((k, val) : acc)+              { values = HashMap.fromListWith (<>) ((k, pure val) : acc)               , extra = False               }           _ ->@@ -244,12 +243,13 @@     str <- Atto.takeWhile1 (/= CloseCurlyBracketP) <* Atto.word8 CloseCurlyBracketP     either fail pure (parseExp (Text.unpack (Text.decodeUtf8 str))) -holeSig :: Atto.Parser HoleSig-holeSig = do+sig :: Atto.Parser (Type, Bool)+sig = do   _ <- Atto.word8 ColonP   spaces   asum-    [ p "bool" BoolT+    [ p "any" AnyT+    , p "bool" BoolT     , p "number" NumberT     , p "string" StringT     , p "ci-string" StringCIT@@ -261,7 +261,7 @@   p name typeName = do     _ <- Atto.string name     q <- optional (Atto.word8 QuestionMarkP)-    pure (HoleSig typeName (isJust q))+    pure (typeName, isJust q)  eof :: Atto.Parser () eof =
src/Aeson/Match/QQ/Internal/PrettyPrint.hs view
@@ -6,29 +6,30 @@   ( pp   ) where -import qualified Data.Aeson as Aeson-import           Data.Bool (bool)-import qualified Data.ByteString.Lazy as ByteString.Lazy-import           Data.CaseInsensitive (CI)-import qualified Data.CaseInsensitive as CI-import qualified Data.Char as Char-import           Data.Foldable (toList)-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import           Data.Int (Int64)-import qualified Data.List as List-import           Data.Scientific (Scientific, floatingOrInteger)-import           Data.String (fromString)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import           Data.Text (Text)-import           Data.Vector (Vector)-import           Text.PrettyPrint ((<+>))-import qualified Text.PrettyPrint as PP+import Control.Monad ((<=<))+import Data.Aeson qualified as Aeson+import Data.Bool (bool)+import Data.ByteString.Lazy qualified as ByteString.Lazy+import Data.CaseInsensitive (CI)+import Data.CaseInsensitive qualified as CI+import Data.Char qualified as Char+import Data.Foldable (toList)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Int (Int64)+import Data.List qualified as List+import Data.List.NonEmpty (NonEmpty)+import Data.Scientific (Scientific, floatingOrInteger)+import Data.String (fromString)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Data.Text (Text)+import Data.Vector (Vector)+import Text.PrettyPrint ((<+>))+import Text.PrettyPrint qualified as PP -import           Aeson.Match.QQ.Internal.Value+import Aeson.Match.QQ.Internal.Value   ( Matcher(..)-  , HoleSig(..)   , Type(..)   , Box(..)   )@@ -44,8 +45,6 @@  rValue :: Matcher Aeson.Value -> PP.Doc rValue = \case-  Hole sig name ->-    rHole sig name   Null ->     rNull   Bool b ->@@ -62,33 +61,13 @@     rArrayUO xs   Object o ->     rObject o+  Sig type_ nullable v ->+    rSig type_ nullable v+  Var name ->+    rVar name   Ext ext ->     rExt ext -rHole :: Maybe HoleSig -> Maybe Text -> PP.Doc-rHole sig name =-  ("_" <> maybe PP.empty rName name) <+> maybe PP.empty rSig sig--rName :: Text -> PP.Doc-rName name =-  PP.text (bool (Text.unpack name) (show name) (hasSpaces name))- where-  hasSpaces =-    Text.any Char.isSpace--rSig :: HoleSig -> PP.Doc-rSig HoleSig {type_, nullable} =-  (":" <+> rType type_) <> bool PP.empty "?" nullable- where-  rType = \case-    BoolT -> "bool"-    NumberT -> "number"-    StringT -> "string"-    StringCIT -> "ci-string"-    ArrayT -> "array"-    ArrayUOT -> "unordered-array"-    ObjectT -> "object"- rNull :: PP.Doc rNull =   "null"@@ -130,13 +109,9 @@     , rArray box     ] -rExt :: Aeson.Value -> PP.Doc-rExt =-  fromString . Text.unpack . Text.decodeUtf8 . ByteString.Lazy.toStrict . Aeson.encode--rObject :: Box (HashMap Text (Matcher Aeson.Value)) -> PP.Doc+rObject :: Box (HashMap Text (NonEmpty (Matcher Aeson.Value))) -> PP.Doc rObject Box {values, extra} =-  case List.sortOn fst (HashMap.toList values) of+  case toKeyValues values of     [] ->       "{}"     kv : kvs ->@@ -154,10 +129,41 @@         , rValue value         ] +toKeyValues :: (Ord k, Foldable t) => HashMap k (t v) -> [(k, v)]+toKeyValues =+  traverse toList <=< List.sortOn fst . HashMap.toList++rSig :: Type -> Bool -> Matcher Aeson.Value -> PP.Doc+rSig type_ nullable val =+  rValue val <+> ((":" <+> rType type_) <> bool PP.empty "?" nullable)+ where+  rType = \case+    AnyT -> "any"+    BoolT -> "bool"+    NumberT -> "number"+    StringT -> "string"+    StringCIT -> "ci-string"+    ArrayT -> "array"+    ArrayUOT -> "unordered-array"+    ObjectT -> "object"++rVar :: Text -> PP.Doc+rVar name =+  "_" <> rName name++rName :: Text -> PP.Doc+rName name =+  PP.text (bool (Text.unpack name) (show name) (hasSpaces name))+ where+  hasSpaces =+    Text.any Char.isSpace++rExt :: Aeson.Value -> PP.Doc+rExt =+  fromString . Text.unpack . Text.decodeUtf8 . ByteString.Lazy.toStrict . Aeson.encode+ simpleValue :: Matcher Aeson.Value -> Bool simpleValue = \case-  Hole {} ->-    True   Null {} ->     True   Bool {} ->@@ -174,5 +180,9 @@     False   Object {} ->     False+  Sig {} ->+    True+  Var {} ->+    True   Ext {} ->     True
src/Aeson/Match/QQ/Internal/Value.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveLift #-} {-# LANGUAGE LambdaCase #-}@@ -8,50 +7,41 @@ {-# 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 Data.Aeson ((.=))+import Data.Aeson qualified as Aeson+import Data.Aeson.KeyMap qualified as Aeson (toHashMapText)+import Data.CaseInsensitive (CI)+import Data.CaseInsensitive qualified as CI+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.List.NonEmpty (NonEmpty(..))+import Data.Scientific (Scientific)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Vector (Vector)+import Data.Vector qualified as Vector+import Language.Haskell.TH (Q, Exp(..), Lit(..))+import Language.Haskell.TH.Syntax (Lift(..))+import Prelude hiding (any, null)+import Text.PrettyPrint.HughesPJClass qualified as PP (Pretty(..)) -import           Aeson.Match.QQ.Internal.AesonUtils (toJSONE)+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+  = Null   | Bool Bool   | Number Scientific   | String Text@@ -61,18 +51,21 @@   | ArrayUO (Array ext)     -- ^ Unordered arrays   | Object (Object ext)+  | Var Text+    -- ^ Unless the name of the variable is '_', then+    -- the value is returned to the user in a 'HashMap'.+  | Sig Type Bool (Matcher ext)   | Ext ext     -- ^ External values spliced into a 'Matcher' using the `#{}` syntax     deriving (Show, Eq, Functor) +type Array ext = Box (Vector (Matcher ext))++type Object ext = Box (HashMap Text (NonEmpty (Matcher ext)))+ 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)         ]@@ -104,6 +97,16 @@         [ "type" .= ("object" :: Text)         , "value" .= v         ]+      Var name ->+        [ "type" .= ("var" :: Text)+        , "name" .= name+        ]+      Sig type_ nullable v ->+        [ "type" .= ("sig" :: Text)+        , "expected-type" .= type_+        , "nullable" .= nullable+        , "value" .= v+        ]       Ext v ->         [ "type" .= ("extension" :: Text)         , "value" .= v@@ -124,18 +127,12 @@       , "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 ->@@ -167,35 +164,30 @@   Object Box {values, extra} -> do     let       quoted =-        fmap toExp (traverse (traverse quote) (HashMap.toList values))+        fmap toExp (traverse (traverse (traverse quote)) (HashMap.toList values))       toExp =-        ListE . map (\(k, v) -> tup2 (LitE (StringL (Text.unpack k)), v))+        ListE . map (\(k, v) -> tup2 (LitE (StringL (Text.unpack k)), nonEmptyE v))       tup2 (a, b) =         TupE [Just a, Just b]+      nonEmptyE (e :| es) =+        AppE (AppE (ConE '(:|)) e) (ListE es)     [| Object Box          { values = HashMap.fromList $quoted          , extra          } :: Matcher Aeson.Value |]+  Sig type_ nullable val ->+    [| Sig type_ nullable $(quote val) :: Matcher Aeson.Value |]+  Var name ->+    [| Var name :: 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+  = AnyT+    -- ^ @_ : any@ or, equivalently, @_@+  | BoolT     -- ^ @_ : bool@   | NumberT     -- ^ @_ : number@@@ -214,7 +206,8 @@ instance Aeson.ToJSON Type where   toJSON =     Aeson.toJSON . \case-      BoolT {} -> "bool" :: Text+      AnyT {} -> "any" :: Text+      BoolT {} -> "bool"       NumberT {} -> "number"       StringT {} -> "string"       StringCIT {} -> "ci-string"@@ -224,6 +217,7 @@  instance PP.Pretty Type where   pPrint = \case+    AnyT {} -> "any"     BoolT {} -> "bool"     NumberT {} -> "number"     StringT {} -> "string"@@ -244,9 +238,5 @@     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}+    Object Box {values = fmap (pure . embed) (Aeson.toHashMapText o), extra = False}
+ test/Aeson/Match/QQ/Internal/ParseSpec.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE GeneralisedNewtypeDeriving #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+module Aeson.Match.QQ.Internal.ParseSpec (spec) where++import Data.Aeson qualified as Aeson+import Test.Hspec++import Aeson.Match.QQ+++spec :: Spec+spec = do+  it "specs" $ do+    [qq| _ |] `shouldBe` Sig AnyT False (Var "")+    [qq| _hole |] `shouldBe` Sig AnyT False (Var "hole")+    [qq| _"fancy hole" |] `shouldBe` Sig AnyT False (Var "fancy hole")+    [qq| _typed-hole : number |] `shouldBe` Sig NumberT False (Var "typed-hole")+    [qq| _typed-?-hole : number? |] `shouldBe` Sig NumberT True (Var "typed-?-hole")+    [qq| _ : any |] `shouldBe` Sig AnyT False (Var "")++    [qq| null |] `shouldBe` Null++    [qq| false |] `shouldBe` Bool False+    [qq| true |] `shouldBe` Bool True++    [qq| 4 |] `shouldBe` Number 4+    [qq| -7 |] `shouldBe` Number (-7)++    [qq| "foo" |] `shouldBe` String "foo"++    [qq| [] |] `shouldBe`+      Array Box {values = [], extra = False}+    [qq| [1, 2, 3] |] `shouldBe`+      Array Box {values = [Number 1, Number 2, Number 3], extra = False}+    [qq| [1, _, 3] |] `shouldBe`+      Array Box {values = [Number 1, Sig AnyT False (Var ""), Number 3], extra = False}+    [qq| [1, _, 3, ...] |] `shouldBe`+      Array Box {values = [Number 1, Sig AnyT False (Var ""), Number 3], extra = True}++    [qq| (unordered) [] |] `shouldBe`+      ArrayUO Box {values = [], extra = False}+    [qq| (unordered) [1, 2, 3] |] `shouldBe`+      ArrayUO Box {values = [Number 1, Number 2, Number 3], extra = False}+    [qq| (unordered) [1, _, 3] |] `shouldBe`+      ArrayUO Box {values = [Number 1, Sig AnyT False (Var ""), Number 3], extra = False}+    [qq| (unordered) [1, _, 3, ...] |] `shouldBe`+      ArrayUO Box {values = [Number 1, Sig AnyT False (Var ""), Number 3], extra = True}++    [qq| {} |] `shouldBe`+      Object Box {values = [], extra = False}+    [qq| {foo: 4} |] `shouldBe`+      Object Box {values = [("foo", [Number 4])], extra = False}+    [qq| {foo: 4, "bar": 7} |] `shouldBe`+      Object Box {values = [("foo", [Number 4]), ("bar", [Number 7])], extra = False}+    [qq| {foo: 4, "bar": 7, ...} |] `shouldBe`+      Object Box {values = [("foo", [Number 4]), ("bar", [Number 7])], extra = True}+    [qq| {foo: 4, foo: _name, "bar": 7} |] `shouldBe`+      Object Box+        { values =+          [ ("foo", [Number 4, Sig AnyT False (Var "name")])+          , ("bar", [Number 7])+          ]+        , extra = False+        }++    [qq| {foo: #{4 + 7 :: Int}} |] `shouldBe`+      Object Box {values = [("foo", [Ext (Aeson.Number 11)])], extra = False}+    [qq| {foo: #{4 + 7 :: ToEncoding Int}} |] `shouldBe`+      Object Box {values = [("foo", [Ext (Aeson.Number 11)])], extra = False}++newtype ToEncoding a = ToEncoding { unToEncoding :: a }+    deriving (Show, Eq, Num)++instance Aeson.ToJSON a => Aeson.ToJSON (ToEncoding a) where+  toJSON =+    error "ToJSON is undefined"+  toEncoding =+    Aeson.toEncoding . unToEncoding
test/Aeson/Match/QQ/Internal/PrettyPrintSpec.hs view
@@ -5,10 +5,10 @@ {-# LANGUAGE QuasiQuotes #-} module Aeson.Match.QQ.Internal.PrettyPrintSpec (spec) where -import           Test.Hspec+import Test.Hspec -import           Aeson.Match.QQ (qq)-import           Aeson.Match.QQ.Internal.PrettyPrint (pp)+import Aeson.Match.QQ (qq)+import Aeson.Match.QQ.Internal.PrettyPrint (pp)   spec :: Spec@@ -16,20 +16,24 @@   it "holes" $ do     pp [qq| _ |] `shouldBe`       "[qq|\n\-      \  _\n\+      \  _ : any\n\       \|]"     pp [qq| _hole |] `shouldBe`       "[qq|\n\-      \  _hole\n\+      \  _hole : any\n\       \|]"     pp [qq| _"fancy hole" |] `shouldBe`       "[qq|\n\-      \  _\"fancy hole\"\n\+      \  _\"fancy hole\" : any\n\       \|]"     pp [qq| _typed-hole : number |] `shouldBe`       "[qq|\n\       \  _typed-hole : number\n\       \|]"+    pp [qq| _nullable-hole : string? |] `shouldBe`+      "[qq|\n\+      \  _nullable-hole : string?\n\+      \|]"    it "basic values" $ do     pp [qq| null |] `shouldBe`@@ -152,5 +156,11 @@     pp [qq| {foo: #{spliced}} |] `shouldBe`       "[qq|\n\       \  { foo: 7\n\+      \  }\n\+      \|]"+    pp [qq| {foo: 4, foo: 7} |] `shouldBe`+      "[qq|\n\+      \  { foo: 4\n\+      \  , foo: 7\n\       \  }\n\       \|]"
test/Aeson/Match/QQSpec.hs view
@@ -5,70 +5,21 @@ {-# LANGUAGE QuasiQuotes #-} module Aeson.Match.QQSpec (spec) where -import qualified Data.Aeson as Aeson-import           Data.Aeson.QQ (aesonQQ)-import           Data.List.NonEmpty (NonEmpty(..))-import qualified Data.HashMap.Strict as HashMap-import           Test.Hspec+import Data.Aeson qualified as Aeson+import Data.Aeson.QQ (aesonQQ)+import Data.List.NonEmpty (NonEmpty(..))+import Data.HashMap.Strict qualified as HashMap+import Test.Hspec -import           Aeson.Match.QQ+import Aeson.Match.QQ   spec :: Spec spec = do-  describe "parse" $-    it "specs" $ do-      [qq| _ |] `shouldBe` Hole Nothing Nothing-      [qq| _hole |] `shouldBe` Hole Nothing (pure "hole")-      [qq| _"fancy hole" |] `shouldBe` Hole Nothing (pure "fancy hole")-      [qq| _typed-hole : number |] `shouldBe` Hole (pure (HoleSig NumberT False)) (pure "typed-hole")-      [qq| _typed-nullable-hole : number? |] `shouldBe` Hole (pure (HoleSig NumberT True)) (pure "typed-nullable-hole")--      [qq| null |] `shouldBe` Null--      [qq| false |] `shouldBe` Bool False-      [qq| true |] `shouldBe` Bool True--      [qq| 4 |] `shouldBe` Number 4-      [qq| -7 |] `shouldBe` Number (-7)--      [qq| "foo" |] `shouldBe` String "foo"--      [qq| [] |] `shouldBe`-        Array Box {values = [], extra = False}-      [qq| [1, 2, 3] |] `shouldBe`-        Array Box {values = [Number 1, Number 2, Number 3], extra = False}-      [qq| [1, _, 3] |] `shouldBe`-        Array Box {values = [Number 1, Hole Nothing Nothing, Number 3], extra = False}-      [qq| [1, _, 3, ...] |] `shouldBe`-        Array Box {values = [Number 1, Hole Nothing Nothing, Number 3], extra = True}--      [qq| (unordered) [] |] `shouldBe`-        ArrayUO Box {values = [], extra = False}-      [qq| (unordered) [1, 2, 3] |] `shouldBe`-        ArrayUO Box {values = [Number 1, Number 2, Number 3], extra = False}-      [qq| (unordered) [1, _, 3] |] `shouldBe`-        ArrayUO Box {values = [Number 1, Hole Nothing Nothing, Number 3], extra = False}-      [qq| (unordered) [1, _, 3, ...] |] `shouldBe`-        ArrayUO Box {values = [Number 1, Hole Nothing Nothing, Number 3], extra = True}--      [qq| {} |] `shouldBe`-        Object Box {values = [], extra = False}-      [qq| {foo: 4} |] `shouldBe`-        Object Box {values = [("foo", Number 4)], extra = False}-      [qq| {foo: 4, "bar": 7} |] `shouldBe`-        Object Box {values = [("foo", Number 4), ("bar", Number 7)], extra = False}-      [qq| {foo: 4, "bar": 7, ...} |] `shouldBe`-        Object Box {values = [("foo", Number 4), ("bar", Number 7)], extra = True}--      [qq| {foo: #{4 + 7 :: Int}} |] `shouldBe`-        Object Box {values = [("foo", Ext (Aeson.Number 11))], extra = False}-      [qq| {foo: #{4 + 7 :: ToEncoding Int}} |] `shouldBe`-        Object Box {values = [("foo", Ext (Aeson.Number 11))], extra = False}-   describe "match" $ do     it "specs" $ do       [qq| _ |] `shouldMatch` [aesonQQ| {foo: 4, bar: 7} |]+      [qq| _ : any |] `shouldMatch` [aesonQQ| {foo: 4, bar: 7} |]       [qq| null |] `shouldMatch` [aesonQQ| null |]       [qq| true |] `shouldMatch` [aesonQQ| true |]       [qq| false |] `shouldMatch` [aesonQQ| false |]@@ -116,6 +67,8 @@         { foo: _ : string         , bar: 7         } |] `shouldNotMatch` [aesonQQ| {foo: null, bar: 7} |]+      [qq| {foo: 4, foo: 7} |] `shouldNotMatch` [aesonQQ| {foo: 4} |]+      [qq| {foo: 4, foo: 7} |] `shouldNotMatch` [aesonQQ| {foo: 7} |]      it "paths" $ do       match [qq| {foo: {bar: {baz: [1, 4]}}} |] [aesonQQ| {foo: {bar: {baz: [1, 7]}}} |] `shouldBe`@@ -130,21 +83,28 @@         match [qq| {foo: _hole} |] [aesonQQ| {foo: {bar: {baz: [1, 4]}}} |] `shouldBe`           pure (HashMap.singleton "hole" [aesonQQ| {bar: {baz: [1, 4]}} |]) -      -- https://github.com/supki/aeson-match-qq/issues/26-      it "#26" $-        match [qq|-          { foo: _hole-          }-        |] [aesonQQ| {foo: {bar: {baz: [1, 4]}}} |] `shouldBe`-          pure (HashMap.singleton "hole" [aesonQQ| {bar: {baz: [1, 4]}} |])+      context "unordered array" $+        it "matches" $ do+          match [qq| (unordered) [1, _hole] |] [aesonQQ| [2, 1] |] `shouldBe`+            pure (HashMap.singleton "hole" [aesonQQ| 2 |])+          match [qq| (unordered) [{foo: _hole}, ...] |] [aesonQQ| [{foo: 2}, 1] |] `shouldBe`+            pure (HashMap.singleton "hole" [aesonQQ| 2 |]) -    context "unordered array" $-      it "named holes" $ do-        match [qq| (unordered) [1, _hole] |] [aesonQQ| [2, 1] |] `shouldBe`-          pure (HashMap.singleton "hole" [aesonQQ| 2 |])-        match [qq| (unordered) [{foo: _hole}, ...] |] [aesonQQ| [{foo: 2}, 1] |] `shouldBe`-          pure (HashMap.singleton "hole" [aesonQQ| 2 |])+      context "duplicate keys" $ do+        it "matches" $ do+          match [qq| {foo: _name, foo: 4} |] [aesonQQ| {foo: 4} |] `shouldBe`+            pure (HashMap.singleton "name" [aesonQQ| 4 |])+          match [qq| {foo: [_a, 7], foo: [4, _b]} |] [aesonQQ| {foo: [4, 7]} |] `shouldBe`+            pure [("a", [aesonQQ| 4 |]), ("b", [aesonQQ| 7 |])] +        it "doesn't match" $ do+          match [qq| {foo: _name, foo: 7} |] [aesonQQ| {foo: 4} |] `shouldBe`+            throwE (Mismatch MkMismatch+              { path = [Key "foo"]+              , matcher = [qq| 7 |]+              , given = Aeson.Number 4+              })+   describe "repro" $ do     -- https://github.com/supki/aeson-match-qq/issues/7     it "#7" $ do@@ -164,7 +124,7 @@       [qq| {foo: []} |] `shouldBe`         Object           (Box-            { values = [("foo", Array (Box {values = [], extra = False}))]+            { values = [("foo", [Array (Box {values = [], extra = False})])]             , extra = False             })       [qq| [{}] |] `shouldBe`@@ -216,6 +176,14 @@           , matcher = Null           , given = Aeson.Number 4           })++    -- https://github.com/supki/aeson-match-qq/issues/26+    it "#26" $+      match [qq|+        { foo: _hole+        }+      |] [aesonQQ| {foo: {bar: {baz: [1, 4]}}} |] `shouldBe`+        pure (HashMap.singleton "hole" [aesonQQ| {bar: {baz: [1, 4]}} |])      -- https://github.com/supki/aeson-match-qq/issues/28     it "#28" $ do