aeson-jsonpath (empty) → 0.1.0.0
raw patch · 8 files changed
+512/−0 lines, 8 filesdep +aesondep +aeson-jsonpathdep +base
Dependencies added: aeson, aeson-jsonpath, base, hspec, parsec, protolude, vector
Files
- CHANGELOG.md +3/−0
- LICENSE +21/−0
- aeson-jsonpath.cabal +55/−0
- src/Data/Aeson/JSONPath.hs +93/−0
- src/Data/Aeson/JSONPath/Parser.hs +122/−0
- test/Main.hs +26/−0
- test/ParserSpec.hs +43/−0
- test/QuerySpec.hs +149/−0
+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## 0.1.0.0++- Initial Release
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2024 Taimoor Zaeem++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ aeson-jsonpath.cabal view
@@ -0,0 +1,55 @@+name: aeson-jsonpath+version: 0.1.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.+license: MIT+license-file: LICENSE+author: Taimoor Zaeem+maintainer: Taimoor Zaeem <mtaimoorzaeem@gmail.com>+category: JSON, Text, Web+homepage: https://github.com/taimoorzaeem/aeson-jsonpath+bug-reports: https://github.com/taimoorzaeem/aeson-jsonpath/issues+build-type: Simple+extra-source-files: CHANGELOG.md+cabal-version: >= 1.10++tested-with:+ -- On Ubuntu Latest+ GHC == 9.4.5++source-repository head+ type: git+ location: https://github.com/taimoorzaeem/aeson-jsonpath++library+ default-language: Haskell2010+ default-extensions: OverloadedStrings+ NoImplicitPrelude+ 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+ , parsec >= 3.1.11 && < 3.2+ , protolude >= 0.3.1 && < 0.4+ , vector >= 0.11 && < 0.14++ ghc-options: -Wall++test-suite spec+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ default-extensions: OverloadedStrings+ NoImplicitPrelude+ hs-source-dirs: test+ main-is: Main.hs+ other-modules: ParserSpec+ QuerySpec+ build-depends: aeson >= 2.0.3 && < 2.3+ , aeson-jsonpath+ , hspec >= 2.3 && < 2.12+ , parsec >= 3.1.11 && < 3.2+ , protolude >= 0.3.1 && < 0.4++ ghc-options: -Wall
+ src/Data/Aeson/JSONPath.hs view
@@ -0,0 +1,93 @@+module Data.Aeson.JSONPath+ ( runJSPQuery )+ 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 Data.Aeson.JSONPath.Parser (JSPQuery (..)+ , JSPSegment (..)+ , JSPChildSegment (..)+ , JSPSelector (..)+ , JSPWildcardT (..)+ , pJSPQuery)+import Protolude+++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+++traverseJSPQuery :: JSPQuery -> JSON.Value -> JSON.Value+traverseJSPQuery (JSPRoot segs) doc = traverseJSPSegments segs doc+++traverseJSPSegments :: [JSPSegment] -> JSON.Value -> JSON.Value+traverseJSPSegments [] doc = doc+traverseJSPSegments (x:xs) doc = traverseJSPSegments xs (traverseJSPSegment x doc)+++traverseJSPSegment :: JSPSegment -> JSON.Value -> JSON.Value+traverseJSPSegment (JSPChildSeg jspChildSeg) doc = traverseJSPChildSeg jspChildSeg 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+++traverseJSPSelectors :: [JSPSelector] -> JSON.Value -> JSON.Value+traverseJSPSelectors sels doc = JSON.Array $ V.concat $ map traverse' sels+ where+ traverse' = flip traverseJSPSelector doc++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 _) _ = V.empty+traverseJSPSelector (JSPSliceSel sliceVals) (JSON.Array arr) = traverseJSPSliceSelector sliceVals arr+traverseJSPSelector (JSPSliceSel _) _ = V.empty+traverseJSPSelector (JSPWildSel JSPWildcard) doc = V.singleton doc+++traverseJSPSliceSelector :: (Maybe Int, Maybe Int, Int) -> JSON.Array -> V.Vector JSON.Value+traverseJSPSliceSelector (start, end, step) doc = getSlice start end step doc+ where+ -- TODO: Refactor this code to make it more pretty+ len = V.length doc+ normalize i = if i >= 0 then i else len + i++ sliceNormalized arr' (n_start, n_end) isStepNeg =+ let (lower, upper) = if isStepNeg then+ (min (max n_end (-1)) (len-1), min (max n_start (-1)) (len-1))+ else+ (min (max n_start 0) len, min (max n_end 0) len)+ in V.slice lower ((if isStepNeg then 1 else 0)+upper-lower) arr'++ getSlice _ _ 0 _ = V.empty+ getSlice (Just st) (Just en) step' arr =+ filterSlice (sliceNormalized arr (normalize st, normalize en) (step' < 0)) step'+ getSlice (Just st) Nothing step' arr =+ filterSlice (sliceNormalized arr (normalize st, len) (step' < 0)) step'+ getSlice Nothing (Just en) step' arr =+ filterSlice (sliceNormalized arr (0, normalize en) (step' < 0)) step'+ getSlice Nothing Nothing step' arr = filterSlice arr step'++ -- trying to avoid a step loop and keeping it "functional"+ 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+ else+ V.ifilter (\i _ -> i `mod` n == 0) slice++emptyJSArray :: JSON.Value+emptyJSArray = JSON.Array V.empty
+ src/Data/Aeson/JSONPath/Parser.hs view
@@ -0,0 +1,122 @@+{-# OPTIONS_GHC -Wno-unused-do-bind #-}+module Data.Aeson.JSONPath.Parser+ ( JSPQuery (..)+ , JSPSegment (..)+ , JSPChildSegment (..)+ , JSPSelector (..)+ , JSPWildcardT (..)+ , pJSPQuery+ )+ where++import qualified Text.ParserCombinators.Parsec as P++import Text.Read (read)++import Protolude++data JSPQuery+ = JSPRoot [JSPSegment]+ deriving (Eq, Show)++-- https://www.rfc-editor.org/rfc/rfc9535#name-segments-2+data JSPSegment+ = JSPChildSeg JSPChildSegment+ deriving (Eq, Show)++-- https://www.rfc-editor.org/rfc/rfc9535#name-child-segment+data JSPChildSegment+ = JSPBracketed [JSPSelector]+ | JSPMemberNameSH JSPNameSelector+ | JSPSegWildcard JSPWildcardT+ deriving (Eq, Show)++-- https://www.rfc-editor.org/rfc/rfc9535#name-selectors-2+data JSPSelector+ = JSPNameSel JSPNameSelector+ | JSPIndexSel JSPIndexSelector+ | JSPSliceSel JSPSliceSelector+ | JSPWildSel JSPWildcardT+ deriving (Eq, Show)++data JSPWildcardT = JSPWildcard+ deriving (Eq, Show)++type JSPNameSelector = Text++type JSPIndexSelector = Int++type JSPSliceSelector = (Maybe Int, Maybe Int, Int)++pJSPQuery :: P.Parser JSPQuery+pJSPQuery = do+ P.char '$'+ segs <- P.many pJSPSegment+ return $ JSPRoot segs++pJSPSelector :: P.Parser JSPSelector+pJSPSelector = P.try pJSPNameSel + <|> P.try pJSPIndexSel+ <|> P.try pJSPSliceSel + <|> P.try pJSPWildSel++pJSPNameSel :: P.Parser JSPSelector+pJSPNameSel = JSPNameSel <$> toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')++pJSPIndexSel :: P.Parser JSPSelector+pJSPIndexSel = do+ num <- pSignedInt+ P.notFollowedBy $ P.char ':'+ return $ JSPIndexSel num++pJSPSliceSel :: P.Parser JSPSelector+pJSPSliceSel = do+ start <- P.optionMaybe pSignedInt+ P.char ':'+ end <- P.optionMaybe pSignedInt+ step <- P.optionMaybe (P.char ':' *> P.optionMaybe pSignedInt)+ return $ JSPSliceSel (start, end, case step of+ Just (Just n) -> n+ _ -> 1)+++pJSPWildSel :: P.Parser JSPSelector+pJSPWildSel = JSPWildSel <$> (P.string ".*" $> JSPWildcard)++pJSPSegment :: P.Parser JSPSegment+pJSPSegment = pJSPChildSegment++pJSPChildSegment :: P.Parser JSPSegment+pJSPChildSegment = + JSPChildSeg <$> (P.try pJSPBracketed + <|> P.try pJSPMemberNameSH + <|> P.try pJSPWildSeg)++pJSPBracketed :: P.Parser JSPChildSegment+pJSPBracketed = do+ P.char '['+ sel <- pJSPSelector+ optionalSels <- P.many pCommaSepSelectors+ P.char ']'+ return $ JSPBracketed (sel:optionalSels)+ where+ pCommaSepSelectors :: P.Parser JSPSelector+ pCommaSepSelectors = P.char ',' *> P.spaces *> pJSPSelector++pJSPMemberNameSH :: P.Parser JSPChildSegment+pJSPMemberNameSH = do+ P.char '.'+ val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@")+ return (JSPMemberNameSH val)++pJSPWildSeg :: P.Parser JSPChildSegment+pJSPWildSeg = JSPSegWildcard <$> (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+ Nothing -> num
+ test/Main.hs view
@@ -0,0 +1,26 @@+module Main+ ( main )+ where++import qualified Test.Hspec as HS+import Test.Hspec.Runner++import qualified ParserSpec+import qualified QuerySpec++import Protolude++allSpecs :: Spec+allSpecs = do+ HS.describe "Run all tests" $ do+ ParserSpec.spec+ QuerySpec.spec++main :: IO ()+main = do+ summary <- hspecWithResult defaultConfig + { configColorMode = ColorAuto -- Colorized output+ } allSpecs+ + putStrLn $ "Total tests: " ++ show (summaryExamples summary)+ putStrLn $ "Failures: " ++ show (summaryFailures summary)
+ test/ParserSpec.hs view
@@ -0,0 +1,43 @@+module ParserSpec+ ( spec )+ where++import qualified Text.ParserCombinators.Parsec as P+import Test.Hspec++import Data.Aeson.JSONPath.Parser (pJSPQuery+ , JSPQuery (..)+ , JSPSegment (..)+ , JSPChildSegment (..)+ , JSPSelector (..))+import Protolude++spec :: Spec+spec = do+ describe "Parse JSPQuery" $ do+ it "parses query: $" $+ P.parse pJSPQuery "" "$" `shouldBe` Right (JSPRoot [])++ it "parses query: $.store" $+ P.parse pJSPQuery "" "$.store" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store")])++ it "parses query: $.store.books" $+ P.parse pJSPQuery "" "$.store.books" `shouldBe` Right (JSPRoot [JSPChildSeg (JSPMemberNameSH "store"), JSPChildSeg (JSPMemberNameSH "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])])++ 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])])++ 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)])])++ 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)])])++ 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)])])++ 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])])
+ test/QuerySpec.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE DeriveGeneric #-}+module QuerySpec+ ( spec )+ where++import qualified Data.Aeson as JSON++import Data.Aeson.JSONPath (runJSPQuery)+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)++instance JSON.ToJSON Root++-- 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 }+ ]+ }+ }+++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 }+ ]+ }+++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 }+ ]+++books0Doc :: JSON.Value+books0Doc = JSON.toJSON $ [+ Book { 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 }+ ]++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 }+ ]++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 }+ ]++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"]++fgArr :: JSON.Value+fgArr = JSON.toJSON $ AList ["f","g"]++bdArr :: JSON.Value+bdArr = JSON.toJSON $ AList ["b","d"]++fdArr :: JSON.Value+fdArr = JSON.toJSON $ AList ["f","d"]++gfedcbaArr :: JSON.Value+gfedcbaArr = JSON.toJSON $ AList ["g","f","e","d","c","b","a"]++spec :: Spec+spec = do+ describe "Run JSPQuery on JSON documents" $ do+ it "returns root document when query is $" $+ runJSPQuery "$" rootDoc `shouldBe` Right rootDoc++ it "returns store object when query is $.store" $+ runJSPQuery "$.store" rootDoc `shouldBe` Right storeDoc++ it "returns books array when query is $.store.books" $+ runJSPQuery "$.store.books" rootDoc `shouldBe` Right booksDoc++ it "returns 0-index book item when query is $.store.books[0]" $+ runJSPQuery "$.store.books[0]" rootDoc `shouldBe` Right books0Doc++ it "returns 0-index book item when query is $.store.books[-4]" $+ runJSPQuery "$.store.books[-4]" rootDoc `shouldBe` Right books0Doc++ it "returns 0,2-index item when query is $.store.books[0,2]" $+ runJSPQuery "$.store.books[0,2]" rootDoc `shouldBe` Right books0And2Doc++ it "returns 1To3-index when query is $.store.books[1:3]" $+ runJSPQuery "$.store.books[1:3]" rootDoc `shouldBe` Right 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++ it "returns slice with query $[5:]" $+ runJSPQuery "$[5:]" alphaArr `shouldBe` Right fgArr++ it "returns slice with query $[1:5:2]" $+ runJSPQuery "$[1:5:2]" alphaArr `shouldBe` Right bdArr++ it "returns slice with query $[5:1:-2]" $+ runJSPQuery "$[5:1:-2]" alphaArr `shouldBe` Right fdArr++ it "returns slice with query $[::-1]" $+ runJSPQuery "$[::-1]" alphaArr `shouldBe` Right gfedcbaArr