packages feed

aeson-match-qq 1.4.1 → 1.4.2

raw patch · 6 files changed

+143/−7 lines, 6 filesdep +containersPVP ok

version bump matches the API change (PVP)

Dependencies added: containers

API changes (from Hackage documentation)

+ Aeson.Match.QQ: ArrayUO :: Array ext -> Value ext
+ Aeson.Match.QQ: ArrayUOT :: Type
+ Aeson.Match.QQ.Internal.Match: I :: (Int, HashMap Text Value) -> I
+ Aeson.Match.QQ.Internal.Match: [$sel:unI:I] :: I -> (Int, HashMap Text Value)
+ Aeson.Match.QQ.Internal.Match: imapMaybe :: (Int -> a -> Maybe b) -> [a] -> [b]
+ Aeson.Match.QQ.Internal.Match: instance GHC.Classes.Eq Aeson.Match.QQ.Internal.Match.I
+ Aeson.Match.QQ.Internal.Match: instance GHC.Classes.Ord Aeson.Match.QQ.Internal.Match.I
+ Aeson.Match.QQ.Internal.Match: matchArrayUO :: Validation (NonEmpty VE) (HashMap Text Value) -> Path -> Box (Vector (Value Value)) -> Vector Value -> Validation (NonEmpty VE) (HashMap Text Value)
+ Aeson.Match.QQ.Internal.Match: newtype I
+ Aeson.Match.QQ.Internal.Value: ArrayUO :: Array ext -> Value ext
+ Aeson.Match.QQ.Internal.Value: ArrayUOT :: Type

Files

CHANGELOG.markdown view
@@ -1,3 +1,8 @@+1.4.2+=====++  * Unordered arrays (https://github.com/supki/aeson-match-qq/pull/15)+ 1.4.1 ===== 
aeson-match-qq.cabal view
@@ -7,7 +7,7 @@ -- hash: 5d1f836e56344a26982a1f67c2ccb2010e6ac6be94d88f239c4b15c9fe108df6  name:           aeson-match-qq-version:        1.4.1+version:        1.4.2 synopsis:       Declarative JSON matchers. description:    See README.markdown category:       Web@@ -42,6 +42,7 @@     , attoparsec     , base >=4.14 && <5     , bytestring+    , containers     , either     , haskell-src-meta     , scientific
src/Aeson/Match/QQ/Internal/Match.hs view
@@ -12,11 +12,14 @@ import           Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.KeyMap as Aeson (toHashMapText)-import           Data.Either.Validation (Validation, eitherToValidation)-import           Data.Foldable (for_)+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)+import           Data.Maybe (mapMaybe)+import qualified Data.Set as Set import           Data.String (IsString(..)) import           Data.Text (Text) import           Data.Vector (Vector)@@ -65,10 +68,11 @@         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)@@ -79,6 +83,11 @@       (Array _, _) -> do         mismatched         pure mempty+      (ArrayUO box, Aeson.Array arr) ->+        matchArrayUO mismatched path box arr+      (ArrayUO _, _) -> do+        mismatched+        pure mempty       (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)@@ -109,6 +118,64 @@     (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 {..})@@ -223,3 +290,7 @@ 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
@@ -49,6 +49,8 @@       string     OpenSquareBracketP ->       array+    OpenParenP ->+      arrayUO     OpenCurlyBracketP ->       object     HashP ->@@ -128,6 +130,17 @@       _ ->         error "impossible" +arrayUO :: Atto.Parser (Value Exp)+arrayUO = do+  _ <- Atto.word8 OpenParenP+  spaces+  _ <- Atto.string "unordered"+  spaces+  _ <- Atto.word8 CloseParenP+  spaces+  Array box <- array+  pure (ArrayUO box)+ object :: Atto.Parser (Value Exp) object = do   _ <- Atto.word8 OpenCurlyBracketP@@ -226,9 +239,14 @@ pattern OpenSquareBracketP = 91 -- '[' pattern CloseSquareBracketP = 93 -- ']' +pattern OpenParenP, CloseParenP :: Word8+pattern OpenParenP = 40 -- '('+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
@@ -39,6 +39,7 @@   | Number Scientific   | String Text   | Array (Array ext)+  | ArrayUO (Array ext)   | Object (Object ext)   | Ext ext     deriving (Show, Eq)@@ -70,6 +71,10 @@         [ "type" .= ("array" :: Text)         , "value" .= v         ]+      ArrayUO v ->+        [ "type" .= ("array-unordered" :: Text)+        , "value" .= v+        ]       Object v ->         [ "type" .= ("object" :: Text)         , "value" .= v@@ -117,6 +122,13 @@           , 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 =@@ -149,6 +161,7 @@   | NumberT   | StringT   | ArrayT+  | ArrayUOT   | ObjectT     deriving (Show, Eq, Lift) @@ -159,6 +172,7 @@       NumberT {} -> "number"       StringT {} -> "string"       ArrayT {} -> "array"+      ArrayUOT {} -> "array-unordered"       ObjectT {} -> "object"  data Nullable
test/Aeson/Match/QQSpec.hs view
@@ -41,6 +41,15 @@       [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`@@ -66,6 +75,13 @@       [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| (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 |]@@ -81,6 +97,9 @@       [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} |]@@ -100,6 +119,13 @@       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`@@ -122,6 +148,7 @@             , extendable = False             }) +    -- https://github.com/supki/aeson-match-qq/issues/13     it "#13" $       [qq| "Слава Україні" |] `shouldMatch` [aesonQQ| "Слава Україні" |]