packages feed

aeson-match-qq 1.0.0 → 1.1.0

raw patch · 7 files changed

+149/−34 lines, 7 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

+ Aeson.Match.QQ: ArrayT :: Type
+ Aeson.Match.QQ: BoolT :: Type
+ Aeson.Match.QQ: NonNullable :: Nullable
+ Aeson.Match.QQ: Nullable :: Nullable
+ Aeson.Match.QQ: NumberT :: Type
+ Aeson.Match.QQ: ObjectT :: Type
+ Aeson.Match.QQ: StringT :: Type
+ Aeson.Match.QQ: TypeSig :: Type -> Nullable -> TypeSig
+ Aeson.Match.QQ: [nullable] :: TypeSig -> Nullable
+ Aeson.Match.QQ: [type_] :: TypeSig -> Type
+ Aeson.Match.QQ: data Nullable
+ Aeson.Match.QQ: data Type
+ Aeson.Match.QQ: data TypeSig
+ Aeson.Match.QQ.Internal.Match: holeTypeMatch :: TypeSig -> Value -> Bool
+ Aeson.Match.QQ.Internal.Value: ArrayT :: Type
+ Aeson.Match.QQ.Internal.Value: BoolT :: Type
+ Aeson.Match.QQ.Internal.Value: NonNullable :: Nullable
+ Aeson.Match.QQ.Internal.Value: Nullable :: Nullable
+ Aeson.Match.QQ.Internal.Value: NumberT :: Type
+ Aeson.Match.QQ.Internal.Value: ObjectT :: Type
+ Aeson.Match.QQ.Internal.Value: StringT :: Type
+ Aeson.Match.QQ.Internal.Value: TypeSig :: Type -> Nullable -> TypeSig
+ Aeson.Match.QQ.Internal.Value: [nullable] :: TypeSig -> Nullable
+ Aeson.Match.QQ.Internal.Value: [type_] :: TypeSig -> Type
+ Aeson.Match.QQ.Internal.Value: data Nullable
+ Aeson.Match.QQ.Internal.Value: data Type
+ Aeson.Match.QQ.Internal.Value: data TypeSig
+ Aeson.Match.QQ.Internal.Value: instance Data.Aeson.Types.ToJSON.ToJSON Aeson.Match.QQ.Internal.Value.Nullable
+ Aeson.Match.QQ.Internal.Value: instance Data.Aeson.Types.ToJSON.ToJSON Aeson.Match.QQ.Internal.Value.Type
+ Aeson.Match.QQ.Internal.Value: instance Data.Aeson.Types.ToJSON.ToJSON Aeson.Match.QQ.Internal.Value.TypeSig
+ Aeson.Match.QQ.Internal.Value: instance GHC.Classes.Eq Aeson.Match.QQ.Internal.Value.Nullable
+ Aeson.Match.QQ.Internal.Value: instance GHC.Classes.Eq Aeson.Match.QQ.Internal.Value.Type
+ Aeson.Match.QQ.Internal.Value: instance GHC.Classes.Eq Aeson.Match.QQ.Internal.Value.TypeSig
+ Aeson.Match.QQ.Internal.Value: instance GHC.Show.Show Aeson.Match.QQ.Internal.Value.Nullable
+ Aeson.Match.QQ.Internal.Value: instance GHC.Show.Show Aeson.Match.QQ.Internal.Value.Type
+ Aeson.Match.QQ.Internal.Value: instance GHC.Show.Show Aeson.Match.QQ.Internal.Value.TypeSig
+ Aeson.Match.QQ.Internal.Value: instance Language.Haskell.TH.Syntax.Lift Aeson.Match.QQ.Internal.Value.Nullable
+ Aeson.Match.QQ.Internal.Value: instance Language.Haskell.TH.Syntax.Lift Aeson.Match.QQ.Internal.Value.Type
+ Aeson.Match.QQ.Internal.Value: instance Language.Haskell.TH.Syntax.Lift Aeson.Match.QQ.Internal.Value.TypeSig
- Aeson.Match.QQ: Any :: Maybe Text -> Value ext
+ Aeson.Match.QQ: Any :: Maybe TypeSig -> Maybe Text -> Value ext
- Aeson.Match.QQ.Internal.Value: Any :: Maybe Text -> Value ext
+ Aeson.Match.QQ.Internal.Value: Any :: Maybe TypeSig -> Maybe Text -> Value ext

Files

