packages feed

aeson-match-qq 1.4.2 → 1.4.3

raw patch · 8 files changed

+146/−28 lines, 8 filesdep +case-insensitivePVP ok

version bump matches the API change (PVP)

Dependencies added: case-insensitive

API changes (from Hackage documentation)

+ Aeson.Match.QQ: StringCI :: CI Text -> Value ext
+ Aeson.Match.QQ: StringCIT :: Type
+ Aeson.Match.QQ: mistype :: Path -> Value Value -> Value -> Validation (NonEmpty VE) a
+ Aeson.Match.QQ.Internal.Match: Mistype :: Mismatch -> VE
+ Aeson.Match.QQ.Internal.Match: mistype :: Path -> Value Value -> Value -> Validation (NonEmpty VE) a
+ Aeson.Match.QQ.Internal.Value: StringCI :: CI Text -> Value ext
+ Aeson.Match.QQ.Internal.Value: StringCIT :: Type
+ Aeson.Match.QQ.Internal.Value: embed :: Value -> Value ext

Files

CHANGELOG.markdown view
@@ -1,3 +1,12 @@+1.4.3+=====++  * 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.4.2 ===== 
README.markdown view
@@ -40,4 +40,17 @@  `[match| {foo: {bar: _n} |]` will return `{n: 4}` if matched with `{foo: {bar: 4}}` +### Case-insensitive strings++A small addition to the JSON syntax that allows string matches that ignore case:++`[match| (ci) "foo" |]` matches any of `"Foo"`, `"fOO"`, or `"FOO"`.++### Unordered arrays++A small addition to the JSON syntax that allows array matches that ignore the order+of the elements, for example:++`[match| (unordered) [1,2,3] |]` matches `[3, 1, 2]`.+   [0]: https://hackage.haskell.org/package/aeson-qq
aeson-match-qq.cabal view
@@ -1,13 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.4.+-- This file has been generated from package.yaml by hpack version 0.34.7. -- -- see: https://github.com/sol/hpack------ hash: 5d1f836e56344a26982a1f67c2ccb2010e6ac6be94d88f239c4b15c9fe108df6  name:           aeson-match-qq-version:        1.4.2+version:        1.4.3 synopsis:       Declarative JSON matchers. description:    See README.markdown category:       Web@@ -42,6 +40,7 @@     , attoparsec     , base >=4.14 && <5     , bytestring+    , case-insensitive     , containers     , either     , haskell-src-meta
src/Aeson/Match/QQ.hs view
@@ -12,6 +12,7 @@   , qq   , match   , mismatch+  , mistype   , missingPathElem   , extraArrayValues   , extraObjectValues@@ -22,9 +23,26 @@ import           Language.Haskell.TH.Quote (QuasiQuoter(..)) import           Language.Haskell.TH.Syntax (Lift(..)) -import           Aeson.Match.QQ.Internal.Match (Path, PathElem(..), match, mismatch, missingPathElem, extraArrayValues, extraObjectValues)+import           Aeson.Match.QQ.Internal.Match+  ( Path+  , PathElem(..)+  , match+  , mismatch+  , mistype+  , 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
src/Aeson/Match/QQ/Internal/Match.hs view
@@ -12,9 +12,10 @@ import           Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.KeyMap as Aeson (toHashMapText)+import           Data.Bool (bool)+import qualified Data.CaseInsensitive as CI import           Data.Either.Validation (Validation(..), eitherToValidation) import           Data.Foldable (for_, toList)-import           Data.Bool (bool) import           Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import           Data.List.NonEmpty (NonEmpty)@@ -26,7 +27,14 @@ import qualified Data.Vector as Vector import           Prelude hiding (any, null) -import           Aeson.Match.QQ.Internal.Value (Value(..), Box(..), TypeSig(..), Type(..), Nullable(..))+import           Aeson.Match.QQ.Internal.Value+  ( Value(..)+  , Box(..)+  , TypeSig(..)+  , Type(..)+  , Nullable(..)+  , embed+  )   match@@ -38,35 +46,42 @@  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)-            mismatched+            mistyped         pure (maybe mempty (\name -> HashMap.singleton name val) nameO)       (Null, Aeson.Null) ->         pure mempty       (Null, _) -> do-        mismatched+        mistyped         pure mempty       (Bool b, Aeson.Bool b') -> do         unless (b == b') mismatched         pure mempty       (Bool _, _) -> do-        mismatched+        mistyped         pure mempty       (Number n, Aeson.Number n') -> do         unless (n == n') mismatched         pure mempty       (Number _, _) -> do-        mismatched+        mistyped         pure mempty       (String str, Aeson.String str') -> do         unless (str == str') mismatched         pure mempty       (String _, _) -> do-        mismatched+        mistyped         pure mempty+      (StringCI str, Aeson.String str') -> do+        unless (str == CI.mk str') mismatched+        pure mempty+      (StringCI _, _) -> do+        mistyped+        pure mempty       (Array Box {knownValues, extendable}, Aeson.Array arr) ->         let           fold f =@@ -81,12 +96,12 @@             (\i v -> maybe (missingPathElem (reverse path) (Idx i)) (go (Idx i : path) v) (arr Vector.!? i))             knownValues       (Array _, _) -> do-        mismatched+        mistyped         pure mempty       (ArrayUO box, Aeson.Array arr) ->         matchArrayUO mismatched path box arr       (ArrayUO _, _) -> do-        mismatched+        mistyped         pure mempty       (Object Box {knownValues, extendable}, Aeson.Object (Aeson.toHashMapText -> o)) ->         let fold f =@@ -101,11 +116,10 @@             (\k v -> maybe (missingPathElem (reverse path) (Key k)) (go (Key k : path) v) (HashMap.lookup k o))             knownValues       (Object _, _) -> do-        mismatched-        pure mempty-      (Ext val, val') -> do-        unless (val == val') mismatched+        mistyped         pure mempty+      (Ext val, val') ->+        go path (embed val) val'  holeTypeMatch :: TypeSig -> Aeson.Value -> Bool holeTypeMatch type_ val =@@ -114,7 +128,9 @@     (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 @@ -180,6 +196,10 @@ 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 {..})@@ -198,6 +218,7 @@  data VE   = Mismatch Mismatch+  | Mistype Mismatch   | MissingPathElem MissingPathElem   | ExtraArrayValues ExtraArrayValues   | ExtraObjectValues ExtraObjectValues@@ -208,6 +229,10 @@     Aeson.object . \case       Mismatch v ->         [ "type" .= ("mismatch" :: Text)+        , "value" .= v+        ]+      Mistype v ->+        [ "type" .= ("mistype" :: Text)         , "value" .= v         ]       MissingPathElem v ->
src/Aeson/Match/QQ/Internal/Parse.hs view
@@ -7,11 +7,12 @@  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,7 +51,7 @@     OpenSquareBracketP ->       array     OpenParenP ->-      arrayUO+      arrayUO <|> stringCI     OpenCurlyBracketP ->       object     HashP ->@@ -91,6 +92,12 @@ 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@@ -132,11 +139,7 @@  arrayUO :: Atto.Parser (Value Exp) arrayUO = do-  _ <- Atto.word8 OpenParenP-  spaces-  _ <- Atto.string "unordered"-  spaces-  _ <- Atto.word8 CloseParenP+  _ <- Atto.string "(unordered)"   spaces   Array box <- array   pure (ArrayUO box)@@ -209,7 +212,9 @@     [ p "bool" BoolT     , p "number" NumberT     , p "string" StringT+    , p "ci-string" StringCIT     , p "array" ArrayT+    , p "unordered-array" ArrayUOT     , p "object" ObjectT     ]  where@@ -239,9 +244,10 @@ pattern OpenSquareBracketP = 91 -- '[' pattern CloseSquareBracketP = 93 -- ']' -pattern OpenParenP, CloseParenP :: Word8+pattern OpenParenP :: Word8 pattern OpenParenP = 40 -- '('-pattern CloseParenP = 41 -- ')'+-- pattern CloseParenP :: Word8+-- pattern CloseParenP = 41 -- ')'  pattern OpenCurlyBracketP, CloseCurlyBracketP, ColonP :: Word8 pattern OpenCurlyBracketP = 123 -- '{'
src/Aeson/Match/QQ/Internal/Value.hs view
@@ -6,6 +6,7 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-} module Aeson.Match.QQ.Internal.Value   ( Value(..)   , Box(..)@@ -14,11 +15,15 @@   , TypeSig(..)   , Type(..)   , Nullable(..)+  , embed   ) where  import           Data.Aeson ((.=)) import qualified Data.Aeson as Aeson+import qualified Data.Aeson.KeyMap as Aeson (toHashMapText) 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)@@ -38,6 +43,7 @@   | Bool Bool   | Number Scientific   | String Text+  | StringCI (CI Text)   | Array (Array ext)   | ArrayUO (Array ext)   | Object (Object ext)@@ -67,6 +73,10 @@         [ "type" .= ("string" :: Text)         , "value" .= v         ]+      StringCI v ->+        [ "type" .= ("string-ci" :: Text)+        , "value" .= CI.original v+        ]       Array v ->         [ "type" .= ("array" :: Text)         , "value" .= v@@ -115,6 +125,8 @@       [| 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 =@@ -160,6 +172,7 @@   = BoolT   | NumberT   | StringT+  | StringCIT   | ArrayT   | ArrayUOT   | ObjectT@@ -171,6 +184,7 @@       BoolT {} -> "bool" :: Text       NumberT {} -> "number"       StringT {} -> "string"+      StringCIT {} -> "string-ci"       ArrayT {} -> "array"       ArrayUOT {} -> "array-unordered"       ObjectT {} -> "object"@@ -185,3 +199,18 @@     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 (Aeson.toHashMapText -> o) ->+    Object Box {knownValues = fmap embed o, extendable = False}
test/Aeson/Match/QQSpec.hs view
@@ -72,9 +72,12 @@       [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] |]@@ -93,6 +96,7 @@       [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] |]@@ -151,6 +155,21 @@     -- 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)