packages feed

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 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