packages feed

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 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"]+    }]+}