aeson-jsonpath 0.1.0.0 → 0.2.0.0
raw patch · 7 files changed
+410/−147 lines, 7 filesdep +template-haskelldep +textdep −protoludedep ~basePVP ok
version bump matches the API change (PVP)
Dependencies added: template-haskell, text
Dependencies removed: protolude
Dependency ranges changed: base
API changes (from Hackage documentation)
- Data.Aeson.JSONPath.Parser: JSPBracketed :: [JSPSelector] -> JSPChildSegment
- Data.Aeson.JSONPath.Parser: JSPMemberNameSH :: JSPNameSelector -> JSPChildSegment
- Data.Aeson.JSONPath.Parser: JSPSegWildcard :: JSPWildcardT -> JSPChildSegment
- Data.Aeson.JSONPath.Parser: data JSPQuery
+ Data.Aeson.JSONPath: jsonPath :: QuasiQuoter
+ Data.Aeson.JSONPath.Parser: JSPChildBracketed :: [JSPSelector] -> JSPChildSegment
+ Data.Aeson.JSONPath.Parser: JSPChildMemberNameSH :: JSPNameSelector -> JSPChildSegment
+ Data.Aeson.JSONPath.Parser: JSPChildWildSeg :: JSPWildcardT -> JSPChildSegment
+ Data.Aeson.JSONPath.Parser: JSPDescBracketed :: [JSPSelector] -> JSPDescSegment
+ Data.Aeson.JSONPath.Parser: JSPDescMemberNameSH :: JSPNameSelector -> JSPDescSegment
+ Data.Aeson.JSONPath.Parser: JSPDescSeg :: JSPDescSegment -> JSPSegment
+ Data.Aeson.JSONPath.Parser: JSPDescWildSeg :: JSPWildcardT -> JSPDescSegment
+ Data.Aeson.JSONPath.Parser: data JSPDescSegment
+ Data.Aeson.JSONPath.Parser: instance GHC.Classes.Eq Data.Aeson.JSONPath.Parser.JSPDescSegment
+ Data.Aeson.JSONPath.Parser: instance GHC.Show.Show Data.Aeson.JSONPath.Parser.JSPDescSegment
+ Data.Aeson.JSONPath.Parser: instance Language.Haskell.TH.Syntax.Lift Data.Aeson.JSONPath.Parser.JSPChildSegment
+ Data.Aeson.JSONPath.Parser: instance Language.Haskell.TH.Syntax.Lift Data.Aeson.JSONPath.Parser.JSPDescSegment
+ Data.Aeson.JSONPath.Parser: instance Language.Haskell.TH.Syntax.Lift Data.Aeson.JSONPath.Parser.JSPQuery
+ Data.Aeson.JSONPath.Parser: instance Language.Haskell.TH.Syntax.Lift Data.Aeson.JSONPath.Parser.JSPSegment
+ Data.Aeson.JSONPath.Parser: instance Language.Haskell.TH.Syntax.Lift Data.Aeson.JSONPath.Parser.JSPSelector
+ Data.Aeson.JSONPath.Parser: instance Language.Haskell.TH.Syntax.Lift Data.Aeson.JSONPath.Parser.JSPWildcardT
+ Data.Aeson.JSONPath.Parser: newtype JSPQuery
- Data.Aeson.JSONPath: runJSPQuery :: Text -> Value -> Either ParseError Value
+ Data.Aeson.JSONPath: runJSPQuery :: JSPQuery -> Value -> Value
Files
- CHANGELOG.md +8/−0
- aeson-jsonpath.cabal +14/−8
- src/Data/Aeson/JSONPath.hs +68/−16
- src/Data/Aeson/JSONPath/Parser.hs +81/−27
- test/Main.hs +1/−1
- test/ParserSpec.hs +34/−10
- test/QuerySpec.hs +204/−85
CHANGELOG.md view
@@ -1,3 +1,11 @@+## 0.2.0.0++- Remove dependency on `protolude`+- Fix parsing bug with Wildcard Selector+- Implement Descendant Segment+- Fix allowed characters in the `member-name-shorthand`+- Add `QuasiQuoter` for compile-time syntax checking+ ## 0.1.0.0 - Initial Release
aeson-jsonpath.cabal view
@@ -1,5 +1,5 @@ name: aeson-jsonpath-version: 0.1.0.0+version: 0.2.0.0 synopsis: Parse and run JSONPath queries on Aeson documents description: RFC 9535 compliant JSONPath parsing and querying package. JSONPath is similar to XPath for querying XML documents.@@ -15,8 +15,10 @@ cabal-version: >= 1.10 tested-with:- -- On Ubuntu Latest- GHC == 9.4.5+ GHC == 9.4.8+ , GHC == 9.6.6+ , GHC == 9.8.4+ , GHC == 9.10.1 source-repository head type: git@@ -26,30 +28,34 @@ default-language: Haskell2010 default-extensions: OverloadedStrings NoImplicitPrelude+ QuasiQuotes hs-source-dirs: src exposed-modules: Data.Aeson.JSONPath Data.Aeson.JSONPath.Parser build-depends: aeson >= 2.0.3 && < 2.3- , base >= 4.9 && < 4.20+ , base >= 4.9 && < 4.22 , parsec >= 3.1.11 && < 3.2- , protolude >= 0.3.1 && < 0.4+ , template-haskell >= 2.12 && < 2.24+ , text >= 1.2.2 && < 2.2 , vector >= 0.11 && < 0.14 - ghc-options: -Wall+ ghc-options: -Wall -Wunused-packages test-suite spec type: exitcode-stdio-1.0 default-language: Haskell2010 default-extensions: OverloadedStrings NoImplicitPrelude+ QuasiQuotes hs-source-dirs: test main-is: Main.hs other-modules: ParserSpec QuerySpec build-depends: aeson >= 2.0.3 && < 2.3 , aeson-jsonpath+ , base >= 4.9 && < 4.22 , hspec >= 2.3 && < 2.12 , parsec >= 3.1.11 && < 3.2- , protolude >= 0.3.1 && < 0.4+ , vector >= 0.11 && < 0.14 - ghc-options: -Wall+ ghc-options: -Wall -Wunused-packages
src/Data/Aeson/JSONPath.hs view
@@ -1,48 +1,88 @@ module Data.Aeson.JSONPath- ( runJSPQuery )+ ( runJSPQuery+ , jsonPath) where import qualified Data.Aeson as JSON-import qualified Text.ParserCombinators.Parsec as P import qualified Data.Aeson.Key as K import qualified Data.Aeson.KeyMap as KM import qualified Data.Vector as V+import qualified Text.ParserCombinators.Parsec as P import Data.Aeson.JSONPath.Parser (JSPQuery (..) , JSPSegment (..) , JSPChildSegment (..)+ , JSPDescSegment (..) , JSPSelector (..) , JSPWildcardT (..) , pJSPQuery)-import Protolude+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Language.Haskell.TH.Quote (QuasiQuoter (..))+import Language.Haskell.TH.Syntax (lift) +import Prelude -runJSPQuery :: Text -> JSON.Value -> Either P.ParseError JSON.Value-runJSPQuery query document = do- jspath <- P.parse pJSPQuery ("failed to parse query: " <> toS query) (toS query)- return $ traverseJSPQuery jspath document +jsonPath :: QuasiQuoter+jsonPath = QuasiQuoter+ { quoteExp = \query -> case P.parse pJSPQuery ("failed to parse query: " <> query) query of+ Left err -> fail $ show err+ Right ex -> lift ex+ , quotePat = error "Error: quotePat"+ , quoteType = error "Error: quoteType"+ , quoteDec = error "Error: quoteDec"+ } +-- | Run JSONPath query+--+-- @+-- {-\# LANGUAGE QuasiQuotes \#-}+--+-- import Data.Aeson (Value)+-- import Data.Aeson.JSONPath (runJSPQuery, jsonPath)+--+-- book :: 'Value'+-- book = [jsonPath|$.store.books[2]|]+-- @+runJSPQuery :: JSPQuery -> JSON.Value -> JSON.Value+runJSPQuery = traverseJSPQuery++ traverseJSPQuery :: JSPQuery -> JSON.Value -> JSON.Value-traverseJSPQuery (JSPRoot segs) doc = traverseJSPSegments segs doc+traverseJSPQuery (JSPRoot segs) = traverseJSPSegments segs traverseJSPSegments :: [JSPSegment] -> JSON.Value -> JSON.Value-traverseJSPSegments [] doc = doc-traverseJSPSegments (x:xs) doc = traverseJSPSegments xs (traverseJSPSegment x doc)+traverseJSPSegments xs doc = foldl (flip traverseJSPSegment) doc xs traverseJSPSegment :: JSPSegment -> JSON.Value -> JSON.Value traverseJSPSegment (JSPChildSeg jspChildSeg) doc = traverseJSPChildSeg jspChildSeg doc+traverseJSPSegment (JSPDescSeg jspDescSeg) doc = traverseJSPDescSeg jspDescSeg doc traverseJSPChildSeg :: JSPChildSegment -> JSON.Value -> JSON.Value-traverseJSPChildSeg (JSPBracketed sels) doc = traverseJSPSelectors sels doc-traverseJSPChildSeg (JSPMemberNameSH key) (JSON.Object obj) = fromMaybe emptyJSArray $ KM.lookup (K.fromText key) obj-traverseJSPChildSeg (JSPMemberNameSH _) _ = emptyJSArray-traverseJSPChildSeg (JSPSegWildcard JSPWildcard) doc = doc+traverseJSPChildSeg (JSPChildBracketed sels) doc = traverseJSPSelectors sels doc+traverseJSPChildSeg (JSPChildMemberNameSH key) (JSON.Object obj) = fromMaybe emptyJSArray $ KM.lookup (K.fromText key) obj+traverseJSPChildSeg (JSPChildMemberNameSH _) _ = emptyJSArray+traverseJSPChildSeg (JSPChildWildSeg JSPWildcard) doc = doc +traverseJSPDescSeg :: JSPDescSegment -> JSON.Value -> JSON.Value+traverseJSPDescSeg (JSPDescBracketed sels) doc = JSON.Array $ V.map (traverseJSPSelectors sels) (allElemsRecursive doc)+traverseJSPDescSeg (JSPDescMemberNameSH key) doc = traverseDescMembers key doc+traverseJSPDescSeg (JSPDescWildSeg JSPWildcard) doc = JSON.Array $ allElemsRecursive doc++-- TODO: Clean this super messy code, might require some refactoring+traverseDescMembers :: Text -> JSON.Value -> JSON.Value+traverseDescMembers key (JSON.Object obj) = JSON.Array $ V.concat [+ maybe V.empty V.singleton $ KM.lookup (K.fromText key) obj,+ V.map (traverseDescMembers key) (allElemsRecursive (JSON.Array $ V.fromList $ KM.elems obj))+ ]+traverseDescMembers key ar@(JSON.Array _) = JSON.Array $ V.map (traverseDescMembers key) (allElemsRecursive ar)+traverseDescMembers _ _ = JSON.Array V.empty+ traverseJSPSelectors :: [JSPSelector] -> JSON.Value -> JSON.Value traverseJSPSelectors sels doc = JSON.Array $ V.concat $ map traverse' sels where@@ -51,10 +91,11 @@ traverseJSPSelector :: JSPSelector -> JSON.Value -> V.Vector JSON.Value traverseJSPSelector (JSPNameSel key) (JSON.Object obj) = maybe V.empty V.singleton $ KM.lookup (K.fromText key) obj traverseJSPSelector (JSPNameSel _) _ = V.empty-traverseJSPSelector (JSPIndexSel idx) (JSON.Array arr) = maybe V.empty V.singleton $ (if idx >= 0 then (V.!?) arr idx else (V.!?) arr (idx + V.length arr))+traverseJSPSelector (JSPIndexSel idx) (JSON.Array arr) = maybe V.empty V.singleton (if idx >= 0 then (V.!?) arr idx else (V.!?) arr (idx + V.length arr)) traverseJSPSelector (JSPIndexSel _) _ = V.empty traverseJSPSelector (JSPSliceSel sliceVals) (JSON.Array arr) = traverseJSPSliceSelector sliceVals arr traverseJSPSelector (JSPSliceSel _) _ = V.empty+-- This does not work right with descendant segment, fix it later traverseJSPSelector (JSPWildSel JSPWildcard) doc = V.singleton doc @@ -85,9 +126,20 @@ filterSlice slice 1 = slice filterSlice slice (-1) = V.reverse slice filterSlice slice n = if n < 0 then- V.ifilter (\i _ -> i `mod` (-1 * n) == 0) $ V.reverse $ V.drop (V.length slice `mod` (-1 * n)) slice+ V.ifilter (\i _ -> i `mod` (-n) == 0) $ V.reverse $ V.drop (V.length slice `mod` (-n)) slice else V.ifilter (\i _ -> i `mod` n == 0) slice emptyJSArray :: JSON.Value emptyJSArray = JSON.Array V.empty++allElemsRecursive :: JSON.Value -> V.Vector JSON.Value+allElemsRecursive (JSON.Object obj) = V.concat [+ V.fromList (KM.elems obj),+ V.concat $ map allElemsRecursive (KM.elems obj)+ ]+allElemsRecursive (JSON.Array arr) = V.concat [+ arr,+ V.concat $ map allElemsRecursive (V.toList arr)+ ]+allElemsRecursive _ = V.empty
src/Data/Aeson/JSONPath/Parser.hs view
@@ -1,46 +1,62 @@+{-# LANGUAGE DeriveLift #-} {-# OPTIONS_GHC -Wno-unused-do-bind #-} module Data.Aeson.JSONPath.Parser ( JSPQuery (..) , JSPSegment (..) , JSPChildSegment (..)+ , JSPDescSegment (..) , JSPSelector (..) , JSPWildcardT (..) , pJSPQuery ) where +import qualified Data.Text as T import qualified Text.ParserCombinators.Parsec as P -import Text.Read (read)+import Data.Functor (($>))+import Data.Text (Text)+import Data.Char (ord)+import Text.ParserCombinators.Parsec ((<|>))+import Language.Haskell.TH.Syntax (Lift (..)) -import Protolude+import Prelude -data JSPQuery+newtype JSPQuery = JSPRoot [JSPSegment]- deriving (Eq, Show)+ deriving (Eq, Show, Lift) -- https://www.rfc-editor.org/rfc/rfc9535#name-segments-2 data JSPSegment = JSPChildSeg JSPChildSegment- deriving (Eq, Show)+ | JSPDescSeg JSPDescSegment+ deriving (Eq, Show, Lift) -- https://www.rfc-editor.org/rfc/rfc9535#name-child-segment data JSPChildSegment- = JSPBracketed [JSPSelector]- | JSPMemberNameSH JSPNameSelector- | JSPSegWildcard JSPWildcardT- deriving (Eq, Show)+ = JSPChildBracketed [JSPSelector]+ | JSPChildMemberNameSH JSPNameSelector+ | JSPChildWildSeg JSPWildcardT+ deriving (Eq, Show, Lift) ++-- https://www.rfc-editor.org/rfc/rfc9535#name-descendant-segment+data JSPDescSegment+ = JSPDescBracketed [JSPSelector]+ | JSPDescMemberNameSH JSPNameSelector+ | JSPDescWildSeg JSPWildcardT+ deriving (Eq, Show, Lift)+ -- https://www.rfc-editor.org/rfc/rfc9535#name-selectors-2 data JSPSelector = JSPNameSel JSPNameSelector | JSPIndexSel JSPIndexSelector | JSPSliceSel JSPSliceSelector | JSPWildSel JSPWildcardT- deriving (Eq, Show)+ deriving (Eq, Show, Lift) data JSPWildcardT = JSPWildcard- deriving (Eq, Show)+ deriving (Eq, Show, Lift) type JSPNameSelector = Text @@ -52,6 +68,7 @@ pJSPQuery = do P.char '$' segs <- P.many pJSPSegment+ P.eof return $ JSPRoot segs pJSPSelector :: P.Parser JSPSelector@@ -61,7 +78,7 @@ <|> P.try pJSPWildSel pJSPNameSel :: P.Parser JSPSelector-pJSPNameSel = JSPNameSel <$> toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')+pJSPNameSel = JSPNameSel . T.pack <$> (P.char '\'' *> P.many (P.noneOf "\'") <* P.char '\'') pJSPIndexSel :: P.Parser JSPSelector pJSPIndexSel = do@@ -81,42 +98,79 @@ pJSPWildSel :: P.Parser JSPSelector-pJSPWildSel = JSPWildSel <$> (P.string ".*" $> JSPWildcard)+pJSPWildSel = JSPWildSel <$> (P.char '*' $> JSPWildcard) pJSPSegment :: P.Parser JSPSegment-pJSPSegment = pJSPChildSegment+pJSPSegment = pJSPChildSegment <|> pJSPDescSegment pJSPChildSegment :: P.Parser JSPSegment pJSPChildSegment = - JSPChildSeg <$> (P.try pJSPBracketed - <|> P.try pJSPMemberNameSH - <|> P.try pJSPWildSeg)+ JSPChildSeg <$> (P.try pJSPChildBracketed + <|> P.try pJSPChildMemberNameSH + <|> P.try pJSPChildWildSeg) -pJSPBracketed :: P.Parser JSPChildSegment-pJSPBracketed = do+pJSPChildBracketed :: P.Parser JSPChildSegment+pJSPChildBracketed = do P.char '[' sel <- pJSPSelector optionalSels <- P.many pCommaSepSelectors P.char ']'- return $ JSPBracketed (sel:optionalSels)+ return $ JSPChildBracketed (sel:optionalSels) where pCommaSepSelectors :: P.Parser JSPSelector pCommaSepSelectors = P.char ',' *> P.spaces *> pJSPSelector -pJSPMemberNameSH :: P.Parser JSPChildSegment-pJSPMemberNameSH = do+pJSPChildMemberNameSH :: P.Parser JSPChildSegment+pJSPChildMemberNameSH = do P.char '.'- val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@")- return (JSPMemberNameSH val)+ P.lookAhead (P.letter <|> P.oneOf "-" <|> pUnicodeChar)+ val <- T.pack <$> P.many1 (P.alphaNum <|> (P.oneOf "-" <|> pUnicodeChar))+ return (JSPChildMemberNameSH val) -pJSPWildSeg :: P.Parser JSPChildSegment-pJSPWildSeg = JSPSegWildcard <$> (P.string ".*" $> JSPWildcard)+pJSPChildWildSeg :: P.Parser JSPChildSegment+pJSPChildWildSeg = JSPChildWildSeg <$> (P.string ".*" $> JSPWildcard) ++pJSPDescSegment :: P.Parser JSPSegment+pJSPDescSegment = + JSPDescSeg <$> (P.try pJSPDescBracketed + <|> P.try pJSPDescMemberNameSH + <|> P.try pJSPDescWildSeg)++pJSPDescBracketed :: P.Parser JSPDescSegment+pJSPDescBracketed = do+ P.string ".."+ P.char '['+ sel <- pJSPSelector+ optionalSels <- P.many pCommaSepSelectors+ P.char ']'+ return $ JSPDescBracketed (sel:optionalSels)+ where+ pCommaSepSelectors :: P.Parser JSPSelector+ pCommaSepSelectors = P.char ',' *> P.spaces *> pJSPSelector++pJSPDescMemberNameSH :: P.Parser JSPDescSegment+pJSPDescMemberNameSH = do+ P.string ".."+ P.lookAhead (P.letter <|> P.oneOf "-" <|> pUnicodeChar)+ val <- T.pack <$> P.many1 (P.alphaNum <|> P.oneOf "-" <|> pUnicodeChar)+ return (JSPDescMemberNameSH val)++pJSPDescWildSeg :: P.Parser JSPDescSegment+pJSPDescWildSeg = JSPDescWildSeg <$> (P.string "..*" $> JSPWildcard)+ pSignedInt :: P.Parser Int pSignedInt = do sign <- P.optionMaybe $ P.char '-' num <- read <$> P.many1 P.digit return $ case sign of- Just _ -> -1 * num+ Just _ -> -num Nothing -> num++pUnicodeChar :: P.Parser Char+pUnicodeChar = P.satisfy inRange+ where+ inRange c = let code = ord c in+ (code >= 0x80 && code <= 0xD7FF) ||+ (code >= 0xE000 && code <= 0x10FFFF)
test/Main.hs view
@@ -8,7 +8,7 @@ import qualified ParserSpec import qualified QuerySpec -import Protolude+import Prelude allSpecs :: Spec allSpecs = do
test/ParserSpec.hs view
@@ -9,8 +9,11 @@ , JSPQuery (..) , JSPSegment (..) , JSPChildSegment (..)- , JSPSelector (..))-import Protolude+ , JSPDescSegment (..)+ , JSPSelector (..)+ , JSPWildcardT (..))+import Data.Either (isLeft)+import Prelude spec :: Spec spec = do@@ -19,25 +22,46 @@ P.parse pJSPQuery "" "$" `shouldBe` Right (JSPRoot []) it "parses query: $.store" $- P.parse pJSPQuery "" "$.store" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store")])+ P.parse pJSPQuery "" "$.store" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store")]) it "parses query: $.store.books" $- P.parse pJSPQuery "" "$.store.books" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books")])+ P.parse pJSPQuery "" "$.store.books" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books")]) it "parses query: $.store.books[0]" $- P.parse pJSPQuery "" "$.store.books[0]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books"), JSPChildSeg (JSPBracketed [JSPIndexSel 0])])+ P.parse pJSPQuery "" "$.store.books[0]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books"), JSPChildSeg (JSPChildBracketed [JSPIndexSel 0])]) it "parses query: $.store.books[0,2]" $- P.parse pJSPQuery "" "$.store.books[0,2]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books"), JSPChildSeg (JSPBracketed [JSPIndexSel 0, JSPIndexSel 2])])+ P.parse pJSPQuery "" "$.store.books[0,2]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books"), JSPChildSeg (JSPChildBracketed [JSPIndexSel 0, JSPIndexSel 2])]) it "parses query: $.store.books[1:3]" $- P.parse pJSPQuery "" "$.store.books[1:3]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books"), JSPChildSeg (JSPBracketed [JSPSliceSel (Just 1, Just 3, 1)])])+ P.parse pJSPQuery "" "$.store.books[1:3]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books"), JSPChildSeg (JSPChildBracketed [JSPSliceSel (Just 1, Just 3, 1)])]) it "parses query: $.store.books[1:4:2]" $- P.parse pJSPQuery "" "$.store.books[1:4:2]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books"), JSPChildSeg (JSPBracketed [JSPSliceSel (Just 1, Just 4, 2)])])+ P.parse pJSPQuery "" "$.store.books[1:4:2]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books"), JSPChildSeg (JSPChildBracketed [JSPSliceSel (Just 1, Just 4, 2)])]) it "parses query: $.store.books[-1:-4:-2]" $- P.parse pJSPQuery "" "$.store.books[-1:-4:-2]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books"), JSPChildSeg (JSPBracketed [JSPSliceSel (Just (-1), Just (-4), -2)])])+ P.parse pJSPQuery "" "$.store.books[-1:-4:-2]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books"), JSPChildSeg (JSPChildBracketed [JSPSliceSel (Just (-1), Just (-4), -2)])]) it "parses query: $.store.books[1:3, 0, 1]" $- P.parse pJSPQuery "" "$.store.books[1:3, 0, 1]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "books"), JSPChildSeg (JSPBracketed [JSPSliceSel (Just 1, Just 3, 1), JSPIndexSel 0, JSPIndexSel 1])])+ P.parse pJSPQuery "" "$.store.books[1:3, 0, 1]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "store"), JSPChildSeg (JSPChildMemberNameSH "books"), JSPChildSeg (JSPChildBracketed [JSPSliceSel (Just 1, Just 3, 1), JSPIndexSel 0, JSPIndexSel 1])])++ it "parses query: $.*" $+ P.parse pJSPQuery "" "$.*" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildWildSeg JSPWildcard)])++ it "parses query: $[*]" $+ P.parse pJSPQuery "" "$[*]" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildBracketed [JSPWildSel JSPWildcard])])++ it "parses query: $..*" $+ P.parse pJSPQuery "" "$..*" `shouldBe` Right (JSPRoot [JSPDescSeg (JSPDescWildSeg JSPWildcard)])++ it "parses query: $..[*]" $+ P.parse pJSPQuery "" "$..[*]" `shouldBe` Right (JSPRoot [JSPDescSeg (JSPDescBracketed [JSPWildSel JSPWildcard])])++ it "parses query $.hyphen-key" $+ P.parse pJSPQuery "" "$.hyphen-key" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "hyphen-key")])++ it "fails with $.1startsWithNum" $+ P.parse pJSPQuery "" "$.1startsWithNum" `shouldSatisfy` isLeft++ it "parses query $.©®±×÷Ωπ•€→∀∃∈≠≤≥✓λ" $+ P.parse pJSPQuery "" "$.©®±×÷Ωπ•€→∀∃∈≠≤≥✓λ" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPChildMemberNameSH "©®±×÷Ωπ•€→∀∃∈≠≤≥✓λ")])
test/QuerySpec.hs view
@@ -1,149 +1,268 @@-{-# LANGUAGE DeriveGeneric #-} module QuerySpec ( spec ) where import qualified Data.Aeson as JSON+import qualified Data.Vector as V -import Data.Aeson.JSONPath (runJSPQuery)+import Data.Aeson.JSONPath (runJSPQuery, jsonPath)+import Data.Aeson.QQ.Simple (aesonQQ) import Test.Hspec -import Protolude--data Book = Book- { title :: Text- , author :: Text- , category :: Text- , price :: Double- } deriving (Show, Generic)--instance JSON.ToJSON Book--data Store = Store- { books :: [Book]- } deriving (Show, Generic)--instance JSON.ToJSON Store--data Root = Root- { store :: Store- } deriving (Show, Generic)+import Prelude -instance JSON.ToJSON Root+toSingletonArray :: JSON.Value -> JSON.Value+toSingletonArray = JSON.Array . V.singleton -- taken from https://serdejsonpath.live/ rootDoc :: JSON.Value-rootDoc = JSON.toJSON $ Root- { store = Store- { books =- [ Book { title = "Guns, Germs, and Steel", author = "Jared Diamond", category = "reference", price = 24.99 }- , Book { title = "David Copperfield", author = "Charles Dickens", category = "fiction", price = 12.99 }- , Book { title = "Moby Dick", author = "Herman Melville", category = "fiction", price = 8.99 }- , Book { title = "Crime and Punishment", author = "Fyodor Dostoevsky", category = "fiction", price = 19.99 }- ]- }- }+rootDoc = [aesonQQ|{+ "store": {+ "books": [+ {+ "title": "Guns, Germs, and Steel",+ "author": "Jared Diamond",+ "category": "reference",+ "price": 24.99+ },+ {+ "title": "David Copperfield",+ "author": "Charles Dickens",+ "category": "fiction",+ "price": 12.99+ },+ {+ "title": "Moby Dick",+ "author": "Herman Melville",+ "category": "fiction",+ "price": 8.99+ },+ {+ "title": "Crime and Punishment",+ "author": "Fyodor Dostoevsky",+ "category": "fiction",+ "price": 19.99+ }+ ]+ }+ }|] storeDoc :: JSON.Value-storeDoc = JSON.toJSON $ Store- { books =- [ Book { title = "Guns, Germs, and Steel", author = "Jared Diamond", category = "reference", price = 24.99 }- , Book { title = "David Copperfield", author = "Charles Dickens", category = "fiction", price = 12.99 }- , Book { title = "Moby Dick", author = "Herman Melville", category = "fiction", price = 8.99 }- , Book { title = "Crime and Punishment", author = "Fyodor Dostoevsky", category = "fiction", price = 19.99 }- ]- }-+storeDoc = [aesonQQ|+ {+ "books": [+ {+ "title": "Guns, Germs, and Steel",+ "author": "Jared Diamond",+ "category": "reference",+ "price": 24.99+ },+ {+ "title": "David Copperfield",+ "author": "Charles Dickens",+ "category": "fiction",+ "price": 12.99+ },+ {+ "title": "Moby Dick",+ "author": "Herman Melville",+ "category": "fiction",+ "price": 8.99+ },+ {+ "title": "Crime and Punishment",+ "author": "Fyodor Dostoevsky",+ "category": "fiction",+ "price": 19.99+ }+ ]+ }|] booksDoc :: JSON.Value-booksDoc = JSON.toJSON $- [ Book { title = "Guns, Germs, and Steel", author = "Jared Diamond", category = "reference", price = 24.99 }- , Book { title = "David Copperfield", author = "Charles Dickens", category = "fiction", price = 12.99 }- , Book { title = "Moby Dick", author = "Herman Melville", category = "fiction", price = 8.99 }- , Book { title = "Crime and Punishment", author = "Fyodor Dostoevsky", category = "fiction", price = 19.99 }- ]+booksDoc = [aesonQQ| [+ {+ "title": "Guns, Germs, and Steel",+ "author": "Jared Diamond",+ "category": "reference",+ "price": 24.99+ },+ {+ "title": "David Copperfield",+ "author": "Charles Dickens",+ "category": "fiction",+ "price": 12.99+ },+ {+ "title": "Moby Dick",+ "author": "Herman Melville",+ "category": "fiction",+ "price": 8.99+ },+ {+ "title": "Crime and Punishment",+ "author": "Fyodor Dostoevsky",+ "category": "fiction",+ "price": 19.99+ }+ ]|] books0Doc :: JSON.Value-books0Doc = JSON.toJSON $ [- Book { title = "Guns, Germs, and Steel", author = "Jared Diamond", category = "reference", price = 24.99 }- ]+books0Doc = [aesonQQ|[+ {+ "title": "Guns, Germs, and Steel",+ "author": "Jared Diamond",+ "category": "reference",+ "price": 24.99+ }+ ]|] books0And2Doc :: JSON.Value-books0And2Doc = JSON.toJSON $ [- Book { title = "Guns, Germs, and Steel", author = "Jared Diamond", category = "reference", price = 24.99 }- , Book { title = "Moby Dick", author = "Herman Melville", category = "fiction", price = 8.99 }- ]+books0And2Doc = [aesonQQ|[+ {+ "title": "Guns, Germs, and Steel",+ "author": "Jared Diamond",+ "category": "reference",+ "price": 24.99+ },+ {+ "title": "Moby Dick",+ "author": "Herman Melville",+ "category": "fiction",+ "price": 8.99+ }+ ]|] books1To3Doc :: JSON.Value-books1To3Doc = JSON.toJSON $ [- Book { title = "David Copperfield", author = "Charles Dickens", category = "fiction", price = 12.99 }- , Book { title = "Moby Dick", author = "Herman Melville", category = "fiction", price = 8.99 }- ]+books1To3Doc = [aesonQQ|[+ {+ "title": "David Copperfield",+ "author": "Charles Dickens",+ "category": "fiction",+ "price": 12.99+ },+ {+ "title": "Moby Dick",+ "author": "Herman Melville",+ "category": "fiction",+ "price": 8.99+ }+ ]|] books1To3And0And1Doc :: JSON.Value-books1To3And0And1Doc = JSON.toJSON $ [- Book { title = "David Copperfield", author = "Charles Dickens", category = "fiction", price = 12.99 }- , Book { title = "Moby Dick", author = "Herman Melville", category = "fiction", price = 8.99 }- , Book { title = "Guns, Germs, and Steel", author = "Jared Diamond", category = "reference", price = 24.99 }- , Book { title = "David Copperfield", author = "Charles Dickens", category = "fiction", price = 12.99 }- ]+books1To3And0And1Doc = [aesonQQ|[+ {+ "title": "David Copperfield",+ "author": "Charles Dickens",+ "category": "fiction",+ "price": 12.99+ },+ {+ "title": "Moby Dick",+ "author": "Herman Melville",+ "category": "fiction",+ "price": 8.99+ },+ {+ "title": "Guns, Germs, and Steel",+ "author": "Jared Diamond",+ "category": "reference",+ "price": 24.99+ },+ {+ "title": "David Copperfield",+ "author": "Charles Dickens",+ "category": "fiction",+ "price": 12.99+ }+ ]|] -data AlphaList = AList [Text]- deriving (Show, Generic)-instance JSON.ToJSON AlphaList alphaArr :: JSON.Value-alphaArr = JSON.toJSON $ AList ["a","b","c","d","e","f","g"]+alphaArr = [aesonQQ| ["a","b","c","d","e","f","g"] |] fgArr :: JSON.Value-fgArr = JSON.toJSON $ AList ["f","g"]+fgArr = [aesonQQ| ["f","g"] |] bdArr :: JSON.Value-bdArr = JSON.toJSON $ AList ["b","d"]+bdArr = [aesonQQ| ["b","d"] |] fdArr :: JSON.Value-fdArr = JSON.toJSON $ AList ["f","d"]+fdArr = [aesonQQ| ["f","d"] |] gfedcbaArr :: JSON.Value-gfedcbaArr = JSON.toJSON $ AList ["g","f","e","d","c","b","a"]+gfedcbaArr = [aesonQQ| ["g","f","e","d","c","b","a"] |] +rfcExample1 :: JSON.Value+rfcExample1 = [aesonQQ| {+ "o": {"j": 1, "k": 2},+ "a": [5, 3, [{"j": 4}, {"k": 6}]]+ } |]++rfcExample1Desc :: JSON.Value+rfcExample1Desc = [aesonQQ| [+ [5, 3, [{"j": 4}, {"k": 6}]],+ {"j": 1, "k": 2},+ 5,+ 3,+ [{"j": 4}, {"k": 6}],+ {"j": 4},+ {"k": 6},+ 4,+ 6,+ 1,+ 2+ ]|]+ spec :: Spec spec = do describe "Run JSPQuery on JSON documents" $ do it "returns root document when query is $" $- runJSPQuery "$" rootDoc `shouldBe` Right rootDoc+ runJSPQuery [jsonPath|$|] rootDoc `shouldBe` rootDoc it "returns store object when query is $.store" $- runJSPQuery "$.store" rootDoc `shouldBe` Right storeDoc+ runJSPQuery [jsonPath|$.store|] rootDoc `shouldBe` storeDoc it "returns books array when query is $.store.books" $- runJSPQuery "$.store.books" rootDoc `shouldBe` Right booksDoc+ runJSPQuery [jsonPath|$.store.books|] rootDoc `shouldBe` booksDoc it "returns 0-index book item when query is $.store.books[0]" $- runJSPQuery "$.store.books[0]" rootDoc `shouldBe` Right books0Doc+ runJSPQuery [jsonPath|$.store.books[0]|] rootDoc `shouldBe` books0Doc it "returns 0-index book item when query is $.store.books[-4]" $- runJSPQuery "$.store.books[-4]" rootDoc `shouldBe` Right books0Doc+ runJSPQuery [jsonPath|$.store.books[-4]|] rootDoc `shouldBe` books0Doc it "returns 0,2-index item when query is $.store.books[0,2]" $- runJSPQuery "$.store.books[0,2]" rootDoc `shouldBe` Right books0And2Doc+ runJSPQuery [jsonPath|$.store.books[0,2]|] rootDoc `shouldBe` books0And2Doc it "returns 1To3-index when query is $.store.books[1:3]" $- runJSPQuery "$.store.books[1:3]" rootDoc `shouldBe` Right books1To3Doc+ runJSPQuery [jsonPath|$.store.books[1:3]|] rootDoc `shouldBe` books1To3Doc it "returns 1To3-index and 0,1-index when query is $.store.books[1:3,0,1]" $- runJSPQuery "$.store.books[1:3,0,1]" rootDoc `shouldBe` Right books1To3And0And1Doc+ runJSPQuery [jsonPath|$.store.books[1:3,0,1]|] rootDoc `shouldBe` books1To3And0And1Doc it "returns slice with query $[5:]" $- runJSPQuery "$[5:]" alphaArr `shouldBe` Right fgArr+ runJSPQuery [jsonPath|$[5:]|] alphaArr `shouldBe` fgArr it "returns slice with query $[1:5:2]" $- runJSPQuery "$[1:5:2]" alphaArr `shouldBe` Right bdArr+ runJSPQuery [jsonPath|$[1:5:2]|] alphaArr `shouldBe` bdArr it "returns slice with query $[5:1:-2]" $- runJSPQuery "$[5:1:-2]" alphaArr `shouldBe` Right fdArr+ runJSPQuery [jsonPath|$[5:1:-2]|] alphaArr `shouldBe` fdArr it "returns slice with query $[::-1]" $- runJSPQuery "$[::-1]" alphaArr `shouldBe` Right gfedcbaArr+ runJSPQuery [jsonPath|$[::-1]|] alphaArr `shouldBe` gfedcbaArr++ it "returns root with query $.*" $+ runJSPQuery [jsonPath|$.*|] rootDoc `shouldBe` rootDoc++ it "returns root with query $[*]" $+ runJSPQuery [jsonPath|$[*]|] rootDoc `shouldBe` (toSingletonArray rootDoc)++ it "returns root with query $..*" $+ runJSPQuery [jsonPath|$..*|] rfcExample1 `shouldBe` rfcExample1Desc++ it "returns root with query $..[*]" $ do+ pendingWith "fix with wildcard selection"+ runJSPQuery [jsonPath|$..[*]|] rfcExample1 `shouldBe` rfcExample1Desc