+ CHANGELOG.markdown view
@@ -0,0 +1,11 @@+1.1.0+=====++  * Optionally typed holes (https://github.com/supki/aeson-match-qq/pull/9)++  * Fixed spurious validation errors (https://github.com/supki/aeson-match-qq/issues/7)++1.0.0+=====++  * Initial release.
aeson-match-qq.cabal view
@@ -4,11 +4,11 @@ -- -- see: https://github.com/sol/hpack ----- hash: 57b8bf8d01a999a9e081c25a75f1254dbf1f7d23805594b588bed35eb03b9a7a+-- hash: b37b8f340b6ce5d17c4938172bdd5e7805347f8290f68163f3215e5b7cfd8a14  name:           aeson-match-qq-version:        1.0.0-synopsis:       Matching Aeson values with a quasiquoter+version:        1.1.0+synopsis:       Declarative JSON matchers. description:    See README.markdown category:       Web maintainer:     matvey.aksenov@gmail.com@@ -18,6 +18,7 @@ build-type:     Simple extra-source-files:     README.markdown+    CHANGELOG.markdown  library   exposed-modules:@@ -33,7 +34,7 @@   build-depends:       aeson     , attoparsec-    , base >=4.7 && <5+    , base >=4.11 && <5     , bytestring     , either     , haskell-src-meta@@ -57,7 +58,7 @@       aeson     , aeson-match-qq     , aeson-qq-    , base >=4.7 && <5+    , base >=4.11 && <5     , hspec     , unordered-containers   default-language: Haskell2010
src/Aeson/Match/QQ.hs view
@@ -3,6 +3,9 @@   , Array   , Object   , Box(..)+  , TypeSig(..)+  , Type(..)+  , Nullable(..)   , Path   , PathElem(..)   , parse@@ -20,7 +23,7 @@  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)+import           Aeson.Match.QQ.Internal.Value (Value(..), Box(..), Array, Object, TypeSig(..), Type(..), Nullable(..))   qq :: QuasiQuoter
src/Aeson/Match/QQ/Internal/Match.hs view
@@ -11,6 +11,7 @@ import           Data.Aeson ((.=)) import qualified Data.Aeson as Aeson 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)@@ -20,7 +21,7 @@ import qualified Data.Vector as Vector import           Prelude hiding (any, null) -import           Aeson.Match.QQ.Internal.Value (Value(..), Box(..))+import           Aeson.Match.QQ.Internal.Value (Value(..), Box(..), TypeSig(..), Type(..), Nullable(..))   match :: Value Aeson.Value -> Aeson.Value -> Validation (NonEmpty VE) (HashMap Text Aeson.Value)@@ -30,10 +31,11 @@   go path matcher given = do     let mismatched = mismatch (reverse path) matcher given     case (matcher, given) of-      (Any Nothing, _) ->-        pure mempty-      (Any (Just name), val) ->-        pure (HashMap.singleton name val)+      (Any holeTypeO nameO, val) -> do+        for_ holeTypeO $ \holeType ->+          unless (holeTypeMatch holeType val)+            mismatched+        pure (maybe mempty (\name -> HashMap.singleton name val) nameO)       (Null, Aeson.Null) ->         pure mempty       (Null, _) -> do@@ -58,14 +60,14 @@         mismatched         pure mempty       (Array Box {knownValues, extendable}, Aeson.Array arr) ->-        let knownValuesL = Vector.length knownValues-            arrL = Vector.length arr-            fold f =+        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 || knownValuesL == arrL)-            (extraArrayValues (reverse path) (Vector.drop knownValuesL arr)) *>+            (extendable || Vector.null extraValues)+            (extraArrayValues (reverse path) extraValues) *>           fold             (\i v -> maybe (missingPathElem (reverse path) (Idx i)) (go (Idx i : path) v) (arr Vector.!? i))             knownValues@@ -73,14 +75,14 @@         mismatched         pure mempty       (Object Box {knownValues, extendable}, Aeson.Object o) ->-        let knownValuesL = HashMap.size knownValues-            oL = HashMap.size o-            fold f =+        let fold f =               HashMap.foldrWithKey (\k v a -> liftA2 HashMap.union a (f k v)) (pure mempty)+            extraValues =+              HashMap.difference o knownValues         in           unless-            (extendable || knownValuesL == oL)-            (extraObjectValues (reverse path) (HashMap.difference o knownValues)) *>+            (extendable || HashMap.null extraValues)+            (extraObjectValues (reverse path) extraValues) *>           fold             (\k v -> maybe (missingPathElem (reverse path) (Key k)) (go (Key k : path) v) (HashMap.lookup k o))             knownValues@@ -90,6 +92,17 @@       (Ext val, val') -> do         unless (val == val') mismatched         pure mempty++holeTypeMatch :: TypeSig -> Aeson.Value -> Bool+holeTypeMatch type_ val =+  case (type_, val) of+    (TypeSig {nullable = Nullable}, Aeson.Null) -> True+    (TypeSig {type_ = BoolT} , Aeson.Bool {}) -> True+    (TypeSig {type_ = NumberT} , Aeson.Number {}) -> True+    (TypeSig {type_ = StringT} , Aeson.String {}) -> True+    (TypeSig {type_ = ArrayT} , Aeson.Array {}) -> True+    (TypeSig {type_ = ObjectT} , Aeson.Object {}) -> True+    (_, _) -> False  mismatch :: Path -> Value Aeson.Value -> Aeson.Value -> Validation (NonEmpty VE) a mismatch path matcher given =
src/Aeson/Match/QQ/Internal/Parse.hs view
@@ -5,14 +5,17 @@   ( parse   ) where -import           Control.Applicative ((<|>))+import           Control.Applicative ((<|>), optional) import qualified Data.Aeson.Parser as Aeson 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.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@@ -22,7 +25,7 @@ import           Language.Haskell.TH (Exp(..)) import           Prelude hiding (any, null) -import           Aeson.Match.QQ.Internal.Value (Value(..), Box(..))+import           Aeson.Match.QQ.Internal.Value (Value(..), Box(..), TypeSig(..), Type(..), Nullable(..))   parse :: ByteString -> Either String (Value Exp)@@ -61,7 +64,10 @@ any :: Atto.Parser (Value Exp) any = do   _ <- Atto.word8 AnyP-  fmap (Any . Just) key <|> pure (Any Nothing)+  name <- fmap Just key <|> pure Nothing+  spaces+  expectedType <- optional typeSig+  pure (Any expectedType name)  null :: Atto.Parser (Value Exp) null =@@ -180,6 +186,23 @@     str <- Atto.takeWhile1 (/= CloseCurlyBracketP) <* Atto.word8 CloseCurlyBracketP     either fail pure (parseExp (Text.unpack (Text.decodeUtf8 str))) +typeSig :: Atto.Parser TypeSig+typeSig = do+  _ <- Atto.word8 ColonP+  spaces+  asum+    [ p "bool" BoolT+    , p "number" NumberT+    , p "string" StringT+    , p "array" ArrayT+    , p "object" ObjectT+    ]+ where+  p name typeName = do+    _ <- Atto.string name+    q <- optional (Atto.word8 QuestionMarkP)+    pure (TypeSig typeName (bool NonNullable Nullable (isJust q)))+ -- This function has been stolen from aeson. -- ref: https://hackage.haskell.org/package/aeson-1.4.6.0/docs/src/Data.Aeson.Parser.Internal.html#skipSpace spaces :: Atto.Parser ()@@ -216,3 +239,6 @@ pattern NewLineP = 0x0a pattern CRP = 0x0d pattern TabP = 0x09++pattern QuestionMarkP :: Word8+pattern QuestionMarkP = 63 -- '?'
src/Aeson/Match/QQ/Internal/Value.hs view
@@ -1,7 +1,9 @@+{-# LANGUAGE DeriveLift #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} module Aeson.Match.QQ.Internal.Value@@ -9,6 +11,9 @@   , Box(..)   , Array   , Object+  , TypeSig(..)+  , Type(..)+  , Nullable(..)   ) where  import           Data.Aeson ((.=))@@ -28,7 +33,7 @@   data Value ext-  = Any (Maybe Text)+  = Any (Maybe TypeSig) (Maybe Text)   | Null   | Bool Bool   | Number Scientific@@ -41,8 +46,9 @@ instance Aeson.ToJSON ext => Aeson.ToJSON (Value ext) where   toJSON =     Aeson.object . \case-      Any name ->+      Any type_ name ->         [ "type" .= ("any" :: Text)+        , "expected-type" .= type_         , "name" .= name         ]       Null ->@@ -94,8 +100,8 @@ -- undefined for some datatypes. instance ext ~ Exp => Lift (Value ext) where   lift = \case-    Any name ->-      [| Any $(pure (maybe (ConE 'Nothing) (AppE (ConE 'Just) . AppE (VarE 'fromString) . LitE . textL) name)) :: Value Aeson.Value |]+    Any type_ name ->+      [| Any type_ $(pure (maybe (ConE 'Nothing) (AppE (ConE 'Just) . AppE (VarE 'fromString) . LitE . textL) name)) :: Value Aeson.Value |]     Null ->       [| Null :: Value Aeson.Value |]     Bool b ->@@ -123,3 +129,43 @@    where     textL =       StringL . Text.unpack++data TypeSig = TypeSig+  { type_    :: Type+  , nullable :: Nullable+  } deriving (Show, Eq, Lift)++instance Aeson.ToJSON TypeSig where+  toJSON TypeSig {..} =+    Aeson.object+      [ "type" .= type_+      , "nullable" .= nullable+      ]++data Type+  = BoolT+  | NumberT+  | StringT+  | ArrayT+  | ObjectT+    deriving (Show, Eq, Lift)++instance Aeson.ToJSON Type where+  toJSON =+    Aeson.toJSON . \case+      BoolT {} -> "bool" :: Text+      NumberT {} -> "number"+      StringT {} -> "string"+      ArrayT {} -> "array"+      ObjectT {} -> "object"++data Nullable+  = Nullable+  | NonNullable+    deriving (Show, Eq, Lift)++instance Aeson.ToJSON Nullable where+  toJSON =+    Aeson.toJSON . \case+      Nullable -> True+      NonNullable -> False
test/Aeson/Match/QQSpec.hs view
@@ -16,9 +16,11 @@ spec = do   describe "parse" $     it "specs" $ do-      [qq| _ |] `shouldBe` Any Nothing-      [qq| _hole |] `shouldBe` Any (pure "hole")-      [qq| _"fancy hole" |] `shouldBe` Any (pure "fancy hole")+      [qq| _ |] `shouldBe` Any Nothing Nothing+      [qq| _hole |] `shouldBe` Any Nothing (pure "hole")+      [qq| _"fancy hole" |] `shouldBe` Any Nothing (pure "fancy hole")+      [qq| _typed-hole : number |] `shouldBe` Any (pure (TypeSig NumberT NonNullable)) (pure "typed-hole")+      [qq| _typed-nullable-hole : number? |] `shouldBe` Any (pure (TypeSig NumberT Nullable)) (pure "typed-nullable-hole")        [qq| null |] `shouldBe` Null @@ -35,9 +37,9 @@       [qq| [1, 2, 3] |] `shouldBe`         Array Box {knownValues = [Number 1, Number 2, Number 3], extendable = False}       [qq| [1, _, 3] |] `shouldBe`-        Array Box {knownValues = [Number 1, Any Nothing, Number 3], extendable = False}+        Array Box {knownValues = [Number 1, Any Nothing Nothing, Number 3], extendable = False}       [qq| [1, _, 3, ...] |] `shouldBe`-        Array Box {knownValues = [Number 1, Any Nothing, Number 3], extendable = True}+        Array Box {knownValues = [Number 1, Any Nothing Nothing, Number 3], extendable = True}        [qq| {} |] `shouldBe`         Object Box {knownValues = [], extendable = False}@@ -62,10 +64,13 @@       [qq| 4 |] `shouldMatch` [aesonQQ| 4 |]       [qq| "foo" |] `shouldMatch` [aesonQQ| "foo" |]       [qq| [1, 2, 3] |] `shouldMatch` [aesonQQ| [1, 2, 3] |]-      [qq| [1, 2, 3, ...] |] `shouldMatch` [aesonQQ| [1, 2, 3, 4] |]+      [qq| [1, _ : number, 3, ...] |] `shouldMatch` [aesonQQ| [1, 2, 3, 4] |]+      [qq| [1, _ : string] |] `shouldMatch` [aesonQQ| [1, "foo"] |]       [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 |]+      [qq| {foo: _ : number, bar: 7} |] `shouldMatch` [aesonQQ| {foo: 4, bar: 7} |]+      [qq| {foo: _ : number?, bar: 7} |] `shouldMatch` [aesonQQ| {foo: null, bar: 7} |]        [qq| null |] `shouldNotMatch` [aesonQQ| 4 |]       [qq| true |] `shouldNotMatch` [aesonQQ| false |]@@ -74,11 +79,14 @@       [qq| "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| {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} |]      it "paths" $ do       match [qq| {foo: {bar: {baz: [1, 4]}}} |] [aesonQQ| {foo: {bar: {baz: [1, 7]}}} |] `shouldBe`@@ -87,6 +95,13 @@     it "named holes" $ do       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/7+    it "#7" $ do+      match [qq| {foo: _} |] [aesonQQ| {} |] `shouldBe`+        missingPathElem [] "foo"+      match [qq| [_] |] [aesonQQ| [] |] `shouldBe`+        missingPathElem [] (Idx 0)  newtype ToEncoding a = ToEncoding { unToEncoding :: a }     deriving (Show, Eq, Num)