jsonpath 0.2.1.0 → 0.3.0.0
raw patch · 15 files changed
+1194/−553 lines, 15 filesdep +hspec-megaparsecdep +megaparsecdep +scientificdep −attoparsecdep −hspec-attoparsecdep ~aesonsetup-changedPVP ok
version bump matches the API change (PVP)
Dependencies added: hspec-megaparsec, megaparsec, scientific
Dependencies removed: attoparsec, hspec-attoparsec
Dependency ranges changed: aeson
API changes (from Hackage documentation)
- Data.JSONPath.Execute: executeJSONPathEither :: [JSONPathElement] -> Value -> Either String [Value]
- Data.JSONPath.ExecutionResult: ResultError :: String -> ExecutionResult a
- Data.JSONPath.ExecutionResult: ResultList :: [a] -> ExecutionResult a
- Data.JSONPath.ExecutionResult: ResultValue :: a -> ExecutionResult a
- Data.JSONPath.ExecutionResult: appendResults :: ExecutionResult a -> ExecutionResult a -> ExecutionResult a
- Data.JSONPath.ExecutionResult: concatResults :: [ExecutionResult a] -> ExecutionResult a
- Data.JSONPath.ExecutionResult: data ExecutionResult a
- Data.JSONPath.ExecutionResult: excludeErrors :: [ExecutionResult a] -> [a]
- Data.JSONPath.ExecutionResult: instance GHC.Base.Applicative Data.JSONPath.ExecutionResult.ExecutionResult
- Data.JSONPath.ExecutionResult: instance GHC.Base.Functor Data.JSONPath.ExecutionResult.ExecutionResult
- Data.JSONPath.ExecutionResult: instance GHC.Base.Monad Data.JSONPath.ExecutionResult.ExecutionResult
- Data.JSONPath.ExecutionResult: maybeToResult :: String -> Maybe a -> ExecutionResult a
- Data.JSONPath.ExecutionResult: resultToEither :: ExecutionResult a -> Either String [a]
- Data.JSONPath.Types: KeyChildren :: [Text] -> JSONPathElement
- Data.JSONPath.Types: LitNumber :: Int -> Literal
- Data.JSONPath.Types: LitString :: Text -> Literal
- Data.JSONPath.Types: SimpleSlice :: Int -> Int -> SliceElement
- Data.JSONPath.Types: SingleIndex :: Int -> SliceElement
- Data.JSONPath.Types: SliceFrom :: Int -> SliceElement
- Data.JSONPath.Types: SliceFromWithStep :: Int -> Int -> SliceElement
- Data.JSONPath.Types: SliceTo :: Int -> SliceElement
- Data.JSONPath.Types: SliceToWithStep :: Int -> Int -> SliceElement
- Data.JSONPath.Types: SliceUnion :: SliceElement -> SliceElement -> JSONPathElement
- Data.JSONPath.Types: SliceWithOnlyStep :: Int -> SliceElement
- Data.JSONPath.Types: SliceWithStep :: Int -> Int -> Int -> SliceElement
- Data.JSONPath.Types: data Literal
- Data.JSONPath.Types: data SliceElement
- Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.Literal
- Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.SliceElement
- Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.Literal
- Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.SliceElement
+ Data.JSONPath.Types: And :: FilterExpr -> FilterExpr -> FilterExpr
+ Data.JSONPath.Types: CmpBool :: Bool -> Comparable
+ Data.JSONPath.Types: CmpNull :: Comparable
+ Data.JSONPath.Types: CmpNumber :: Scientific -> Comparable
+ Data.JSONPath.Types: CmpPath :: SingularPath -> Comparable
+ Data.JSONPath.Types: CmpString :: Text -> Comparable
+ Data.JSONPath.Types: ComparisonExpr :: Comparable -> Condition -> Comparable -> FilterExpr
+ Data.JSONPath.Types: ExistsExpr :: SingularPath -> FilterExpr
+ Data.JSONPath.Types: GreaterThanOrEqual :: Condition
+ Data.JSONPath.Types: Index :: Int -> SingularPathElement
+ Data.JSONPath.Types: IndexChild :: Int -> JSONPathElement
+ Data.JSONPath.Types: Key :: Text -> SingularPathElement
+ Data.JSONPath.Types: Not :: FilterExpr -> FilterExpr
+ Data.JSONPath.Types: Or :: FilterExpr -> FilterExpr -> FilterExpr
+ Data.JSONPath.Types: SingularPath :: BeginningPoint -> [SingularPathElement] -> SingularPath
+ Data.JSONPath.Types: SmallerThanOrEqual :: Condition
+ Data.JSONPath.Types: UEIndexChild :: Int -> UnionElement
+ Data.JSONPath.Types: UEKeyChild :: Text -> UnionElement
+ Data.JSONPath.Types: UESlice :: Maybe Int -> Maybe Int -> Maybe Int -> UnionElement
+ Data.JSONPath.Types: Union :: [UnionElement] -> JSONPathElement
+ Data.JSONPath.Types: data Comparable
+ Data.JSONPath.Types: data FilterExpr
+ Data.JSONPath.Types: data SingularPath
+ Data.JSONPath.Types: data SingularPathElement
+ Data.JSONPath.Types: data UnionElement
+ Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.Comparable
+ Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.FilterExpr
+ Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.SingularPath
+ Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.SingularPathElement
+ Data.JSONPath.Types: instance GHC.Classes.Eq Data.JSONPath.Types.UnionElement
+ Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.Comparable
+ Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.FilterExpr
+ Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.SingularPath
+ Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.SingularPathElement
+ Data.JSONPath.Types: instance GHC.Show.Show Data.JSONPath.Types.UnionElement
- Data.JSONPath.Execute: executeJSONPath :: [JSONPathElement] -> Value -> ExecutionResult Value
+ Data.JSONPath.Execute: executeJSONPath :: [JSONPathElement] -> Value -> [Value]
- Data.JSONPath.Execute: executeJSONPathElement :: JSONPathElement -> Value -> ExecutionResult Value
+ Data.JSONPath.Execute: executeJSONPathElement :: JSONPathElement -> Value -> Value -> [Value]
- Data.JSONPath.Parser: jsonPath :: Parser [JSONPathElement]
+ Data.JSONPath.Parser: jsonPath :: Parser a -> Parser [JSONPathElement]
- Data.JSONPath.Types: Filter :: BeginningPoint -> [JSONPathElement] -> Condition -> Literal -> JSONPathElement
+ Data.JSONPath.Types: Filter :: FilterExpr -> JSONPathElement
- Data.JSONPath.Types: Slice :: SliceElement -> JSONPathElement
+ Data.JSONPath.Types: Slice :: Maybe Int -> Maybe Int -> Maybe Int -> JSONPathElement
Files
- ChangeLog.md +31/−3
- Setup.hs +1/−0
- jsonpath.cabal +6/−9
- src/Data/JSONPath.hs +5/−6
- src/Data/JSONPath/Execute.hs +194/−94
- src/Data/JSONPath/ExecutionResult.hs +0/−59
- src/Data/JSONPath/Parser.hs +185/−125
- src/Data/JSONPath/Types.hs +65/−30
- test/Data/JSONPathSpec.hs +71/−50
- test/resources/json-path-tests/DotOperator.json +41/−48
- test/resources/json-path-tests/EmptyPath.json +50/−0
- test/resources/json-path-tests/FieldAccess.json +57/−0
- test/resources/json-path-tests/FilterExpressionSubscriptOperator.json +326/−32
- test/resources/json-path-tests/IndexedSubscriptOperator.json +124/−58
- test/resources/json-path-tests/SearchOperator.json +38/−39
ChangeLog.md view
@@ -1,5 +1,36 @@ # Changelog for jsonpath-hs +## v0.3.0.0 - Lots of breaking changes, they come with new features++This release aims to address many deviations from similar libraries in other+programming languages, this is thanks to Christoph Burgmer's+[json-path-comparison+project](https://cburgmer.github.io/json-path-comparison/).++There has also been significant work in codifying JSONPath by the IETF-WG for+JSONPath, the draft spec can be acccessed+[here](https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html).+This release also aims to adapt some of the ideas from from spec.++As a result there have been significant breaking changes in the types and also+small changes in the way JSONPaths are executed.++List of changes:+* Fix compiler warnings and bugs with non-total pattern matches.+* Allow double quoted literals and field accessors.+* Ensure termination when start or end of slice are too big/small.+* Implement slice execution based on IETF draft spec .+* Ensure that a valid JSONPath never fails to execute.+* Drop support for GHC <= 8.2.+* Use megaparsec instead of attoparsec for better error messages.+* Allow escape sequences in key names.+* Allow parsing empty paths.+* Allow spaces arround index selectors.+* Allow selecting keys in unions and allow many union elements.+* Implement 'and', 'or' and 'not' operator support in filters.+* Allow comparison between two singular paths.+* Allow bools and nulls in filters.+ ## v0.2.1.0 * Support and require aeson >= 2@@ -21,6 +52,3 @@ ## v0.1.0.0 * Start the project.---## Unreleased changes
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
jsonpath.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12 name: jsonpath-version: 0.2.1.0+version: 0.3.0.0 synopsis: Library to parse and execute JSONPath description: Please see the README on GitHub at <https://github.com/akshaymankar/jsonpath-hs#readme> category: Text, Web, JSON@@ -16,10 +16,7 @@ extra-source-files: README.md ChangeLog.md- test/resources/json-path-tests/DotOperator.json- test/resources/json-path-tests/FilterExpressionSubscriptOperator.json- test/resources/json-path-tests/IndexedSubscriptOperator.json- test/resources/json-path-tests/SearchOperator.json+ test/resources/json-path-tests/*.json source-repository head type: git@@ -29,7 +26,6 @@ exposed-modules: Data.JSONPath Data.JSONPath.Execute- Data.JSONPath.ExecutionResult Data.JSONPath.Parser Data.JSONPath.Types other-modules:@@ -38,8 +34,9 @@ src build-depends: aeson >=2- , attoparsec >=0.13+ , megaparsec , base >=4.9 && <5+ , scientific , text >=1.2 , unordered-containers >=0.2.8 , vector >=0.12@@ -59,12 +56,12 @@ build-depends: aeson >=1.1 , aeson-casing- , attoparsec >=0.13+ , megaparsec , base >=4.9 && <5 , bytestring , file-embed , hspec- , hspec-attoparsec+ , hspec-megaparsec , jsonpath , text >=1.2 , unordered-containers >=0.2.8
src/Data/JSONPath.hs view
@@ -1,11 +1,10 @@-{-# LANGUAGE OverloadedStrings #-} module Data.JSONPath- ( module Data.JSONPath.Types- , module Data.JSONPath.Parser- , module Data.JSONPath.Execute+ ( module Data.JSONPath.Types,+ module Data.JSONPath.Parser,+ module Data.JSONPath.Execute, ) where -import Data.JSONPath.Types-import Data.JSONPath.Parser import Data.JSONPath.Execute+import Data.JSONPath.Parser+import Data.JSONPath.Types
src/Data/JSONPath/Execute.hs view
@@ -1,112 +1,212 @@-{-# LANGUAGE CPP #-}-module Data.JSONPath.Execute- (executeJSONPath, executeJSONPathEither, executeJSONPathElement)-where+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-} +module Data.JSONPath.Execute (executeJSONPath, executeJSONPathElement) where+ import Data.Aeson-import Data.Aeson.Text-import Data.Function ((&))-import qualified Data.Aeson.KeyMap as Map import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as Map+import qualified Data.Foldable as Foldable import Data.JSONPath.Types-import Data.Text (unpack)+import Data.Maybe (fromMaybe, isJust, maybeToList)+import Data.Text (Text)+import qualified Data.Vector as V -#if !MIN_VERSION_base (4,11,0)-import Data.Semigroup ((<>))-#endif+executeJSONPath :: [JSONPathElement] -> Value -> [Value]+executeJSONPath path rootVal = go path rootVal+ where+ go :: [JSONPathElement] -> Value -> [Value]+ go [] v = [v]+ go (j : js) v =+ go js =<< executeJSONPathElement j rootVal v -import qualified Data.Text.Lazy as LazyText-import qualified Data.Vector as V+executeJSONPathElement :: JSONPathElement -> Value -> Value -> [Value]+executeJSONPathElement (KeyChild key) _ val =+ executeKeyChildOnValue key val+executeJSONPathElement AnyChild _ val =+ case val of+ Object o -> map snd $ Map.toList o+ Array a -> V.toList a+ _ -> []+executeJSONPathElement (IndexChild i) _ val =+ executeIndexChildOnValue i val+executeJSONPathElement (Slice start end step) _ val =+ executeSliceOnValue start end step val+executeJSONPathElement (Union elements) _ val =+ concatMap (flip executeUnionElement val) elements+executeJSONPathElement (Filter expr) rootVal val =+ case val of+ Array a -> executeFilter expr rootVal (V.toList a)+ Object o -> executeFilter expr rootVal (Map.elems o)+ _ -> []+executeJSONPathElement s@(Search js) origVal val =+ let x = executeJSONPath js val+ y = mconcat $ valMap (executeJSONPathElement s origVal) val+ in x <> y -executeJSONPath :: [JSONPathElement] -> Value -> ExecutionResult Value-executeJSONPath [] val = ResultError "empty json path"-executeJSONPath (j:[]) val = executeJSONPathElement j val-executeJSONPath (j:js) val = executeJSONPath js =<< executeJSONPathElement j val+valMap :: ToJSON b => (Value -> [b]) -> Value -> [[b]]+valMap f (Object o) = map snd . Map.toList $ Map.map f o+valMap f (Array a) = V.toList $ V.map f a+valMap _ _ = [] -executeJSONPathEither :: [JSONPathElement] -> Value -> Either String [Value]-executeJSONPathEither js val = resultToEither $ executeJSONPath js val+executeConditionOnMaybes :: Maybe Value -> Condition -> Maybe Value -> Bool+executeConditionOnMaybes (Just val1) c (Just val2) = executeCondition val1 c val2+executeConditionOnMaybes Nothing Equal Nothing = True+executeConditionOnMaybes Nothing GreaterThanOrEqual Nothing = True+executeConditionOnMaybes Nothing SmallerThanOrEqual Nothing = True+executeConditionOnMaybes Nothing NotEqual (Just _) = True+executeConditionOnMaybes (Just _) NotEqual Nothing = True+executeConditionOnMaybes _ _ _ = False -executeJSONPathElement :: JSONPathElement -> Value -> ExecutionResult Value-executeJSONPathElement (KeyChild key) val =- case val of- Object o -> Map.lookup (Key.fromText key) o- & maybeToResult (notFoundErr key o)- _ -> ResultError $ expectedObjectErr val-executeJSONPathElement AnyChild val =- case val of- Object o -> ResultList . map snd $ Map.toList o- Array a -> ResultList $ V.toList a- _ -> ResultError $ expectedObjectErr val-executeJSONPathElement (Slice slice) val =- case val of- Array a -> executeSliceElement slice a- _ -> ResultError $ expectedArrayErr val-executeJSONPathElement (SliceUnion first second) val =- case val of- Array a -> appendResults (executeSliceElement first a) (executeSliceElement second a)- _ -> ResultError $ expectedArrayErr val-executeJSONPathElement (Filter _ jsonPath cond lit) val =+{- ORMOLU_DISABLE -}+isEqualTo :: Value -> Value -> Bool+(Object _) `isEqualTo` _ = False+_ `isEqualTo` (Object _) = False+(Array _) `isEqualTo` _ = False+_ `isEqualTo` (Array _) = False+val1 `isEqualTo` val2 = val1 == val2++isSmallerThan :: Value -> Value -> Bool+(Number n1) `isSmallerThan` (Number n2) = n1 < n2+(String s1) `isSmallerThan` (String s2) = s1 < s2+_ `isSmallerThan` _ = False+{- ORMOLU_ENABLE -}++executeCondition :: Value -> Condition -> Value -> Bool+executeCondition val1 NotEqual val2 = not (executeCondition val1 Equal val2)+executeCondition val1 Equal val2 = val1 `isEqualTo` val2+executeCondition val1 SmallerThan val2 = val1 `isSmallerThan` val2+executeCondition val1 GreaterThan val2 =+ canCompare val1 val2+ && not (executeCondition val1 SmallerThan val2)+ && not (executeCondition val1 Equal val2)+executeCondition val GreaterThanOrEqual lit =+ canCompare val lit+ && not (executeCondition val SmallerThan lit)+executeCondition val1 SmallerThanOrEqual val2 =+ canCompare val1 val2+ && not (executeCondition val1 GreaterThan val2)++canCompare :: Value -> Value -> Bool+canCompare (Number _) (Number _) = True+canCompare (String _) (String _) = True+canCompare _ _ = False++executeSliceOnValue :: Maybe Int -> Maybe Int -> Maybe Int -> Value -> [Value]+executeSliceOnValue start end step val = case val of- Array a -> do- let l = V.toList a- ResultList $ Prelude.map (executeJSONPath jsonPath) l- & zip l- & excludeSndErrors- & Prelude.foldr (\(x,ys) acc -> if length ys == 1 then (x, head ys):acc else acc) []- & Prelude.filter (\(origVal, exprVal) -> executeCondition exprVal cond lit)- & Prelude.map fst- _ -> ResultError $ expectedArrayErr val-executeJSONPathElement s@(Search js) val =- let x = either (const []) id $ executeJSONPathEither js val- y = excludeErrors $ valMap (executeJSONPathElement s) val- in if Prelude.null x && Prelude.null y- then ResultError "Search failed"- else ResultList $ x ++ y+ Array a -> executeSlice start end step a+ _ -> [] -valMap :: ToJSON b => (Value -> ExecutionResult b) -> Value -> [ExecutionResult b]-valMap f v@(Object o) = map snd . Map.toList $ Map.map f o-valMap f (Array a) = V.toList $ V.map f a-valMap _ v = pure $ ResultError $ "Expected object or array, found " <> (encodeJSONToString v)+-- | Implementation is based on+-- https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-array-slice-selector+executeSlice :: forall a. Maybe Int -> Maybe Int -> Maybe Int -> V.Vector a -> [a]+executeSlice mStart mEnd mStep v+ | step == 0 = []+ | step > 0 = postitiveStepLoop lowerBound+ | otherwise = negativeStepLoop upperBound+ where+ postitiveStepLoop :: Int -> [a]+ postitiveStepLoop i+ | i < upperBound = v V.! i : postitiveStepLoop (i + step)+ | otherwise = [] -executeCondition :: Value -> Condition -> Literal -> Bool-executeCondition (Number n1) Equal (LitNumber n2) = n1 == (fromInteger $ toInteger n2)-executeCondition (String s1) Equal (LitString s2) = s1 == s2+ negativeStepLoop :: Int -> [a]+ negativeStepLoop i+ | i > lowerBound = v V.! i : negativeStepLoop (i + step)+ | otherwise = [] -executeSliceElement :: SliceElement -> V.Vector Value -> ExecutionResult Value-executeSliceElement (SingleIndex i) v = if i < 0- then maybeToResult (invalidIndexErr i v) $ (V.!?) v (V.length v + i)- else maybeToResult (invalidIndexErr i v) $ (V.!?) v i-executeSliceElement (SimpleSlice start end) v = sliceEither v start end 1-executeSliceElement (SliceWithStep start end step) v = sliceEither v start end step-executeSliceElement (SliceTo end) v = sliceEither v 0 end 1-executeSliceElement (SliceToWithStep end step) v = sliceEither v 0 end step-executeSliceElement (SliceFrom start) v = sliceEither v start (-1) 1-executeSliceElement (SliceFromWithStep start step) v = sliceEither v start (-1) step-executeSliceElement (SliceWithOnlyStep step) v = sliceEither v 0 (-1) step+ len = V.length v+ step = fromMaybe 1 mStep -sliceEither :: ToJSON a- => V.Vector a -> Int -> Int -> Int -> ExecutionResult a-sliceEither v start end step = let len = V.length v- realStart = if start < 0 then len + start else start- realEnd = if end < 0 then len + end + 1 else end- in if realStart < realEnd- then appendResults (indexEither v realStart) (sliceEither v (realStart + step) realEnd step)- else ResultList []+ normalizeIndex :: Int -> Int+ normalizeIndex i =+ if i >= 0 then i else len + i -indexEither :: ToJSON a => V.Vector a -> Int -> ExecutionResult a-indexEither v i = (V.!?) v i- & maybeToResult (invalidIndexErr i v)+ defaultStart+ | step >= 0 = 0+ | otherwise = len - 1+ start = fromMaybe defaultStart mStart+ normalizedStart = normalizeIndex start -excludeSndErrors :: [(c, ExecutionResult a)] -> [(c, [a])]-excludeSndErrors xs = Prelude.foldr accumulateFn ([] :: [(c, b)]) xs where- accumulateFn (x, ResultList ys) acc = (x, ys):acc- accumulateFn (x, ResultValue y) acc = (x, [y]):acc- accumulateFn (x, _) acc = acc+ defaultEnd+ | step >= 0 = len+ | otherwise = negate len - 1+ end = fromMaybe defaultEnd mEnd+ normalizedEnd = normalizeIndex end -encodeJSONToString :: ToJSON a => a -> String-encodeJSONToString x = LazyText.unpack $ encodeToLazyText x+ lowerBound+ | step >= 0 = min (max normalizedStart 0) len+ | otherwise = min (max normalizedEnd (-1)) (len - 1)+ upperBound+ | step >= 0 = min (max normalizedEnd 0) len+ | otherwise = min (max normalizedStart (-1)) (len - 1) -notFoundErr key o = "expected key " <> unpack key <> " in object " <> (encodeJSONToString o)-invalidIndexErr i a = "index " <> show i <> " invalid for array " <> (encodeJSONToString a)-expectedObjectErr val = "expected object, found " <> (encodeJSONToString val)-expectedArrayErr val = "expected array, found " <> (encodeJSONToString val)+executeIndexChild :: Int -> V.Vector a -> Maybe a+executeIndexChild i v =+ if i < 0+ then (V.!?) v (V.length v + i)+ else (V.!?) v i++executeUnionElement :: UnionElement -> Value -> [Value]+executeUnionElement (UEIndexChild i) v = executeIndexChildOnValue i v+executeUnionElement (UESlice start end step) v = executeSliceOnValue start end step v+executeUnionElement (UEKeyChild child) v = executeKeyChildOnValue child v++executeKeyChildOnValue :: Text -> Value -> [Value]+executeKeyChildOnValue key val =+ maybeToList $ executeSingularPathElement (Key key) val++executeIndexChildOnValue :: Int -> Value -> [Value]+executeIndexChildOnValue i val =+ maybeToList $ executeSingularPathElement (Index i) val++executeSingularPathElement :: SingularPathElement -> Value -> Maybe Value+executeSingularPathElement (Key key) val =+ case val of+ Object o -> Map.lookup (Key.fromText key) o+ _ -> Nothing+executeSingularPathElement (Index i) val =+ case val of+ Array a -> executeIndexChild i a+ _ -> Nothing++executeSingularPath :: SingularPath -> Value -> Value -> Maybe Value+executeSingularPath (SingularPath beginnigPoint ps) rootVal currentVal =+ let val = case beginnigPoint of+ Root -> rootVal+ CurrentObject -> currentVal+ in Foldable.foldl'+ ( \case+ Nothing -> const Nothing+ Just v -> flip executeSingularPathElement v+ )+ (Just val)+ ps++executeFilter :: FilterExpr -> Value -> [Value] -> [Value]+executeFilter expr rootVal = Prelude.filter (filterExprPred expr rootVal)++comparableToValue :: Comparable -> Value -> Value -> Maybe Value+comparableToValue (CmpNumber n) _ _ = Just $ Number n+comparableToValue (CmpString s) _ _ = Just $ String s+comparableToValue (CmpBool b) _ _ = Just $ Bool b+comparableToValue CmpNull _ _ = Just Null+comparableToValue (CmpPath p) rootVal val =+ executeSingularPath p rootVal val++filterExprPred :: FilterExpr -> Value -> Value -> Bool+filterExprPred expr rootVal val =+ case expr of+ ComparisonExpr cmp1 cond cmp2 ->+ let val1 = comparableToValue cmp1 rootVal val+ val2 = comparableToValue cmp2 rootVal val+ in executeConditionOnMaybes val1 cond val2+ ExistsExpr path ->+ isJust $ executeSingularPath path rootVal val+ Or e1 e2 ->+ filterExprPred e1 rootVal val || filterExprPred e2 rootVal val+ And e1 e2 ->+ filterExprPred e1 rootVal val && filterExprPred e2 rootVal val+ Not e ->+ not $ filterExprPred e rootVal val
− src/Data/JSONPath/ExecutionResult.hs
@@ -1,59 +0,0 @@-module Data.JSONPath.ExecutionResult where--data ExecutionResult a = ResultList [a]- | ResultValue a- | ResultError String--instance Functor ExecutionResult where- fmap f (ResultList xs) = ResultList $ Prelude.map f xs- fmap f (ResultValue x) = ResultValue $ f x- fmap f (ResultError err) = ResultError err--instance Applicative ExecutionResult where- pure = ResultValue- (<*>) (ResultList fs) (ResultList xs) = ResultList $ fs <*> xs- (<*>) (ResultList fs) (ResultValue x) = ResultList $ Prelude.map (\f -> f x) fs- (<*>) (ResultValue f) (ResultList xs) = ResultList $ Prelude.map f xs- (<*>) (ResultValue f) (ResultValue x) = ResultValue $ f x- (<*>) (ResultError e) _ = ResultError e- (<*>) _ (ResultError e) = ResultError e--instance Monad ExecutionResult where- (>>=) (ResultValue x) f = f x- (>>=) (ResultList xs) f = concatResults $ Prelude.map f xs- (>>=) (ResultError e) f = ResultError e--concatResults :: [ExecutionResult a] -> ExecutionResult a-concatResults [] = ResultList []-concatResults (ResultList xs:rs) = case concatResults rs of- ResultList ys -> ResultList (xs ++ ys)- ResultValue y -> ResultList (y:xs)- e -> e-concatResults (ResultValue x:[]) = ResultValue x-concatResults (ResultValue x:rs) = case concatResults rs of- ResultList ys -> ResultList (x:ys)- ResultValue y -> ResultList [x,y]- e -> e-concatResults (e:_) = e--appendResults :: ExecutionResult a -> ExecutionResult a -> ExecutionResult a-appendResults (ResultValue x) (ResultValue y) = ResultList [x,y]-appendResults (ResultValue x) (ResultList ys) = ResultList $ x:ys-appendResults (ResultList xs) (ResultValue y) = ResultList $ y:xs-appendResults (ResultList xs) (ResultList ys) = ResultList $ xs ++ ys-appendResults _ e = e--maybeToResult :: String -> Maybe a ->ExecutionResult a-maybeToResult _ (Just x) = ResultValue x-maybeToResult err _ = ResultError err--resultToEither :: ExecutionResult a -> Either String [a]-resultToEither (ResultList xs) = return xs-resultToEither (ResultValue x) = return [x]-resultToEither (ResultError e) = Left e--excludeErrors :: [ExecutionResult a] -> [a]-excludeErrors [] = []-excludeErrors (ResultError _:rs) = excludeErrors rs-excludeErrors (ResultList xs:rs) = xs ++ excludeErrors rs-excludeErrors (ResultValue x:rs) = x:(excludeErrors rs)
src/Data/JSONPath/Parser.hs view
@@ -1,167 +1,227 @@+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}-module Data.JSONPath.Parser- (jsonPathElement, jsonPath)-where+{-# LANGUAGE TypeFamilies #-} -import Control.Applicative ((<|>))-import Data.Attoparsec.Text as A+module Data.JSONPath.Parser (jsonPathElement, jsonPath) where++import qualified Data.Char as Char import Data.Functor+import Data.Functor.Identity import Data.JSONPath.Types+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Void (Void)+import Text.Megaparsec as P+import Text.Megaparsec.Char (char, space, string)+import qualified Text.Megaparsec.Char.Lexer as L -jsonPath :: Parser [JSONPathElement]-jsonPath = do- _ <- skip (== '$') <|> pure ()- many1 jsonPathElement+type Parser = P.ParsecT Void Text Identity +jsonPath :: Parser a -> Parser [JSONPathElement]+jsonPath endParser = do+ _ <- optional $ char '$'+ manyTill jsonPathElement (hidden $ lookAhead endParser)+ jsonPathElement :: Parser JSONPathElement-jsonPathElement = do- (keyChildDot <?> "keyChildDot")- <|> (keyChildBracket <?> "keyChildBracket")- <|> (keyChildren <?> "keyChildren")- <|> (anyChild <?> "anyChild")- <|> (slice <?> "slice")- <|> (sliceUnion <?> "sliceUnion")- <|> (filterParser <?> "filterParser")- <|> (search <?> "search")- <|> (searchBeginningWithSlice <?> "searchBeginningWithSlice")+jsonPathElement =+ ignoreSurroundingSpace $+ try anyChild+ <|> try keyChild+ <|> try slice+ <|> try indexChild+ <|> try union+ <|> try filterParser+ <|> try search+ <|> searchBeginningWithSlice +indexChild :: Parser JSONPathElement+indexChild = IndexChild <$> inSqBr indexChildWithoutBrackets++indexChildWithoutBrackets :: Parser Int+indexChildWithoutBrackets = ignoreSurroundingSpace $ L.signed space L.decimal+ slice :: Parser JSONPathElement-slice = Slice <$> ignoreSurroundingSqBr sliceWithoutBrackets+slice =+ uncurry3 Slice+ <$> inSqBr sliceWithoutBrackets -sliceWithoutBrackets = (sliceWithStep <?> "sliceWithStep")- <|> (simpleSlice <?> "simpleSlice")- <|> (sliceFromWithStep <?> "sliceFromWithStep")- <|> (sliceFrom <?> "sliceFrom")- <|> (singleIndex <?> "singleIndex")- <|> (sliceToWithStep <?> "sliceToWithStep")- <|> (sliceTo <?> "sliceTo")- <|> (sliceWithOnlyStep <?> "sliceWithOnlyStep")+sliceWithoutBrackets :: Parser (Maybe Int, Maybe Int, Maybe Int)+sliceWithoutBrackets = do+ (,,)+ <$> parseStart+ <*> parseEnd+ <*> parseStep+ where+ parseStart :: Parser (Maybe Int)+ parseStart =+ ignoreSurroundingSpace (optional (L.signed space L.decimal))+ <* char ':' -singleIndex :: Parser SliceElement-singleIndex = SingleIndex <$> signed decimal+ parseEnd =+ ignoreSurroundingSpace $ optional $ L.signed space L.decimal -keyChildBracket :: Parser JSONPathElement-keyChildBracket = KeyChild- <$> (string "['" *> takeWhile1 (inClass "a-zA-Z0-9_-") <* string "']")+ parseStep =+ optional (char ':')+ *> ignoreSurroundingSpace (optional (L.signed space L.decimal)) -keyChildDot :: Parser JSONPathElement-keyChildDot = KeyChild- <$> (char '.' *> takeWhile1 (inClass "a-zA-Z0-9_-"))+keyChild :: Parser JSONPathElement+keyChild = KeyChild <$> (try sqBrKeyChild <|> dotKeyChild) -keyChildren :: Parser JSONPathElement-keyChildren = do- _ <- string "['"- firstKey <- takeWhile1 (inClass "a-zA-Z0-9_-")- restKeys <- many' $ char '.' *> takeWhile1 (inClass "a-zA-Z0-9_-")- _ <- string "']"- return $ KeyChildren (firstKey:restKeys)+sqBrKeyChild :: Parser Text+sqBrKeyChild =+ inSqBr $ ignoreSurroundingSpace quotedString +dotKeyChild :: Parser Text+dotKeyChild = char '.' *> takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-' || c == '_')+ anyChild :: Parser JSONPathElement-anyChild = AnyChild <$ (string ".*" <|> string "[*]")+anyChild = ignoreSurroundingSpace $ AnyChild <$ (void (string ".*") <|> void (inSqBr (char '*'))) --- peekAssertClosingSqBr :: Parser ()--- peekAssertClosingSqBr +union :: Parser JSONPathElement+union =+ inSqBr $+ Union <$> do+ firstElement <- unionElement+ restElements <- some (char ',' *> unionElement)+ pure (firstElement : restElements) -simpleSlice :: Parser SliceElement-simpleSlice = do- start <- signed decimal- _ <- char ':'- end <- signed decimal- return $ SimpleSlice start end+unionElement :: Parser UnionElement+unionElement =+ try (uncurry3 UESlice <$> sliceWithoutBrackets)+ <|> try (UEIndexChild <$> indexChildWithoutBrackets)+ <|> UEKeyChild <$> ignoreSurroundingSpace quotedString -sliceWithStep :: Parser SliceElement-sliceWithStep = do- start <- signed decimal- _ <- char ':'- end <- signed decimal- _ <- char ':'- step <- signed decimal- return $ SliceWithStep start end step+filterParser :: Parser JSONPathElement+filterParser = inSqBr $ do+ _ <- ignoreSurroundingSpace $ char '?'+ Filter <$> filterExpr (ignoreSurroundingSpace (char ']')) -sliceFrom :: Parser SliceElement-sliceFrom = do- start <- signed decimal- _ <- char ':'- return $ SliceFrom start+filterExpr :: Parser a -> Parser FilterExpr+filterExpr endParser =+ try (orFilterExpr endParser)+ <|> try (andFilterExpr endParser)+ <|> basicFilterExpr endParser -sliceFromWithStep :: Parser SliceElement-sliceFromWithStep = do- start <- signed decimal- _ <- string "::"- step <- signed decimal- return $ SliceFromWithStep start step+basicFilterExpr :: Parser a -> Parser FilterExpr+basicFilterExpr endParser = do+ maybeNot <- optional (char '!')+ expr <-+ try (comparisionFilterExpr endParser)+ <|> try (existsFilterExpr endParser)+ <|> (inParens (filterExpr closingParen) <* lookAhead endParser)+ case maybeNot of+ Nothing -> pure expr+ Just _ -> pure $ Not expr -sliceTo :: Parser SliceElement-sliceTo = do- _ <- char ':'- end <- signed decimal- return $ SliceTo end+comparisionFilterExpr :: Parser a -> Parser FilterExpr+comparisionFilterExpr endParser = do+ expr <-+ ComparisonExpr+ <$> comparable condition+ <*> condition+ <*> comparable endParser+ _ <- lookAhead endParser+ pure expr -sliceToWithStep :: Parser SliceElement-sliceToWithStep = do- _ <- char ':'- end <- signed decimal- _ <- char ':'- step <- signed decimal- return $ SliceToWithStep end step+existsFilterExpr :: Parser a -> Parser FilterExpr+existsFilterExpr endParser =+ ExistsExpr <$> singularPath endParser -sliceWithOnlyStep :: Parser SliceElement-sliceWithOnlyStep = do- _ <- string "::"- step <- signed decimal- return $ SliceWithOnlyStep step+singularPath :: Parser a -> Parser SingularPath+singularPath endParser =+ SingularPath+ <$> beginningPoint+ <*> manyTill singularPathElement (lookAhead endParser) -sliceUnion :: Parser JSONPathElement-sliceUnion = ignoreSurroundingSqBr $ do- firstElement <- sliceWithoutBrackets <?> "firstElement"- _ <- char ','- secondElement <- sliceWithoutBrackets <?> "secondElement"- return $ SliceUnion firstElement secondElement+singularPathElement :: Parser SingularPathElement+singularPathElement =+ (Key <$> try dotKeyChild)+ <|> (Key <$> try sqBrKeyChild)+ <|> Index <$> inSqBr indexChildWithoutBrackets -filterParser :: Parser JSONPathElement-filterParser = do- _ <- string "[?(" <?> "[?("- b <- beginningPoint <?> "beginning point"- js <- jsonPath <?> "jsonPathElements"- c <- condition <?> "condition"- l <- literal <?> "literal"- _ <- string ")]" <?> ")]"- return $ Filter b js c l+orFilterExpr :: Parser a -> Parser FilterExpr+orFilterExpr endParser = do+ let orOperator = ignoreSurroundingSpace $ string "||"+ e1 <-+ -- If there is an '&&' operation, it should take precedence over the '||'+ try (andFilterExpr orOperator)+ <|> basicFilterExpr orOperator+ _ <- orOperator+ Or e1 <$> filterExpr endParser +andFilterExpr :: Parser a -> Parser FilterExpr+andFilterExpr endParser = do+ let andOperator = ignoreSurroundingSpace $ string "&&"+ e1 <- basicFilterExpr andOperator+ _ <- andOperator+ And e1 <$> filterExpr endParser+ search :: Parser JSONPathElement search = do _ <- char '.'- isDot <- (== '.') <$> peekChar'- if isDot- then Search <$> many1 jsonPathElement- else fail "not a search element"+ _ <- lookAhead (char '.')+ Search <$> some jsonPathElement searchBeginningWithSlice :: Parser JSONPathElement searchBeginningWithSlice = do _ <- string ".."- isBracket <- (== '[') <$> peekChar'- if isBracket- then Search <$> many1 jsonPathElement- else fail "not a search element"+ _ <- lookAhead (char '[')+ Search <$> some jsonPathElement beginningPoint :: Parser BeginningPoint-beginningPoint = do- ((char '$' $> Root) <|> (char '@' $> CurrentObject))+beginningPoint =+ try (char '$' $> Root)+ <|> (char '@' $> CurrentObject) condition :: Parser Condition-condition = ignoreSurroundingSpace- $ string "==" $> Equal- <|> string "!=" $> NotEqual- <|> string ">" $> GreaterThan- <|> string "<" $> SmallerThan+condition =+ ignoreSurroundingSpace $+ string "==" $> Equal+ <|> string "!=" $> NotEqual+ <|> string "<=" $> SmallerThanOrEqual+ <|> string ">=" $> GreaterThanOrEqual+ <|> string ">" $> GreaterThan+ <|> string "<" $> SmallerThan -literal :: Parser Literal-literal = do- (LitNumber <$> signed decimal)- <|> LitString <$> (char '"' *> A.takeWhile (/= '"') <* char '"')+comparable :: Parser a -> Parser Comparable+comparable endParser = do+ CmpNumber <$> L.scientific+ <|> CmpString <$> quotedString+ <|> CmpBool <$> bool+ <|> CmpNull <$ string "null"+ <|> CmpPath <$> singularPath endParser +bool :: Parser Bool+bool =+ True <$ string "true"+ <|> False <$ string "false"+ ignoreSurroundingSpace :: Parser a -> Parser a-ignoreSurroundingSpace p = many' space *> p <* many' space+ignoreSurroundingSpace p = space *> p <* space -ignoreSurroundingSqBr :: Parser a -> Parser a-ignoreSurroundingSqBr p = char '[' *> p <* char ']'+inSqBr :: Parser a -> Parser a+inSqBr p = openingSqBr *> p <* closingSqBr++openingSqBr :: Parser Char+openingSqBr = ignoreSurroundingSpace (char '[')++closingSqBr :: Parser Char+closingSqBr = ignoreSurroundingSpace (char ']')++inParens :: Parser a -> Parser a+inParens p = openingParen *> p <* closingParen++openingParen :: Parser Char+openingParen = ignoreSurroundingSpace (char '(')++closingParen :: Parser Char+closingParen = ignoreSurroundingSpace (char ')')++quotedString :: Parser Text+quotedString = ignoreSurroundingSpace $ Text.pack <$> (inQuotes '"' <|> inQuotes '\'')+ where+ inQuotes quoteChar =+ char quoteChar *> manyTill L.charLiteral (char quoteChar)++uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uncurry3 f (a, b, c) = f a b c
src/Data/JSONPath/Types.hs view
@@ -1,45 +1,80 @@ module Data.JSONPath.Types- ( BeginningPoint(..)- , Condition(..)- , Literal(..)- , JSONPathElement(..)- , SliceElement(..)- , module Data.JSONPath.ExecutionResult+ ( BeginningPoint (..),+ Condition (..),+ Comparable (..),+ JSONPathElement (..),+ UnionElement (..),+ FilterExpr (..),+ SingularPathElement (..),+ SingularPath (..), ) where +import Data.Scientific (Scientific) import Data.Text-import Data.JSONPath.ExecutionResult -data BeginningPoint = Root- | CurrentObject+data BeginningPoint+ = Root+ | CurrentObject deriving (Show, Eq) -data Condition = Equal- | NotEqual- | GreaterThan- | SmallerThan+-- | A JSONPath which finds at max one value, given a beginning point. Used by+-- 'FilterExpr' for 'ExistsExpr' and 'ComparisonExpr'.+data SingularPath+ = SingularPath BeginningPoint [SingularPathElement] deriving (Show, Eq) -data Literal = LitNumber Int- | LitString Text+data SingularPathElement+ = Key Text+ | Index Int deriving (Show, Eq) -data SliceElement = SingleIndex Int- | SimpleSlice Int Int- | SliceWithStep Int Int Int- | SliceTo Int- | SliceToWithStep Int Int- | SliceFrom Int- | SliceFromWithStep Int Int- | SliceWithOnlyStep Int+data Comparable+ = CmpNumber Scientific+ | CmpString Text+ | CmpBool Bool+ | CmpNull+ | CmpPath SingularPath deriving (Show, Eq) -data JSONPathElement = KeyChild Text- | KeyChildren [Text]- | AnyChild- | Slice SliceElement- | SliceUnion SliceElement SliceElement- | Filter BeginningPoint [JSONPathElement] Condition Literal- | Search [JSONPathElement]+data Condition+ = Equal+ | NotEqual+ | GreaterThan+ | SmallerThan+ | GreaterThanOrEqual+ | SmallerThanOrEqual+ deriving (Show, Eq)++data FilterExpr+ = ExistsExpr SingularPath+ | ComparisonExpr Comparable Condition Comparable+ | And FilterExpr FilterExpr+ | Or FilterExpr FilterExpr+ | Not FilterExpr+ deriving (Show, Eq)++-- | Elements which can occur inside a union+data UnionElement+ = UEKeyChild Text+ | UEIndexChild Int+ | UESlice (Maybe Int) (Maybe Int) (Maybe Int)+ deriving (Show, Eq)++-- | A 'JSONPath' is a list of 'JSONPathElement's.+data JSONPathElement+ = -- | '$.foo' or '$["foo"]'+ KeyChild Text+ | -- | '$[1]'+ IndexChild Int+ | -- | '$[*]'+ AnyChild+ | -- | '$[1:7]', '$[0:10:2]', '$[::2]', '$[::]', etc.+ Slice (Maybe Int) (Maybe Int) (Maybe Int)+ | -- | '$[0,1,9]' or '$[0, 1:2, "foo", "bar"]'+ Union [UnionElement]+ | -- | '$[?(@.foo == 42)]', '$[?(@.foo > @.bar)]', etc.+ Filter FilterExpr+ | -- | '$..foo.bar'+ Search [JSONPathElement] deriving (Show, Eq)
test/Data/JSONPathSpec.hs view
@@ -1,41 +1,41 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+ module Data.JSONPathSpec where +import Control.Monad.IO.Class (liftIO) import Data.Aeson import Data.Aeson.Casing-import Data.Aeson.Text import Data.Aeson.TH-import Data.Attoparsec.Text+import Data.Aeson.Text+import Data.Bifunctor (Bifunctor (first))+import qualified Data.ByteString.Lazy as LBS import Data.Either import Data.FileEmbed import Data.JSONPath-import Data.Text (Text, unpack)+import Data.Text (Text, unpack)+import qualified Data.Text.Lazy as LazyText+import qualified Data.Vector as V import GHC.Generics+import System.Timeout import Test.Hspec-import Test.Hspec.Attoparsec--#if !MIN_VERSION_base (4,11,0)-import Data.Semigroup ((<>))-#endif--import qualified Data.ByteString.Lazy as LBS-import qualified Data.Text.Lazy as LazyText-import qualified Data.Vector as V+import Test.Hspec.Megaparsec+import Text.Megaparsec -data Test = Test { path :: Text- , result :: Value- }- deriving (Eq, Show, Generic)+data Test = Test+ { path :: Text,+ result :: Value+ }+ deriving (Eq, Show, Generic) -data TestGroup = TestGroup { groupTitle :: Text- , groupData :: Value- , groupTests :: [Test]- }- deriving (Eq, Show, Generic)+data TestGroup = TestGroup+ { groupTitle :: Text,+ groupData :: Value,+ groupTests :: [Test]+ }+ deriving (Eq, Show, Generic) $(deriveJSON defaultOptions ''Test) $(deriveJSON (aesonPrefix snakeCase) ''TestGroup)@@ -44,35 +44,56 @@ spec = let testFiles = map snd $(embedDir "test/resources/json-path-tests") testVals :: Either String [TestGroup]- testVals = sequenceA $ map (eitherDecode . LBS.fromStrict) testFiles- in case testVals of- Left e -> describe "JSONPath Tests"- $ it "shouldn't fail to parse test files"- $ expectationFailure ("failed to parse test files with error: \n" <> e)- Right gs -> describe "JSONPath"- $ do- mapM_ group gs- describe "Parser" $ do- it "should parse basic things" $ do- (".foo" :: Text) ~> (jsonPathElement <* endOfInput)- `shouldParse` KeyChild "foo"- ("$.foo" :: Text) ~> (jsonPath <* endOfInput)- `shouldParse` [KeyChild "foo"]+ testVals = traverse (eitherDecode . LBS.fromStrict) testFiles+ in case testVals of+ Left e ->+ describe "JSONPath Tests" $+ it "shouldn't fail to parse test files" $+ expectationFailure ("failed to parse test files with error: \n" <> e)+ Right gs -> describe "JSONPath" $+ do+ mapM_ group gs+ describe "Parser" $ do+ it "should parse basic things" $ do+ parse (jsonPathElement <* eof) "" ".foo"+ `shouldParse` KeyChild "foo"+ parse (jsonPath eof) "" "$.foo"+ `shouldParse` [KeyChild "foo"] parseJSONPath :: Text -> Either String [JSONPathElement]-parseJSONPath = parseOnly (jsonPath <* endOfInput)+parseJSONPath = first errorBundlePretty . parse (jsonPath eof) "" group :: TestGroup -> Spec-group TestGroup{..} = describe (unpack groupTitle)- $ mapM_ (test groupData) groupTests+group TestGroup {..} = do+ describe (unpack groupTitle) $+ mapM_ (test groupData) groupTests +-- | 100 ms+timeLimit :: Int+timeLimit = 100000+ test :: Value -> Test -> Spec test testData (Test path expected) =- let result = parseJSONPath path >>= (flip executeJSONPathEither testData)- in it (unpack path) $- case expected of- Array a -> case result of- Left err -> expectationFailure $ "Unexpected Left: " <> err- Right r -> r `shouldMatchList` (V.toList a)- Bool False -> result `shouldSatisfy` isLeft- v -> expectationFailure $ "Invalid result in test data " <> (LazyText.unpack $ encodeToLazyText v)+ it (unpack path) $ do+ mResult <-+ liftIO $+ timeout timeLimit $ do+ -- Using '$!' here ensures that the computation is strict, so this can+ -- be timed out properly+ pure $! do+ parsed <- parseJSONPath path+ Right $ executeJSONPath parsed testData++ result <- case mResult of+ Just r -> pure r+ Nothing -> do+ expectationFailure "JSONPath execution timed out"+ undefined++ case expected of+ Array a -> case result of+ Left e -> expectationFailure $ "Unexpected Left: " <> e+ -- TODO: Define order of result and make this `shouldBe`+ Right r -> r `shouldMatchList` V.toList a+ Bool False -> result `shouldSatisfy` isLeft+ v -> expectationFailure $ "Invalid result in test data " <> LazyText.unpack (encodeToLazyText v)
test/resources/json-path-tests/DotOperator.json view
@@ -1,48 +1,41 @@-{ - "title": "Dot Operator", - "data": { - "firstName": "John", - "lastName": "Doe", - "home": "987-654-3210", - "mobile": null - }, - "tests": [ - { - "path": "$.firstName", - "result": [ - "John" - ] - }, - { - "path": "$.lastName", - "result": [ - "Doe" - ] - }, - { - "path": "$.home", - "result": [ - "987-654-3210" - ] - }, - { - "path": "$.mobile", - "result": [ - null - ] - }, - { - "path": "$.missingKey", - "result": false - }, - { - "path": "$.*", - "result": [ - "John", - "Doe", - "987-654-3210", - null - ] - } - ] -} +{+ "title": "Dot Operator",+ "data": {+ "firstName": "John",+ "lastName": "Doe",+ "home": "987-654-3210",+ "mobile": null+ },+ "tests": [{+ "path": "$.firstName",+ "result": [+ "John"+ ]+ }, {+ "path": "$.lastName",+ "result": [+ "Doe"+ ]+ }, {+ "path": "$.home",+ "result": [+ "987-654-3210"+ ]+ }, {+ "path": "$.mobile",+ "result": [+ null+ ]+ }, {+ "path": "$.missingKey",+ "result": []+ }, {+ "path": "$.*",+ "result": [+ "John",+ "Doe",+ "987-654-3210",+ null+ ]+ }]+}
+ test/resources/json-path-tests/EmptyPath.json view
@@ -0,0 +1,50 @@+{+ "title" : "Chained Elements",+ "data" : {+ "firstName" : "John",+ "lastName" : "Doe",+ "eyes" : "blue",+ "children" : [{+ "firstName" : "Sally",+ "lastName" : "Doe",+ "favoriteGames" : ["Halo", "Minecraft", "Lego: Star Wars"]+ }, {+ "firstName" : "Mike",+ "lastName" : "Doe",+ "eyes" : "green"+ }]+ },+ "tests" : [{+ "path" : "",+ "result" : [{+ "firstName" : "John",+ "lastName" : "Doe",+ "eyes" : "blue",+ "children" : [{+ "firstName" : "Sally",+ "lastName" : "Doe",+ "favoriteGames" : ["Halo", "Minecraft", "Lego: Star Wars"]+ }, {+ "firstName" : "Mike",+ "lastName" : "Doe",+ "eyes" : "green"+ }]+ }]+ }, {+ "path" : "$",+ "result" : [{+ "firstName" : "John",+ "lastName" : "Doe",+ "eyes" : "blue",+ "children" : [{+ "firstName" : "Sally",+ "lastName" : "Doe",+ "favoriteGames" : ["Halo", "Minecraft", "Lego: Star Wars"]+ }, {+ "firstName" : "Mike",+ "lastName" : "Doe",+ "eyes" : "green"+ }]+ }]+ }]+}
+ test/resources/json-path-tests/FieldAccess.json view
@@ -0,0 +1,57 @@+{+ "title": "Field Access",+ "data": {+ "a": 1,+ "b": {"c" : { "d" : 2 }},+ "e.f": {"g.h" : 3},+ "!@#$+=- %)@!(*%\"": 4,+ ":": 5,+ "": 6,+ "'": 7,+ "\"": 8,+ "\\'": 9+ },+ "tests": [{+ "path": "$.a",+ "result": [1]+ }, {+ "path": "$['a']",+ "result": [1]+ }, {+ "path": "$[\"a\"]",+ "result": [1]+ }, {+ "path": "$[ 'a']",+ "result": [1]+ }, {+ "path": "$[ \"a\"]",+ "result": [1]+ }, {+ "path": "$['b']['c'].d",+ "result": [2]+ }, {+ "path": "$['e.f']['g.h']",+ "result": [3]+ }, {+ "path": "$['!@#$+=- %)@!(*%\"']",+ "result": [4]+ }, {+ "path": "$[':']",+ "result": [5]+ }, {+ "path": "$['']",+ "result": [6]+ }, {+ "path": "$[\"\"]",+ "result": [6]+ }, {+ "path": "$['\\'']",+ "result": [7]+ }, {+ "path": "$[\"\\\"\"]",+ "result": [8]+ }, {+ "path": "$['\\\\\\'']",+ "result": [9]+ }]+}
test/resources/json-path-tests/FilterExpressionSubscriptOperator.json view
@@ -1,32 +1,326 @@-{ - "title" : "Filter Expression Subscript Operator", - "data" : [4, - "string", - false, - [1, 2, 3], { - "index" : 1, - "name" : "John Doe", - "occupation" : "Architect" - }, { - "index" : 2, - "name" : "Jane Smith", - "occupation" : "Architect", - "color" : "blue" - } - ], - "tests" : [{ - "path" : "$[?(@.occupation == \"Architect\")]", - "result" : [{ - "index" : 1, - "name" : "John Doe", - "occupation" : "Architect" - }, { - "index" : 2, - "name" : "Jane Smith", - "occupation" : "Architect", - "color" : "blue" - } - ] - } - ] -} +{+ "title" : "Filter Expression Subscript Operator",+ "data" : {"arr" : [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ } , {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }+ ],+ "obj": {"foo": 1, "bar": 2, "baz": {"qux": 3}, "bux": 4}+ },+ "tests" : [{+ "path" : "$.arr[?(@.occupation == \"Architect\")]",+ "result" : [{+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ }]+ }, {+ "path" : "$.arr[?(@.occupation != \"Architect\")]",+ "result" : [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }]+ }, {+ "path" : "$.arr[?(@.name == \"John Smith\")]",+ "result" : [{+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }]+ }, {+ "path" : "$.arr[?(@.name == 'John Smith')]",+ "result" : [{+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }]+ }, {+ "path" : "$.arr[?(@.val > 3)]",+ "result" : [{ "val" : 4 }]+ }, {+ "path" : "$.arr[?(3 < @.val)]",+ "result" : [{ "val" : 4 }]+ }, {+ "path" : "$.arr[?@.val > 3]",+ "result" : [{ "val" : 4 }]+ }, {+ "path" : "$.arr[? @.val > 3]",+ "result" : [{ "val" : 4 }]+ }, {+ "path" : "$.arr[?(@.val >= 3)]",+ "result" : [{ "val" : 4 }, { "val" : 3 }]+ }, {+ "path" : "$.arr[?(@.val < 3)]",+ "result" : [{ "val" : 1 }, { "val" : 2 }]+ }, {+ "path" : "$.arr[?@.val]",+ "result" : [{ "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 }]+ }, {+ "path" : "$.arr[?(@.val <= 3)]",+ "result" : [{ "val" : 1 }, { "val" : 2 }, { "val" : 3 }]+ }, {+ "path" : "$.arr[?(@.val <= 1) || (@.val > 3)]",+ "result" : [{ "val" : 1 }, { "val" : 4 }]+ }, {+ "path" : "$.arr[?(@.val <= 1) || (@.val > 1)]",+ "result" : [{ "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 }]+ }, {+ "path" : "$.arr[?@.val <= 1 || @.val > 1]",+ "result" : [{ "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 }]+ }, {+ "path" : "$.arr[?@.val > 1 && @.val < 4]",+ "result" : [{ "val": 2 }, { "val": 3 }]+ }, {+ "path" : "$.arr[?@.val <= 1 && @.val > 4]",+ "result" : []+ }, {+ "path" : "$.arr[?@.val <= 1 && @.val >= 4 || @.val < 3]",+ "result" : [{ "val": 1 }, { "val": 2 }]+ }, {+ "path" : "$.arr[?(@.val <= 1 && @.val >= 4) || @.val < 3]",+ "result" : [{ "val": 1 }, { "val": 2 }]+ }, {+ "path" : "$.arr[?@.val >= 4 || @.val < 3 && @.val <= 1]",+ "result" : [{ "val": 1 }, { "val": 4 }]+ }, {+ "path" : "$.arr[?@.val >= 4 || (@.val < 3 && @.val <= 1)]",+ "result" : [{ "val": 1 }, { "val": 4 }]+ }, {+ "path" : "$.arr[?@.val == 4 || @.val == 3 || @.val == 1]",+ "result" : [{ "val": 1 }, { "val": 4 }, { "val": 3}]+ }, {+ "path" : "$.arr[?@.val <= 4 && @.val <= 3 && @.val <= 1]",+ "result" : [{ "val": 1 }]+ }, {+ "path" : "$.arr[?@.val <= 4 && @.val <= 3 && @.val <= 1 || @.val > 1]",+ "result" : [{ "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 }]+ }, {+ "path" : "$.arr[?!@.val]",+ "result" : [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ } , {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }+ ]+ }, {+ "path" : "$.arr[?!@.val == 1]",+ "comment": "This is not allowed according to the IETF Draft.",+ "result" : [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ } , {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "val": 2 }, { "val": 4 }, { "val": 3 },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }+ ]+ }, {+ "path" : "$.arr[?!(@.val == 1)]",+ "result" : [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ } , {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "val": 2 }, { "val": 4 }, { "val": 3 },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }+ ]+ }, {+ "path" : "$.arr[?(@.textval > \"c\")]",+ "result" : [{ "textval" : "d" }]+ }, {+ "path" : "$.arr[?(@.textval >= \"c\")]",+ "result" : [{ "textval" : "d" }, { "textval" : "c" }]+ }, {+ "path" : "$.arr[?(@.textval < \"b\")]",+ "result" : [{ "textval" : "a" }]+ }, {+ "path" : "$.arr[?(@.textval <= \"b\")]",+ "result" : [{ "textval" : "a" }, { "textval" : "b" }]+ }, {+ "path" : "$.arr[?(@.textval <= 'b')]",+ "result" : [{ "textval" : "a" }, { "textval" : "b" }]+ }, {+ "path" : "$.arr[?(@.val == \"1\")]",+ "result" : []+ }, {+ "path": "$.obj[?@ > 1]",+ "result": [2, 4]+ }, {+ "path": "$.obj[?(@.qux)]",+ "result": [{"qux": 3}]+ }, {+ "path": "$[?@.foo < @.baz.qux]",+ "result": [{"foo": 1, "bar": 2, "baz": {"qux": 3}, "bux": 4}]+ }, {+ "path": "$[?@.foo < @.baz.qux]",+ "result": [{"foo": 1, "bar": 2, "baz": {"qux": 3}, "bux": 4}]+ }, {+ "path": "$[?@[0] == @[8].index]",+ "comment": "Both arr and obj match this as 4 == 4 for arr and no_path == no_path for obj",+ "result": [+ [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ } , {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }+ ],+ {"foo": 1, "bar": 2, "baz": {"qux": 3}, "bux": 4}+ ]+ }, {+ "path": "$[?@[0] && @[0] == @[8].index]",+ "result": [+ [4,+ "string",+ false,+ null,+ [1, 2, 3], {+ "index" : 1,+ "name" : "John Doe",+ "occupation" : "Architect"+ }, {+ "index" : 2,+ "name" : "Jane Smith",+ "occupation" : "Architect",+ "color" : "blue"+ } , {+ "index" : 3,+ "name" : "John Smith",+ "occupation" : "Plumber"+ }, {+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ },+ { "val": 1 }, { "val": 2 }, { "val": 4 }, { "val": 3 },+ { "textval": "a" }, { "textval": "b" }, { "textval": "d" }, { "textval": "c" }+ ]+ ]+ }, {+ "path": "$.arr[?@ > 3]",+ "result": [4]+ }, {+ "path": "$.arr[?@ == null]",+ "result": [null]+ }, {+ "path": "$.arr[?@ == false]",+ "result": [false]+ }, {+ "path": "$.arr[?@ == true]",+ "result": []+ }, {+ "path": "$.arr[?@.index == $.obj.bux]",+ "result": [{+ "index" : 4,+ "name" : "Jane Doe",+ "occupation" : "Pilot"+ }]+ }+ ]++}
test/resources/json-path-tests/IndexedSubscriptOperator.json view
@@ -1,58 +1,124 @@-{ - "title" : "Indexed Subscript Operator", - "data" : [ - "John Doe", - 36, - "Architect", - "one", - "three", - "five" - ], - "tests" : [{ - "path" : "$[2]", - "result" : ["Architect"] - }, { - "path" : "$[-1]", - "result" : ["five"] - }, { - "path" : "$[10]", - "result" : false - }, { - "path" : "$[1:4]", - "result" : [36, "Architect", "one"] - }, { - "path" : "$[1:4:2]", - "result" : [36, "one"] - }, { - "path" : "$[:4:2]", - "result" : ["John Doe", "Architect"] - }, { - "path" : "$[1::2]", - "result" : [36, "one", "five"] - }, { - "path" : "$[::2]", - "result" : ["John Doe", "Architect", "three"] - }, { - "path" : "$[1:]", - "result" : [36, "Architect", "one", "three", "five"] - }, { - "path" : "$[:3]", - "result" : ["John Doe", 36, "Architect"] - }, { - "path" : "$[0,3]", - "result" : ["John Doe", "one"] - }, { - "path" : "$[0,1::2]", - "result" : ["John Doe", 36, "one", "five"] - }, { - "path" : "$[0:2,4:6]", - "result" : ["John Doe", 36, "three", "five"] - }, { - "path" : "$[0:2,4]", - "result" : ["John Doe", 36, "three"] - }, { - "path" : "$[*]", - "result" : ["John Doe", 36, "Architect", "one", "three", "five"] - } - ] -} +{+ "title" : "Indexed Subscript Operator",+ "data" : [+ "John Doe",+ 36,+ "Architect",+ "one",+ "three",+ "five",+ {"string": "str", "array": ["arr"]}+ ],+ "tests" : [{+ "path" : "$[2]",+ "result" : ["Architect"]+ }, {+ "path" : "$[ 2 ]",+ "result" : ["Architect"]+ },{+ "path" : "$[-1]",+ "result" : [{"string": "str", "array": ["arr"]}]+ }, {+ "path" : "$[10]",+ "result" : []+ }, {+ "path" : "$[1:4]",+ "result" : [36, "Architect", "one"]+ }, {+ "path" : "$[1:4:2]",+ "result" : [36, "one"]+ }, {+ "path" : "$[ 1 : 4 : 2 ]",+ "result" : [36, "one"]+ }, {+ "path" : "$[:4:2]",+ "result" : ["John Doe", "Architect"]+ }, {+ "path" : "$[1::2]",+ "result" : [36, "one", "five"]+ }, {+ "path" : "$[::2]",+ "result" : ["John Doe", "Architect", "three", {"string": "str", "array": ["arr"]}]+ }, {+ "path" : "$[1:]",+ "result" : [36, "Architect", "one", "three", "five", {"string": "str", "array": ["arr"]}]+ }, {+ "path" : "$[:3]",+ "result" : ["John Doe", 36, "Architect"]+ }, {+ "path" : "$[0,3]",+ "result" : ["John Doe", "one"]+ }, {+ "path" : "$[0,1::2]",+ "result" : ["John Doe", 36, "one", "five"]+ }, {+ "path" : "$[0:2,4:6]",+ "result" : ["John Doe", 36, "three", "five"]+ }, {+ "path" : "$[0:2,4]",+ "result" : ["John Doe", 36, "three"]+ }, {+ "path" : "$[ 0:2 , 4]",+ "result" : ["John Doe", 36, "three"]+ }, {+ "path" : "$[0:2, 'foo']",+ "result" : ["John Doe", 36]+ }, {+ "path" : "$[-1]['string', 'array']",+ "result" : ["str", ["arr"]]+ }, {+ "path" : "$[-1][\"not-here\", \"array\", \"string\"]",+ "result" : [["arr"], "str"]+ }, {+ "path" : "$[*]",+ "result" : ["John Doe", 36, "Architect", "one", "three", "five", {"string": "str", "array": ["arr"]}]+ }, {+ "path": "$[2:113667776004]",+ "result": ["Architect", "one", "three", "five", {"string": "str", "array": ["arr"]}]+ }, {+ "path": "$[-113667776004:2]",+ "result": ["John Doe", 36]+ }, {+ "path": "$[::0]",+ "result": []+ }, {+ "path": "$[5:0:-1]",+ "result": [+ "five",+ "three",+ "one",+ "Architect",+ 36+ ]+ }, {+ "path": "$[-1:0:-1]",+ "result": [+ "five",+ "three",+ "one",+ "Architect",+ 36,+ {"string": "str", "array": ["arr"]}+ ]+ }, {+ "path": "$[::-2]",+ "result": [+ {"string": "str", "array": ["arr"]},+ "three",+ "Architect",+ "John Doe"+ ]+ }, {+ "path": "$[::]",+ "result": [+ "John Doe",+ 36,+ "Architect",+ "one",+ "three",+ "five",+ {"string": "str", "array": ["arr"]}+ ]+ }+ ]+}
test/resources/json-path-tests/SearchOperator.json view
@@ -1,39 +1,38 @@-{ - "title" : "Search Operator", - "data" : { - "firstName" : "John", - "lastName" : "Doe", - "eyes" : "blue", - "children" : [{ - "firstName" : "Sally", - "lastName" : "Doe", - "favoriteGames" : ["Halo", "Minecraft", "Lego: Star Wars"] - }, { - "firstName" : "Mike", - "lastName" : "Doe", - "eyes" : "green" - } - ] - }, - "tests" : [{ - "path" : "$..firstName", - "result" : ["John", "Sally", "Mike"] - }, { - "path" : "$..lastName", - "result" : ["Doe", "Doe", "Doe"] - }, { - "path" : "$..eyes", - "result" : ["blue", "green"] - }, { - "path" : "$..missingKey", - "result" : false - }, { - "path" : "$..[1]", - "result" : [{ - "firstName" : "Mike", - "lastName" : "Doe", - "eyes" : "green" - }, "Minecraft"] - } - ] -} +{+ "title" : "Search Operator",+ "data" : {+ "firstName" : "John",+ "lastName" : "Doe",+ "eyes" : "blue",+ "children" : [{+ "firstName" : "Sally",+ "lastName" : "Doe",+ "favoriteGames" : ["Halo", "Minecraft", "Lego: Star Wars"]+ }, {+ "firstName" : "Mike",+ "lastName" : "Doe",+ "eyes" : "green"+ }+ ]+ },+ "tests" : [{+ "path" : "$..firstName",+ "result" : ["John", "Sally", "Mike"]+ }, {+ "path" : "$..lastName",+ "result" : ["Doe", "Doe", "Doe"]+ }, {+ "path" : "$..eyes",+ "result" : ["blue", "green"]+ }, {+ "path" : "$..missingKey",+ "result" : []+ }, {+ "path" : "$..[1]",+ "result" : [{+ "firstName" : "Mike",+ "lastName" : "Doe",+ "eyes" : "green"+ }, "Minecraft"]+ }]+}