scrod-0.2026.2.21: source/library/Scrod/JsonPointer/Evaluate.hs
{-# LANGUAGE TemplateHaskellQuotes #-}
module Scrod.JsonPointer.Evaluate where
import qualified Data.Function as Function
import Data.List ((!?))
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Scrod.Extra.Read as Read
import qualified Scrod.Json.Array as Array
import qualified Scrod.Json.Object as Object
import qualified Scrod.Json.Pair as Pair
import qualified Scrod.Json.String as String
import qualified Scrod.Json.Value as Value
import qualified Scrod.JsonPointer.Pointer as Pointer
import qualified Scrod.JsonPointer.Token as Token
import qualified Scrod.Spec as Spec
-- | Evaluates a JSON Pointer against a JSON Value. Returns 'Nothing' if the
-- path does not exist or is invalid. Per RFC 6901:
--
-- - An empty pointer returns the document itself.
-- - For arrays, tokens must be valid non-negative integer indices.
-- - For objects, tokens are matched against member names.
evaluate :: Pointer.Pointer -> Value.Value -> Maybe Value.Value
evaluate = Function.fix $ \rec pointer value ->
case Pointer.unwrap pointer of
[] -> Just value
token : rest -> do
child <- step token value
rec (Pointer.MkPointer rest) child
-- | Takes a single step in a JSON value using a reference token.
step :: Token.Token -> Value.Value -> Maybe Value.Value
step token value = case value of
Value.Array array -> stepArray token array
Value.Object object -> stepObject token object
_ -> Nothing
-- | Steps into an array using a token as an index. Per RFC 6901, array indices
-- must be:
--
-- - Non-negative integers.
-- - Either "0" or not starting with "0" (no leading zeros).
stepArray :: Token.Token -> Array.Array a -> Maybe a
stepArray token array = do
index <- case Text.unpack $ Token.unwrap token of
'0' : _ : _ -> Nothing
string -> Read.readM string
Array.unwrap array !? index
-- | Steps into an object using a token as a key.
stepObject :: Token.Token -> Object.Object a -> Maybe a
stepObject token =
fmap Pair.value
. List.find ((== Token.unwrap token) . String.unwrap . Pair.name)
. Object.unwrap
spec :: (Applicative m, Monad n) => Spec.Spec m n -> n ()
spec s = do
Spec.named s 'evaluate $ do
Spec.it s "empty pointer returns the document" $ do
Spec.assertEq s (evaluate (Pointer.pointer []) Value.null) $ Just Value.null
Spec.it s "empty pointer returns the document (object)" $ do
let doc = Value.object [("foo", Value.integer 1)]
Spec.assertEq s (evaluate (Pointer.pointer []) doc) $ Just doc
Spec.it s "returns object member by name" $ do
let doc = Value.object [("foo", Value.string "bar")]
Spec.assertEq s (evaluate (Pointer.pointer ["foo"]) doc) . Just $ Value.string "bar"
Spec.it s "returns nested object member" $ do
let doc = Value.object [("foo", Value.object [("bar", Value.integer 42)])]
Spec.assertEq s (evaluate (Pointer.pointer ["foo", "bar"]) doc) . Just $ Value.integer 42
Spec.it s "returns array element by index" $ do
let doc = Value.array [Value.string "a", Value.string "b", Value.string "c"]
Spec.assertEq s (evaluate (Pointer.pointer ["1"]) doc) . Just $ Value.string "b"
Spec.it s "returns first array element with index 0" $ do
let doc = Value.array [Value.string "first", Value.string "second"]
Spec.assertEq s (evaluate (Pointer.pointer ["0"]) doc) . Just $ Value.string "first"
Spec.it s "returns Nothing for out-of-bounds array index" $ do
let doc = Value.array [Value.string "a"]
Spec.assertEq s (evaluate (Pointer.pointer ["5"]) doc) Nothing
Spec.it s "returns Nothing for non-integer array index" $ do
let doc = Value.array [Value.string "a"]
Spec.assertEq s (evaluate (Pointer.pointer ["foo"]) doc) Nothing
Spec.it s "returns Nothing for leading zero in array index" $ do
let doc = Value.array [Value.string "a", Value.string "b"]
Spec.assertEq s (evaluate (Pointer.pointer ["01"]) doc) Nothing
Spec.it s "returns Nothing for negative array index" $ do
let doc = Value.array [Value.string "a", Value.string "b"]
Spec.assertEq s (evaluate (Pointer.pointer ["-1"]) doc) Nothing
Spec.it s "returns Nothing for missing object key" $ do
let doc = Value.object [("foo", Value.string "bar")]
Spec.assertEq s (evaluate (Pointer.pointer ["baz"]) doc) Nothing
Spec.it s "returns Nothing when stepping into a scalar" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["foo"]) (Value.string "bar")) Nothing
Spec.describe s "rfc 6901 section 5" $ do
let rfc6901Doc =
Value.object
[ ("foo", Value.array [Value.string "bar", Value.string "baz"]),
("", Value.integer 0),
("a/b", Value.integer 1),
("c%d", Value.integer 2),
("e^f", Value.integer 3),
("g|h", Value.integer 4),
("i\\j", Value.integer 5),
("k\"l", Value.integer 6),
(" ", Value.integer 7),
("m~n", Value.integer 8)
]
Spec.it s "empty pointer returns the whole document" $ do
Spec.assertEq s (evaluate (Pointer.pointer []) rfc6901Doc) $ Just rfc6901Doc
Spec.it s "/foo returns [\"bar\", \"baz\"]" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["foo"]) rfc6901Doc) . Just $ Value.array [Value.string "bar", Value.string "baz"]
Spec.it s "/foo/0 returns \"bar\"" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["foo", "0"]) rfc6901Doc) . Just $ Value.string "bar"
Spec.it s "/ (empty token) returns 0" $ do
Spec.assertEq s (evaluate (Pointer.pointer [""]) rfc6901Doc) . Just $ Value.integer 0
Spec.it s "/a~1b (unescaped a/b) returns 1" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["a/b"]) rfc6901Doc) . Just $ Value.integer 1
Spec.it s "/c%d returns 2" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["c%d"]) rfc6901Doc) . Just $ Value.integer 2
Spec.it s "/e^f returns 3" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["e^f"]) rfc6901Doc) . Just $ Value.integer 3
Spec.it s "/g|h returns 4" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["g|h"]) rfc6901Doc) . Just $ Value.integer 4
Spec.it s "/i\\j returns 5" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["i\\j"]) rfc6901Doc) . Just $ Value.integer 5
Spec.it s "/k\"l returns 6" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["k\"l"]) rfc6901Doc) . Just $ Value.integer 6
Spec.it s "/ (space) returns 7" $ do
Spec.assertEq s (evaluate (Pointer.pointer [" "]) rfc6901Doc) . Just $ Value.integer 7
Spec.it s "/m~0n (unescaped m~n) returns 8" $ do
Spec.assertEq s (evaluate (Pointer.pointer ["m~n"]) rfc6901Doc) . Just $ Value.integer 8
Spec.it s "handles empty string key" $ do
let doc = Value.object [("", Value.string "empty key")]
Spec.assertEq s (evaluate (Pointer.pointer [""]) doc) . Just $ Value.string "empty key"
Spec.it s "handles deeply nested path" $ do
let doc = Value.object [("a", Value.object [("b", Value.object [("c", Value.string "deep")])])]
Spec.assertEq s (evaluate (Pointer.pointer ["a", "b", "c"]) doc) . Just $ Value.string "deep"
Spec.it s "handles mixed array and object traversal" $ do
let doc = Value.object [("items", Value.array [Value.object [("name", Value.string "first")], Value.object [("name", Value.string "second")]])]
Spec.assertEq s (evaluate (Pointer.pointer ["items", "1", "name"]) doc) . Just $ Value.string "second"