aeson-match-qq 1.3.2 → 1.4.0
raw patch · 7 files changed
+39/−274 lines, 7 filesdep −case-insensitivedep −containers
Dependencies removed: case-insensitive, containers
Files
- CHANGELOG.markdown +2/−12
- aeson-match-qq.cabal +5/−5
- src/Aeson/Match/QQ.hs +3/−22
- src/Aeson/Match/QQ/Internal/Match.hs +21/−118
- src/Aeson/Match/QQ/Internal/Parse.hs +1/−25
- src/Aeson/Match/QQ/Internal/Value.hs +3/−43
- test/Aeson/Match/QQSpec.hs +4/−49
CHANGELOG.markdown view
@@ -1,17 +1,7 @@-1.3.2-=====-- * Improved matching errors for embedded Haskell values (https://github.com/supki/aeson-match-qq/pull/18)-- * Separate error types for type- and value- level mismatches (https://github.com/supki/aeson-match-qq/pull/18)-- * Case insensitive strings (https://github.com/supki/aeson-match-qq/pull/17)--1.3.1+1.4.0 ===== - * Unordered arrays (https://github.com/supki/aeson-match-qq/pull/15) - * Proper Unicode support (https://github.com/supki/aeson-match-qq/pull/14)+ * Aeson 2.0 compatibility 1.3.0 =====
aeson-match-qq.cabal view
@@ -1,18 +1,20 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.7.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack+--+-- hash: eeb53a9a8a4433aa06146edfc817e03b1cef449f46c1866e6302e4f9bdfee782 name: aeson-match-qq-version: 1.3.2+version: 1.4.0 synopsis: Declarative JSON matchers. description: See README.markdown category: Web homepage: https://github.com/supki/aeson-match-qq#readme bug-reports: https://github.com/supki/aeson-match-qq/issues maintainer: matvey.aksenov@gmail.com-copyright: Matvey Aksenov 2022+copyright: Matvey Aksenov 2021 license: BSD2 license-file: LICENSE build-type: Simple@@ -40,8 +42,6 @@ , attoparsec , base >=4.14 && <5 , bytestring- , case-insensitive- , containers , either , haskell-src-meta , scientific
src/Aeson/Match/QQ.hs view
@@ -12,43 +12,24 @@ , qq , match , mismatch- , mistype , missingPathElem , extraArrayValues , extraObjectValues ) where import Data.String (IsString(..))-import qualified Data.Text.Encoding as Text import Language.Haskell.TH.Quote (QuasiQuoter(..)) import Language.Haskell.TH.Syntax (Lift(..)) -import Aeson.Match.QQ.Internal.Match- ( Path- , PathElem(..)- , match- , mismatch- , mistype- , missingPathElem- , extraArrayValues- , extraObjectValues- )+import Aeson.Match.QQ.Internal.Match (Path, PathElem(..), match, mismatch, missingPathElem, extraArrayValues, extraObjectValues) import Aeson.Match.QQ.Internal.Parse (parse)-import Aeson.Match.QQ.Internal.Value- ( Value(..)- , Box(..)- , Array- , Object- , TypeSig(..)- , Type(..)- , Nullable(..)- )+import Aeson.Match.QQ.Internal.Value (Value(..), Box(..), Array, Object, TypeSig(..), Type(..), Nullable(..)) qq :: QuasiQuoter qq = QuasiQuoter { quoteExp = \str ->- case parse (Text.encodeUtf8 (fromString str)) of+ case parse (fromString str) of Left err -> error ("Aeson.Match.QQ.qq: " ++ err) Right val ->
src/Aeson/Match/QQ/Internal/Match.hs view
@@ -4,88 +4,68 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ViewPatterns #-} module Aeson.Match.QQ.Internal.Match where import Control.Applicative (liftA2) import Control.Monad (unless) import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson-import Data.Bool (bool)-import qualified Data.CaseInsensitive as CI-import Data.Either.Validation (Validation(..), eitherToValidation)-import Data.Foldable (for_, toList)+import qualified Data.Aeson.KeyMap as Aeson (toHashMapText)+import Data.Either.Validation (Validation, eitherToValidation)+import Data.Foldable (for_) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.List.NonEmpty (NonEmpty)-import Data.Maybe (mapMaybe)-import qualified Data.Set as Set import Data.String (IsString(..)) import Data.Text (Text) import Data.Vector (Vector) import qualified Data.Vector as Vector import Prelude hiding (any, null) -import Aeson.Match.QQ.Internal.Value- ( Value(..)- , Box(..)- , TypeSig(..)- , Type(..)- , Nullable(..)- , embed- )+import Aeson.Match.QQ.Internal.Value (Value(..), Box(..), TypeSig(..), Type(..), Nullable(..)) -match- :: Value Aeson.Value- -> Aeson.Value- -> Validation (NonEmpty VE) (HashMap Text Aeson.Value)+match :: Value Aeson.Value -> Aeson.Value -> Validation (NonEmpty VE) (HashMap Text Aeson.Value) match = go [] where go path matcher given = do let mismatched = mismatch (reverse path) matcher given- mistyped = mistype (reverse path) matcher given case (matcher, given) of (Any holeTypeO nameO, val) -> do for_ holeTypeO $ \holeType -> unless (holeTypeMatch holeType val)- mistyped+ mismatched pure (maybe mempty (\name -> HashMap.singleton name val) nameO) (Null, Aeson.Null) -> pure mempty (Null, _) -> do- mistyped+ mismatched pure mempty (Bool b, Aeson.Bool b') -> do unless (b == b') mismatched pure mempty (Bool _, _) -> do- mistyped+ mismatched pure mempty (Number n, Aeson.Number n') -> do unless (n == n') mismatched pure mempty (Number _, _) -> do- mistyped+ mismatched pure mempty (String str, Aeson.String str') -> do unless (str == str') mismatched pure mempty (String _, _) -> do- mistyped- pure mempty- (StringCI str, Aeson.String str') -> do- unless (str == CI.mk str') mismatched- pure mempty- (StringCI _, _) -> do- mistyped+ mismatched pure mempty (Array Box {knownValues, extendable}, Aeson.Array arr) ->- let- fold f =- Vector.ifoldr (\i v a -> liftA2 HashMap.union a (f i v)) (pure mempty)- extraValues =- Vector.drop (Vector.length knownValues) arr+ let fold f =+ Vector.ifoldr (\i v a -> liftA2 HashMap.union a (f i v)) (pure mempty)+ extraValues =+ Vector.drop (Vector.length knownValues) arr in unless (extendable || Vector.null extraValues)@@ -94,14 +74,9 @@ (\i v -> maybe (missingPathElem (reverse path) (Idx i)) (go (Idx i : path) v) (arr Vector.!? i)) knownValues (Array _, _) -> do- mistyped- pure mempty- (ArrayUO box, Aeson.Array arr) ->- matchArrayUO mismatched path box arr- (ArrayUO _, _) -> do- mistyped+ mismatched pure mempty- (Object Box {knownValues, extendable}, Aeson.Object o) ->+ (Object Box {knownValues, extendable}, Aeson.Object (Aeson.toHashMapText -> o)) -> let fold f = HashMap.foldrWithKey (\k v a -> liftA2 HashMap.union a (f k v)) (pure mempty) extraValues =@@ -114,10 +89,11 @@ (\k v -> maybe (missingPathElem (reverse path) (Key k)) (go (Key k : path) v) (HashMap.lookup k o)) knownValues (Object _, _) -> do- mistyped+ mismatched pure mempty- (Ext val, val') ->- go path (embed val) val'+ (Ext val, val') -> do+ unless (val == val') mismatched+ pure mempty holeTypeMatch :: TypeSig -> Aeson.Value -> Bool holeTypeMatch type_ val =@@ -126,78 +102,14 @@ (TypeSig {type_ = BoolT} , Aeson.Bool {}) -> True (TypeSig {type_ = NumberT} , Aeson.Number {}) -> True (TypeSig {type_ = StringT} , Aeson.String {}) -> True- (TypeSig {type_ = StringCIT} , Aeson.String {}) -> True (TypeSig {type_ = ArrayT} , Aeson.Array {}) -> True- (TypeSig {type_ = ArrayUOT} , Aeson.Array {}) -> True (TypeSig {type_ = ObjectT} , Aeson.Object {}) -> True (_, _) -> False -matchArrayUO- :: Validation (NonEmpty VE) (HashMap Text Aeson.Value)- -> Path- -> Box (Vector (Value Aeson.Value))- -> Vector Aeson.Value- -> Validation (NonEmpty VE) (HashMap Text Aeson.Value)-matchArrayUO mismatched path Box {knownValues, extendable} xs = do- -- Collect possible indices in `xs` for each position in `knownValues`.- let indices = map (collectMatchingIndices (toList xs)) (toList knownValues)- -- Find all unique valid ways to map each position in `knownValues` to- -- a member of `xs`.- case allIndicesAssignments indices of- -- If no assignment has been found, we give up.- [] ->- mismatched- ivs : _- -- If some positions in `knownValues` cannot be mapped to- -- anything in `xs`, we give up.- | length ivs < length knownValues ->- mismatched- -- If there are some members of `xs` that aren't matched by- -- anything in `knownValues`, we check if the pattern is- -- extendable.- | length ivs < length xs && not extendable -> do- let is = Set.fromList (map fst ivs)- extraValues = Vector.ifilter (\i _ -> not (i `Set.member` is)) xs- extraArrayValues (reverse path) extraValues- | otherwise ->- pure (foldMap snd ivs)- where- collectMatchingIndices is knownValue =- imapMaybe matchingIndex is- where- matchingIndex i x =- case match knownValue x of- Success vs ->- Just (i, vs)- Failure _ ->- Nothing- allIndicesAssignments = map (map unI) . cleanUp . go Set.empty- where- go _ [] = [[]]- go known (is : iss) = do- (i, vs) <- is- bool (map (I (i, vs) :) (go (Set.insert i known) iss)) [] (i `Set.member` known)- cleanUp =- toList . Set.fromList . map (Set.toAscList . Set.fromList)--newtype I = I { unI :: (Int, HashMap Text Aeson.Value) }--instance Eq I where- I (a, _) == I (b, _) =- a == b--instance Ord I where- I (a, _) `compare` I (b, _) =- a `compare` b- mismatch :: Path -> Value Aeson.Value -> Aeson.Value -> Validation (NonEmpty VE) a mismatch path matcher given = throwE (Mismatch MkMismatch {..}) -mistype :: Path -> Value Aeson.Value -> Aeson.Value -> Validation (NonEmpty VE) a-mistype path matcher given =- throwE (Mistype MkMismatch {..})- missingPathElem :: Path -> PathElem -> Validation (NonEmpty VE) a missingPathElem path missing = throwE (MissingPathElem MkMissingPathElem {..})@@ -216,7 +128,6 @@ data VE = Mismatch Mismatch- | Mistype Mismatch | MissingPathElem MissingPathElem | ExtraArrayValues ExtraArrayValues | ExtraObjectValues ExtraObjectValues@@ -229,10 +140,6 @@ [ "type" .= ("mismatch" :: Text) , "value" .= v ]- Mistype v ->- [ "type" .= ("mistype" :: Text)- , "value" .= v- ] MissingPathElem v -> [ "type" .= ("missing-path-elem" :: Text) , "value" .= v@@ -313,7 +220,3 @@ instance IsString PathElem where fromString = Key . fromString--imapMaybe :: (Int -> a -> Maybe b) -> [a] -> [b]-imapMaybe f =- mapMaybe (uncurry f) . zip [0..]
src/Aeson/Match/QQ/Internal/Parse.hs view
@@ -7,12 +7,11 @@ import Control.Applicative ((<|>), optional) import qualified Data.Aeson.Parser as Aeson-import qualified Data.Attoparsec.ByteString as Atto import qualified Data.ByteString as ByteString -- cannot use .Text here due to .Aeson parsers being tied to .ByteString+import qualified Data.Attoparsec.ByteString as Atto import Data.Bool (bool) 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@@ -50,8 +49,6 @@ string OpenSquareBracketP -> array- OpenParenP ->- arrayUO <|> stringCI OpenCurlyBracketP -> object HashP ->@@ -92,12 +89,6 @@ string = fmap String Aeson.jstring -stringCI :: Atto.Parser (Value Exp)-stringCI = do- _ <- Atto.string "(ci)"- spaces- fmap (StringCI . CI.mk) Aeson.jstring- array :: Atto.Parser (Value Exp) array = do _ <- Atto.word8 OpenSquareBracketP@@ -137,13 +128,6 @@ _ -> error "impossible" -arrayUO :: Atto.Parser (Value Exp)-arrayUO = do- _ <- Atto.string "(unordered)"- spaces- Array box <- array- pure (ArrayUO box)- object :: Atto.Parser (Value Exp) object = do _ <- Atto.word8 OpenCurlyBracketP@@ -212,9 +196,7 @@ [ p "bool" BoolT , p "number" NumberT , p "string" StringT- , p "ci-string" StringCIT , p "array" ArrayT- , p "unordered-array" ArrayUOT , p "object" ObjectT ] where@@ -244,15 +226,9 @@ pattern OpenSquareBracketP = 91 -- '[' pattern CloseSquareBracketP = 93 -- ']' -pattern OpenParenP :: Word8-pattern OpenParenP = 40 -- '('--- pattern CloseParenP :: Word8--- pattern CloseParenP = 41 -- ')'- pattern OpenCurlyBracketP, CloseCurlyBracketP, ColonP :: Word8 pattern OpenCurlyBracketP = 123 -- '{' pattern CloseCurlyBracketP = 125 -- '}'- pattern ColonP = 58 -- ':' pattern ZeroP, NineP, MinusP :: Word8
src/Aeson/Match/QQ/Internal/Value.hs view
@@ -6,7 +6,6 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE ViewPatterns #-} module Aeson.Match.QQ.Internal.Value ( Value(..) , Box(..)@@ -15,14 +14,11 @@ , TypeSig(..) , Type(..) , Nullable(..)- , embed ) where import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.Encoding.Internal as Aeson (encodingToLazyByteString)-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)@@ -32,7 +28,7 @@ import Data.Vector (Vector) import qualified Data.Vector as Vector import Language.Haskell.TH (Exp(..), Lit(..))-import Language.Haskell.TH.Syntax (Lift(..))+import Language.Haskell.TH.Syntax (Lift(..), unsafeTExpCoerce) import Prelude hiding (any, null) @@ -42,9 +38,7 @@ | Bool Bool | Number Scientific | String Text- | StringCI (CI Text) | Array (Array ext)- | ArrayUO (Array ext) | Object (Object ext) | Ext ext deriving (Show, Eq)@@ -72,18 +66,10 @@ [ "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@@ -124,8 +110,6 @@ [| Number (fromRational $(pure (LitE (RationalL (toRational n))))) :: Value Aeson.Value |] String str -> [| String (fromString $(pure (LitE (textL str)))) :: Value Aeson.Value |]- StringCI str ->- [| StringCI (fromString $(pure (LitE (textL (CI.original str))))) :: Value Aeson.Value |] Array Box {knownValues, extendable} -> [| Array Box { knownValues =@@ -133,13 +117,6 @@ , extendable } :: Value Aeson.Value |]- ArrayUO Box {knownValues, extendable} -> [|- ArrayUO Box- { knownValues =- Vector.fromList $(fmap (ListE . Vector.toList) (traverse lift knownValues))- , extendable- } :: Value Aeson.Value- |] Object Box {knownValues, extendable} -> [| Object Box { knownValues =@@ -152,6 +129,8 @@ where textL = StringL . Text.unpack+ liftTyped =+ unsafeTExpCoerce . lift data TypeSig = TypeSig { type_ :: Type@@ -169,9 +148,7 @@ = BoolT | NumberT | StringT- | StringCIT | ArrayT- | ArrayUOT | ObjectT deriving (Show, Eq, Lift) @@ -181,9 +158,7 @@ BoolT {} -> "bool" :: Text NumberT {} -> "number" StringT {} -> "string"- StringCIT {} -> "string-ci" ArrayT {} -> "array"- ArrayUOT {} -> "array-unordered" ObjectT {} -> "object" data Nullable@@ -196,18 +171,3 @@ Aeson.toJSON . \case Nullable -> True NonNullable -> False--embed :: Aeson.Value -> Value 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 {knownValues = fmap embed xs, extendable = False}- Aeson.Object o ->- Object Box {knownValues = fmap embed o, extendable = False}
test/Aeson/Match/QQSpec.hs view
@@ -41,15 +41,6 @@ [qq| [1, _, 3, ...] |] `shouldBe` Array Box {knownValues = [Number 1, Any Nothing Nothing, Number 3], extendable = True} - [qq| (unordered) [] |] `shouldBe`- ArrayUO Box {knownValues = [], extendable = False}- [qq| (unordered) [1, 2, 3] |] `shouldBe`- ArrayUO Box {knownValues = [Number 1, Number 2, Number 3], extendable = False}- [qq| (unordered) [1, _, 3] |] `shouldBe`- ArrayUO Box {knownValues = [Number 1, Any Nothing Nothing, Number 3], extendable = False}- [qq| (unordered) [1, _, 3, ...] |] `shouldBe`- ArrayUO Box {knownValues = [Number 1, Any Nothing Nothing, Number 3], extendable = True}- [qq| {} |] `shouldBe` Object Box {knownValues = [], extendable = False} [qq| {foo: 4} |] `shouldBe`@@ -72,19 +63,9 @@ [qq| false |] `shouldMatch` [aesonQQ| false |] [qq| 4 |] `shouldMatch` [aesonQQ| 4 |] [qq| "foo" |] `shouldMatch` [aesonQQ| "foo" |]- [qq| (ci) "foo" |] `shouldMatch` [aesonQQ| "Foo" |] [qq| [1, 2, 3] |] `shouldMatch` [aesonQQ| [1, 2, 3] |] [qq| [1, _ : number, 3, ...] |] `shouldMatch` [aesonQQ| [1, 2, 3, 4] |] [qq| [1, _ : string] |] `shouldMatch` [aesonQQ| [1, "foo"] |]- [qq| [1, _ : ci-string] |] `shouldMatch` [aesonQQ| [1, "foo"] |]- [qq| [1, _ : unordered-array] |] `shouldMatch` [aesonQQ| [1, ["foo"]] |]- [qq| (unordered) [] |] `shouldMatch` [aesonQQ| [] |]- [qq| (unordered) [1, 2, 3] |] `shouldMatch` [aesonQQ| [1, 2, 3] |]- [qq| (unordered) [1, 2, 3] |] `shouldMatch` [aesonQQ| [2, 3, 1] |]- [qq| (unordered) [1, 2, 2] |] `shouldMatch` [aesonQQ| [2, 2, 1] |]- [qq| (unordered) [1, _, 2] |] `shouldMatch` [aesonQQ| [2, 2, 1] |]- [qq| (unordered) [1, 2, ...] |] `shouldMatch` [aesonQQ| [2, 3, 1] |]- [qq| (unordered) [1, 2, ...] |] `shouldMatch` [aesonQQ| [2, 2, 1] |] [qq| {foo: 4, bar: 7} |] `shouldMatch` [aesonQQ| {foo: 4, bar: 7} |] [qq| {foo: 4, bar: 7, ...} |] `shouldMatch` [aesonQQ| {foo: 4, bar: 7, baz: 11} |] [qq| #{1 + 2 :: Int} |] `shouldMatch` [aesonQQ| 3 |]@@ -96,20 +77,20 @@ [qq| false |] `shouldNotMatch` [aesonQQ| true |] [qq| 4 |] `shouldNotMatch` [aesonQQ| 7 |] [qq| "foo" |] `shouldNotMatch` [aesonQQ| "bar" |]- [qq| (ci) "foo" |] `shouldNotMatch` [aesonQQ| "Bar" |] [qq| [1, 2, 3] |] `shouldNotMatch` [aesonQQ| [1, 2, 3, 4] |] [qq| [1, 2, 3, ...] |] `shouldNotMatch` [aesonQQ| [1, 2] |] [qq| [1, _ : string] |] `shouldNotMatch` [aesonQQ| [1, 2] |] [qq| [1, 2, 3, ...] |] `shouldNotMatch` [aesonQQ| [1, 2, 4] |]- [qq| (unordered) [1, 2, 3] |] `shouldNotMatch` [aesonQQ| [1, 2, 4] |]- [qq| (unordered) [1, 2, 3, 4] |] `shouldNotMatch` [aesonQQ| [1, 2, 3] |]- [qq| (unordered) [1, 2] |] `shouldNotMatch` [aesonQQ| [1, 2, 2] |] [qq| {foo: 4, bar: 7} |] `shouldNotMatch` [aesonQQ| {foo: 7, bar: 4} |] [qq| {foo: 4, bar: 7} |] `shouldNotMatch` [aesonQQ| {foo: 4, baz: 7} |] [qq| {foo: 4, bar: 7, ...} |] `shouldNotMatch` [aesonQQ| {foo: 4, baz: 11} |] [qq| #{1 + 2 :: Int} |] `shouldNotMatch` [aesonQQ| 4 |] [qq| {foo: _ : number, bar: 7} |] `shouldNotMatch` [aesonQQ| {foo: "foo", bar: 7} |] [qq| {foo: _ : number, bar: 7} |] `shouldNotMatch` [aesonQQ| {foo: null, bar: 7} |]+ [qq|+ { foo: _ : string+ , bar: 7+ } |] `shouldNotMatch` [aesonQQ| {foo: null, bar: 7} |] it "paths" $ do match [qq| {foo: {bar: {baz: [1, 4]}}} |] [aesonQQ| {foo: {bar: {baz: [1, 7]}}} |] `shouldBe`@@ -119,13 +100,6 @@ match [qq| {foo: _hole} |] [aesonQQ| {foo: {bar: {baz: [1, 4]}}} |] `shouldBe` pure (HashMap.singleton "hole" [aesonQQ| {bar: {baz: [1, 4]}} |]) - 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 |])- -- https://github.com/supki/aeson-match-qq/issues/7 it "#7" $ do match [qq| {foo: _} |] [aesonQQ| {} |] `shouldBe`@@ -147,25 +121,6 @@ { knownValues = [Object (Box {knownValues = [], extendable = False})] , extendable = False })-- -- https://github.com/supki/aeson-match-qq/issues/13- it "#13" $- [qq| "Слава Україні" |] `shouldMatch` [aesonQQ| "Слава Україні" |]-- -- https://github.com/supki/aeson-match-qq/issues/18- it "#18" $ do- -- string ~ string- match [qq| "foo" |] [aesonQQ| "bar" |] `shouldBe`- mismatch [] (String "foo") (Aeson.String "bar")- -- string !~ number- match [qq| "foo" |] [aesonQQ| 4 |] `shouldBe`- mistype [] (String "foo") (Aeson.Number 4)- -- string !~ null- match [qq| "foo" |] [aesonQQ| null |] `shouldBe`- mistype [] (String "foo") Aeson.Null- -- null !~ number- match [qq| null |] [aesonQQ| 4 |] `shouldBe`- mistype [] Null (Aeson.Number 4) newtype ToEncoding a = ToEncoding { unToEncoding :: a } deriving (Show, Eq, Num)