11{-# OPTIONS_GHC -Wno-unused-do-bind #-}
2- {-# LANGUAGE LambdaCase #-}
2+ {-# LANGUAGE NamedFieldPuns #-}
33module PostgREST.Config.JSPath
4- ( JSPath
5- , JSPathExp (.. )
6- , FilterExp (.. )
4+ ( JSPath (.. )
5+ , defaultRoleJSPathKey
76 , dumpJSPath
87 , pRoleClaimKey
9- , walkJSPath
8+ , evaluateJSPath
109 ) where
1110
1211import qualified Data.Aeson as JSON
13- import qualified Data.Aeson.Key as K
14- import qualified Data.Aeson.KeyMap as KM
12+ import qualified Data.Aeson.JSONPath as JSP
13+ import qualified Data.Aeson.JSONPath.Parser as JSP
14+ import qualified Data.Aeson.JSONPath.Types as JSP
1515import qualified Data.Text as T
1616import qualified Data.Vector as V
1717import qualified Text.ParserCombinators.Parsec as P
@@ -23,94 +23,68 @@ import Text.Read (read)
2323import Protolude
2424
2525
26- -- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")]
27- type JSPath = [JSPathExp ]
26+ -- | full jspath, e.g. "$.property[0].attr.detail[?(@ == "role1")] | [1:0]"
27+ data JSPath = JSPath
28+ { jsPath :: JSP. Query
29+ , jsPathSlice :: Maybe (Maybe Int , Maybe Int )
30+ }
2831
29- -- NOTE: We only accept one JSPFilter expr (at the end of input)
30- -- | jspath expression
31- data JSPathExp
32- = JSPKey Text -- .property or ."property-dash"
33- | JSPIdx Int -- [0]
34- | JSPSlice (Maybe Int ) (Maybe Int ) -- [0:5] or [0:] or [:5] or [:]
35- | JSPFilter FilterExp -- [?(@ == "match")]
32+ defaultRoleJSPathKey :: JSPath
33+ defaultRoleJSPathKey = JSPath (JSP. Query JSP. Root [JSP. QuerySegment JSP. Child $ JSP. Dotted " role" ]) Nothing
3634
37- data FilterExp
38- = EqualsCond Text
39- | NotEqualsCond Text
40- | StartsWithCond Text
41- | EndsWithCond Text
42- | ContainsCond Text
43-
44- dumpJSPath :: JSPathExp -> Text
45- -- TODO: this needs to be quoted properly for special chars
46- dumpJSPath (JSPKey k) = " ." <> show k
47- dumpJSPath (JSPIdx i) = " [" <> show i <> " ]"
48- dumpJSPath (JSPSlice s e) = " [" <> maybe " " show s <> " :" <> maybe " " show e <> " ]"
49- dumpJSPath (JSPFilter cond) = " [?(@" <> expr <> " )]"
50- where
51- expr =
52- case cond of
53- EqualsCond text -> " == " <> show text
54- NotEqualsCond text -> " != " <> show text
55- StartsWithCond text -> " ^== " <> show text
56- EndsWithCond text -> " ==^ " <> show text
57- ContainsCond text -> " *== " <> show text
35+ -- | Dump JSPath
36+ -- e.g. "$$.property[0].attr.detail[?(@ == "role1")] | [1:0]"
37+ dumpJSPath :: JSPath -> Text
38+ dumpJSPath JSPath {jsPath, jsPathSlice} =
39+ T. replace " \" " " \\\" " (jsPathDump <> jsPathSliceDump jsPathSlice)
40+ where
41+ jsPathDump = " $" <> JSP. dumpQuery jsPath
42+ jsPathSliceDump (Just (s, e)) = T. pack (" | " <> " [" <> maybe " " show s <> " :" <> maybe " " show e <> " ]" )
43+ jsPathSliceDump Nothing = mempty
5844
5945-- | Evaluate JSPath on a JSON
60- walkJSPath :: Maybe JSON. Value -> JSPath -> Maybe JSON. Value
61- walkJSPath x [] = x
62- walkJSPath (Just (JSON. Object o)) (JSPKey key: rest) = walkJSPath (KM. lookup (K. fromText key) o) rest
63- walkJSPath (Just (JSON. Array ar)) (JSPIdx idx: rest) = walkJSPath (ar V. !? idx) rest
64- walkJSPath (Just (JSON. String str)) (JSPSlice start end: rest) =
65- let
66- len = T. length str
46+ evaluateJSPath :: Maybe JSON. Value -> JSPath -> Maybe JSON. Value
47+ evaluateJSPath Nothing _ = Nothing
48+ evaluateJSPath (Just json) JSPath {jsPath, jsPathSlice} =
49+ case jsPathSlice of
50+ Nothing -> role
51+ Just indices -> getSlice indices role
52+ where
53+ jsPathResult = JSP. queryQQ jsPath json
54+ role = case jsPathResult V. !? 0 of
55+ -- If the result is a string, return it. Otherwise return Nothing
56+ Just (JSON. String r) -> Just $ JSON. String r
57+ _ -> Nothing
58+
59+ getSlice :: (Maybe Int , Maybe Int ) -> Maybe JSON. Value -> Maybe JSON. Value
60+ getSlice (start, end) (Just (JSON. String str)) = Just $ JSON. String slicedString
61+ where
62+ len = T. length str
6763
68- norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive
69- norm = fmap (\ i -> max 0 $ min len $ if i < 0 then len + i else i)
64+ norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive
65+ norm = fmap (\ i -> max 0 $ min len $ if i < 0 then len + i else i)
7066
71- s = fromMaybe 0 $ norm start -- normalized start index
72- e = fromMaybe len $ norm end -- normalized end index
73- slicedString = if s >= e then T. empty else T. take (e- s) $ T. drop s str
74- in
75- walkJSPath (Just $ JSON. String slicedString) rest
67+ s = fromMaybe 0 $ norm start -- normalized start index
68+ e = fromMaybe len $ norm end -- normalized end index
69+ slicedString = if s >= e then T. empty else T. take (e- s) $ T. drop s str
7670
77- walkJSPath (Just (JSON. Array ar)) (JSPFilter jspFilter: rest) = case jspFilter of
78- EqualsCond txt -> walkJSPath (findFirstMatch (==) txt ar) rest
79- NotEqualsCond txt -> walkJSPath (findFirstMatch (/=) txt ar) rest
80- StartsWithCond txt -> walkJSPath (findFirstMatch T. isPrefixOf txt ar) rest
81- EndsWithCond txt -> walkJSPath (findFirstMatch T. isSuffixOf txt ar) rest
82- ContainsCond txt -> walkJSPath (findFirstMatch T. isInfixOf txt ar) rest
83- where
84- findFirstMatch matchWith pattern = find (\ case
85- JSON. String txt -> pattern `matchWith` txt
86- _ -> False )
87- walkJSPath _ _ = Nothing
71+ getSlice _ _ = Nothing
8872
8973-- Used for the config value "role-claim-key"
9074pRoleClaimKey :: Text -> Either Text JSPath
9175pRoleClaimKey selStr =
9276 mapLeft show $ P. parse pJSPath (" failed to parse role-claim-key value (" <> toS selStr <> " )" ) (toS selStr)
9377
78+ -- | Parse RFC 9535 JSPath: $.roles[0]
9479pJSPath :: P. Parser JSPath
95- pJSPath = P. many1 pJSPathExp <* P. eof
96-
97- pJSPathExp :: P. Parser JSPathExp
98- pJSPathExp = P. try pJSPKey <|> P. try pJSPFilter <|> P. try pJSPIdx <|> pJSPSlice
99-
100- pJSPKey :: P. Parser JSPathExp
101- pJSPKey = do
102- P. char ' .'
103- val <- toS <$> P. many1 (P. alphaNum <|> P. oneOf " _$@" ) <|> pQuotedValue
104- return (JSPKey val) <?> " pJSPKey: JSPath attribute key"
105-
106- pJSPIdx :: P. Parser JSPathExp
107- pJSPIdx = do
108- P. char ' ['
109- num <- read <$> P. many1 P. digit
110- P. char ' ]'
111- return (JSPIdx num) <?> " pJSPIdx: JSPath array index"
112-
113- pJSPSlice :: P. Parser JSPathExp
80+ pJSPath = do
81+ jsPath' <- JSP. pRootQuery
82+ jsPathSlice' <- P. optionMaybe (P. try (P. spaces *> P. char ' |' *> P. spaces *> pJSPSlice))
83+ P. eof
84+ return (JSPath jsPath' jsPathSlice') <?> " pJSPath: JSPath root"
85+
86+ -- | Parse slice operator: [1:]
87+ pJSPSlice :: P. Parser (Maybe Int , Maybe Int )
11488pJSPSlice = do
11589 P. char ' ['
11690 startSign <- P. optionMaybe $ P. char ' -'
@@ -121,30 +95,4 @@ pJSPSlice = do
12195 P. char ' ]'
12296 let start' = if isJust startSign then ((- 1 ) * ) <$> startIndex else startIndex
12397 end' = if isJust endSign then ((- 1 ) * ) <$> endIndex else endIndex
124- return (JSPSlice start' end') <?> " pJSPSlice: JSPath string slice"
125-
126- pJSPFilter :: P. Parser JSPathExp
127- pJSPFilter = do
128- P. try $ P. string " [?("
129- condition <- pFilterConditionParser
130- P. char ' )'
131- P. char ' ]'
132- return (JSPFilter condition) <?> " pJSPFilter: JSPath filter exp"
133-
134- pFilterConditionParser :: P. Parser FilterExp
135- pFilterConditionParser = do
136- P. char ' @'
137- P. spaces
138- filt <- matchOperator
139- P. spaces
140- filt <$> pQuotedValue
141- where
142- matchOperator =
143- P. try (P. string " ==^" $> EndsWithCond )
144- <|> P. try (P. string " ==" $> EqualsCond )
145- <|> P. try (P. string " !=" $> NotEqualsCond )
146- <|> P. try (P. string " ^==" $> StartsWithCond )
147- <|> P. try (P. string " *==" $> ContainsCond )
148-
149- pQuotedValue :: P. Parser Text
150- pQuotedValue = toS <$> (P. char ' "' *> P. many (P. noneOf " \" " ) <* P. char ' "' )
98+ return (start', end') <?> " pJSPSlice: JSPath string slice"
0 commit comments