···17 | Ok s -> s
18 | Error e -> failwith e
190000000000000020(* Test: parse pointer and print indices *)
21let test_parse pointer_str =
22 try
23- let p = Jsont_pointer.of_string pointer_str in
24- let indices = Jsont_pointer.indices p in
25 let index_strs = List.map (fun idx ->
26 match idx with
27- | `Mem s -> Printf.sprintf "Mem:%s" s
28- | `Nth n -> Printf.sprintf "Nth:%d" n
29- | `End -> "End"
30 ) indices in
31- Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
032 with Jsont.Error e ->
33 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
3435(* Test: roundtrip pointer string *)
36let test_roundtrip pointer_str =
37 try
38- let p = Jsont_pointer.of_string pointer_str in
39- let s = Jsont_pointer.to_string p in
40 if s = pointer_str then
41 Printf.printf "OK: %s\n" s
42 else
···48let test_eval json_path pointer_str =
49 try
50 let json = parse_json (read_file json_path) in
51- let p = Jsont_pointer.of_string pointer_str in
52 let result = Jsont_pointer.get p json in
53 Printf.printf "OK: %s\n" (json_to_string result)
54 with
···73(* Test: URI fragment roundtrip *)
74let test_uri_fragment pointer_str =
75 try
76- let p = Jsont_pointer.of_string pointer_str in
77- let frag = Jsont_pointer.to_uri_fragment p in
78- let p2 = Jsont_pointer.of_uri_fragment frag in
79- let s2 = Jsont_pointer.to_string p2 in
00080 if s2 = pointer_str then
81 Printf.printf "OK: %s -> %s\n" pointer_str frag
82 else
···88let test_add json_str pointer_str value_str =
89 try
90 let json = parse_json json_str in
91- let p = Jsont_pointer.of_string pointer_str in
92 let value = parse_json value_str in
93- let result = Jsont_pointer.add p json ~value in
00094 Printf.printf "%s\n" (json_to_string result)
95 with Jsont.Error e ->
96 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···99let test_remove json_str pointer_str =
100 try
101 let json = parse_json json_str in
102- let p = Jsont_pointer.of_string pointer_str in
103 let result = Jsont_pointer.remove p json in
104 Printf.printf "%s\n" (json_to_string result)
105 with Jsont.Error e ->
···109let test_replace json_str pointer_str value_str =
110 try
111 let json = parse_json json_str in
112- let p = Jsont_pointer.of_string pointer_str in
113 let value = parse_json value_str in
114 let result = Jsont_pointer.replace p json ~value in
115 Printf.printf "%s\n" (json_to_string result)
···120let test_move json_str from_str path_str =
121 try
122 let json = parse_json json_str in
123- let from = Jsont_pointer.of_string from_str in
124- let path = Jsont_pointer.of_string path_str in
125- let result = Jsont_pointer.move ~from ~path json in
00126 Printf.printf "%s\n" (json_to_string result)
127 with Jsont.Error e ->
128 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···131let test_copy json_str from_str path_str =
132 try
133 let json = parse_json json_str in
134- let from = Jsont_pointer.of_string from_str in
135- let path = Jsont_pointer.of_string path_str in
136- let result = Jsont_pointer.copy ~from ~path json in
00137 Printf.printf "%s\n" (json_to_string result)
138 with Jsont.Error e ->
139 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···142let test_test json_str pointer_str expected_str =
143 try
144 let json = parse_json json_str in
145- let p = Jsont_pointer.of_string pointer_str in
146 let expected = parse_json expected_str in
147 let result = Jsont_pointer.test p json ~expected in
148 Printf.printf "%b\n" result
···153let test_has json_str pointer_str =
154 try
155 let json = parse_json json_str in
156- let p = Jsont_pointer.of_string pointer_str in
157 let result = Jsont_pointer.find p json in
158 Printf.printf "%b\n" (Option.is_some result)
159 with Jsont.Error e ->
···17 | Ok s -> s
18 | Error e -> failwith e
1920+(* Helper to get indices from either nav or append pointer *)
21+let indices_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
22+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
23+ match result with
24+ | `Nav p -> Jsont_pointer.indices p
25+ | `Append p -> Jsont_pointer.indices p
26+27+(* Helper to convert to string from either nav or append pointer *)
28+let to_string_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
29+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
30+ match result with
31+ | `Nav p -> Jsont_pointer.to_string p
32+ | `Append p -> Jsont_pointer.to_string p
33+34(* Test: parse pointer and print indices *)
35let test_parse pointer_str =
36 try
37+ let result = Jsont_pointer.of_string pointer_str in
38+ let indices = indices_of_result result in
39 let index_strs = List.map (fun idx ->
40 match idx with
41+ | Jsont.Path.Mem (s, _) -> Printf.sprintf "Mem:%s" s
42+ | Jsont.Path.Nth (n, _) -> Printf.sprintf "Nth:%d" n
043 ) indices in
44+ let suffix = match result with `Nav _ -> "" | `Append _ -> ", /-" in
45+ Printf.printf "OK: [%s%s]\n" (String.concat ", " index_strs) suffix
46 with Jsont.Error e ->
47 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
4849(* Test: roundtrip pointer string *)
50let test_roundtrip pointer_str =
51 try
52+ let result = Jsont_pointer.of_string pointer_str in
53+ let s = to_string_of_result result in
54 if s = pointer_str then
55 Printf.printf "OK: %s\n" s
56 else
···62let test_eval json_path pointer_str =
63 try
64 let json = parse_json (read_file json_path) in
65+ let p = Jsont_pointer.of_string_nav pointer_str in
66 let result = Jsont_pointer.get p json in
67 Printf.printf "OK: %s\n" (json_to_string result)
68 with
···87(* Test: URI fragment roundtrip *)
88let test_uri_fragment pointer_str =
89 try
90+ let result = Jsont_pointer.of_string pointer_str in
91+ let frag = match result with
92+ | `Nav p -> Jsont_pointer.to_uri_fragment p
93+ | `Append p -> Jsont_pointer.to_uri_fragment p
94+ in
95+ let result2 = Jsont_pointer.of_uri_fragment frag in
96+ let s2 = to_string_of_result result2 in
97 if s2 = pointer_str then
98 Printf.printf "OK: %s -> %s\n" pointer_str frag
99 else
···105let test_add json_str pointer_str value_str =
106 try
107 let json = parse_json json_str in
0108 let value = parse_json value_str in
109+ let result = match Jsont_pointer.of_string pointer_str with
110+ | `Nav p -> Jsont_pointer.add p json ~value
111+ | `Append p -> Jsont_pointer.add p json ~value
112+ in
113 Printf.printf "%s\n" (json_to_string result)
114 with Jsont.Error e ->
115 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···118let test_remove json_str pointer_str =
119 try
120 let json = parse_json json_str in
121+ let p = Jsont_pointer.of_string_nav pointer_str in
122 let result = Jsont_pointer.remove p json in
123 Printf.printf "%s\n" (json_to_string result)
124 with Jsont.Error e ->
···128let test_replace json_str pointer_str value_str =
129 try
130 let json = parse_json json_str in
131+ let p = Jsont_pointer.of_string_nav pointer_str in
132 let value = parse_json value_str in
133 let result = Jsont_pointer.replace p json ~value in
134 Printf.printf "%s\n" (json_to_string result)
···139let test_move json_str from_str path_str =
140 try
141 let json = parse_json json_str in
142+ let from = Jsont_pointer.of_string_nav from_str in
143+ let result = match Jsont_pointer.of_string path_str with
144+ | `Nav path -> Jsont_pointer.move ~from ~path json
145+ | `Append path -> Jsont_pointer.move ~from ~path json
146+ in
147 Printf.printf "%s\n" (json_to_string result)
148 with Jsont.Error e ->
149 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···152let test_copy json_str from_str path_str =
153 try
154 let json = parse_json json_str in
155+ let from = Jsont_pointer.of_string_nav from_str in
156+ let result = match Jsont_pointer.of_string path_str with
157+ | `Nav path -> Jsont_pointer.copy ~from ~path json
158+ | `Append path -> Jsont_pointer.copy ~from ~path json
159+ in
160 Printf.printf "%s\n" (json_to_string result)
161 with Jsont.Error e ->
162 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···165let test_test json_str pointer_str expected_str =
166 try
167 let json = parse_json json_str in
168+ let p = Jsont_pointer.of_string_nav pointer_str in
169 let expected = parse_json expected_str in
170 let result = Jsont_pointer.test p json ~expected in
171 Printf.printf "%b\n" result
···176let test_has json_str pointer_str =
177 try
178 let json = parse_json json_str in
179+ let p = Jsont_pointer.of_string_nav pointer_str in
180 let result = Jsont_pointer.find p json in
181 Printf.printf "%b\n" (Option.is_some result)
182 with Jsont.Error e ->
+218-107
doc/tutorial.md
···4[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
5the `jsont-pointer` OCaml library through interactive examples.
600000000000000000007## Setup
89First, let's set up our environment with helper functions:
1011```ocaml
12# open Jsont_pointer;;
13-# #install_printer Jsont_pointer_top.printer;;
014# #install_printer Jsont_pointer_top.json_printer;;
15# #install_printer Jsont_pointer_top.error_printer;;
16# let parse_json s =
···47The JSON Pointer `/users/0/name` refers to the string `"Alice"`:
4849```ocaml
50-# let ptr = of_string "/users/0/name";;
51-val ptr : t = [`Mem "users"; `Nth 0; `Mem "name"]
52# get ptr users_json;;
53- : Jsont.json = "Alice"
54```
5556-In OCaml, this is represented by the `Jsont_pointer.t` type - a sequence
57-of navigation steps from the document root to a target value.
005859## Syntax: Reference Tokens
60···78Let's see this in action:
7980```ocaml
81-# of_string "";;
82-- : t = []
83```
8485The empty pointer has no reference tokens - it points to the root.
8687```ocaml
88-# of_string "/foo";;
89-- : t = [`Mem "foo"]
90```
9192The pointer `/foo` has one token: `foo`. Since it's not a number, it's
93interpreted as an object member name (`Mem`).
9495```ocaml
96-# of_string "/foo/0";;
97-- : t = [`Mem "foo"; `Nth 0]
98```
99100Here we have two tokens: `foo` (a member name) and `0` (interpreted as
101an array index `Nth`).
102103```ocaml
104-# of_string "/foo/bar/baz";;
105-- : t = [`Mem "foo"; `Mem "bar"; `Mem "baz"]
106```
107108Multiple tokens navigate deeper into nested structures.
109110### The Index Type
111112-Each reference token becomes an `Index.t` value in the library:
113114<!-- $MDX skip -->
115```ocaml
116-type t = [
117- | `Mem of string (* Object member access *)
118- | `Nth of int (* Array index access *)
119- | `End (* The special "-" marker for append operations *)
120-]
121```
122123-The `Mem` variant holds the **unescaped** member name - you work with the
124-actual key string (like `"a/b"`) and the library handles any escaping needed
0125for the JSON Pointer string representation.
126127### Invalid Syntax
···129What happens if a pointer doesn't start with `/`?
130131```ocaml
132-# of_string "foo";;
133Exception:
134Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo.
135```
···140141```ocaml
142# of_string_result "foo";;
143-- : (t, string) result =
144Error "Invalid JSON Pointer: must be empty or start with '/': foo"
145# of_string_result "/valid";;
146-- : (t, string) result = Ok [`Mem "valid"]
0147```
148149## Evaluation: Navigating JSON
···190### Object Member Access
191192```ocaml
193-# get (of_string "/foo") rfc_example ;;
194- : Jsont.json = ["bar","baz"]
195```
196···199### Array Index Access
200201```ocaml
202-# get (of_string "/foo/0") rfc_example ;;
203- : Jsont.json = "bar"
204-# get (of_string "/foo/1") rfc_example ;;
205- : Jsont.json = "baz"
206```
207···212JSON allows empty strings as object keys:
213214```ocaml
215-# get (of_string "/") rfc_example ;;
216- : Jsont.json = 0
217```
218···224The RFC example includes keys with `/` and `~` characters:
225226```ocaml
227-# get (of_string "/a~1b") rfc_example ;;
228- : Jsont.json = 1
229```
230···232[below](#escaping-special-characters).
233234```ocaml
235-# get (of_string "/m~0n") rfc_example ;;
236- : Jsont.json = 8
237```
238···242to worry about escaping. The `Mem` variant holds the literal key name:
243244```ocaml
245-# let slash_ptr = make [`Mem "a/b"];;
246-val slash_ptr : t = [`Mem "a/b"]
247# to_string slash_ptr;;
248- : string = "/a~1b"
249# get slash_ptr rfc_example ;;
···257Most characters don't need escaping in JSON Pointer strings:
258259```ocaml
260-# get (of_string "/c%d") rfc_example ;;
261- : Jsont.json = 2
262-# get (of_string "/e^f") rfc_example ;;
263- : Jsont.json = 3
264-# get (of_string "/g|h") rfc_example ;;
265- : Jsont.json = 4
266-# get (of_string "/ ") rfc_example ;;
267- : Jsont.json = 7
268```
269···274What happens when we try to access something that doesn't exist?
275276```ocaml
277-# get_result (of_string "/nonexistent") rfc_example;;
278- : (Jsont.json, Jsont.Error.t) result =
279Error JSON Pointer: member 'nonexistent' not found
280File "-":
281-# find (of_string "/nonexistent") rfc_example;;
282- : Jsont.json option = None
283```
284285Or an out-of-bounds array index:
286287```ocaml
288-# find (of_string "/foo/99") rfc_example;;
289- : Jsont.json option = None
290```
291292Or try to index into a non-container:
293294```ocaml
295-# find (of_string "/foo/0/invalid") rfc_example;;
296- : Jsont.json option = None
297```
298···300301<!-- $MDX skip -->
302```ocaml
303-val get : t -> Jsont.json -> Jsont.json
304-val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
305-val find : t -> Jsont.json -> Jsont.json option
306```
307308### Array Index Rules
···318> note that leading zeros are not allowed
319320```ocaml
321-# of_string "/foo/0";;
322-- : t = [`Mem "foo"; `Nth 0]
323```
324325Zero itself is fine.
326327```ocaml
328-# of_string "/foo/01";;
329-- : t = [`Mem "foo"; `Mem "01"]
330```
331332But `01` has a leading zero, so it's NOT treated as an array index - it
333becomes a member name instead. This protects against accidental octal
334interpretation.
335336-## The End-of-Array Marker: `-`
337338RFC 6901, Section 4 introduces a special token:
339340> exactly the single character "-", making the new referenced value the
341> (nonexistent) member after the last array element.
342343-This is primarily useful for JSON Patch operations (RFC 6902). Let's see
344-how it parses:
00034500000346```ocaml
347-# of_string "/foo/-";;
348-- : t = [`Mem "foo"; `End]
0349```
350351-The `-` is recognized as a special `End` index.
352-353-However, you cannot evaluate a pointer containing `-` because it refers
354-to a position that doesn't exist:
355356```ocaml
357-# find (of_string "/foo/-") rfc_example;;
358-- : Jsont.json option = None
00359```
360361-The RFC explains this:
00000362363> Note that the use of the "-" character to index an array will always
364> result in such an error condition because by definition it refers to
365> a nonexistent array element.
366367-But we'll see later that `-` is very useful for mutation operations!
000000000000000000000000000000000000000000368369## Mutation Operations
370···372(JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer`
373library provides these operations.
37400000000000000375### Add
376377The `add` operation inserts a value at a location:
···379```ocaml
380# let obj = parse_json {|{"foo":"bar"}|};;
381val obj : Jsont.json = {"foo":"bar"}
382-# add (of_string "/baz") obj ~value:(Jsont.Json.string "qux")
383 ;;
384- : Jsont.json = {"foo":"bar","baz":"qux"}
385```
···389```ocaml
390# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
391val arr_obj : Jsont.json = {"foo":["a","b"]}
392-# add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
393 ;;
394- : Jsont.json = {"foo":["a","X","b"]}
395```
396397-This is where the `-` marker shines - it appends to the end:
398399```ocaml
400-# add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c")
000000000401 ;;
402- : Jsont.json = {"foo":["a","b","c"]}
403```
404405### Remove
406407-The `remove` operation deletes a value:
0408409```ocaml
410# let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};;
411val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
412-# remove (of_string "/baz") two_fields ;;
413- : Jsont.json = {"foo":"bar"}
414```
415···418```ocaml
419# let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
420val three_elem : Jsont.json = {"foo":["a","b","c"]}
421-# remove (of_string "/foo/1") three_elem ;;
422- : Jsont.json = {"foo":["a","c"]}
423```
424···427The `replace` operation updates an existing value:
428429```ocaml
430-# replace (of_string "/foo") obj ~value:(Jsont.Json.string "baz")
431 ;;
432- : Jsont.json = {"foo":"baz"}
433```
434435-Unlike `add`, `replace` requires the target to already exist.
436Attempting to replace a nonexistent path raises an error.
437438### Move
439440-The `move` operation relocates a value:
00441442```ocaml
443# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
444val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
445-# move ~from:(of_string "/foo/bar") ~path:(of_string "/qux/thud") nested
446 ;;
447- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}
448```
449450### Copy
451452-The `copy` operation duplicates a value:
453454```ocaml
455# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
456val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
457-# copy ~from:(of_string "/foo/bar") ~path:(of_string "/foo/qux") to_copy
458 ;;
459- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}
460```
···464The `test` operation verifies a value (useful in JSON Patch):
465466```ocaml
467-# test (of_string "/foo") obj ~expected:(Jsont.Json.string "bar");;
468- : bool = true
469-# test (of_string "/foo") obj ~expected:(Jsont.Json.string "wrong");;
470- : bool = false
471```
472···493and escaping happens automatically during serialization:
494495```ocaml
496-# let p = make [`Mem "a/b"];;
497-val p : t = [`Mem "a/b"]
498# to_string p;;
499- : string = "/a~1b"
500-# of_string "/a~1b";;
501-- : t = [`Mem "a/b"]
502```
503504### Escaping in Action
···561This adds percent-encoding on top of the `~0`/`~1` escaping:
562563```ocaml
564-# to_uri_fragment (of_string "/foo");;
565- : string = "/foo"
566-# to_uri_fragment (of_string "/a~1b");;
567- : string = "/a~1b"
568-# to_uri_fragment (of_string "/c%d");;
569- : string = "/c%25d"
570-# to_uri_fragment (of_string "/ ");;
571- : string = "/%20"
572```
573···592Instead of parsing strings, you can build pointers from indices:
593594```ocaml
595-# let port_ptr = make [`Mem "database"; `Mem "port"];;
596-val port_ptr : t = [`Mem "database"; `Mem "port"]
597# to_string port_ptr;;
598- : string = "/database/port"
599```
600601-For array access, use `Nth`:
602603```ocaml
604-# let first_feature_ptr = make [`Mem "features"; `Nth 0];;
605-val first_feature_ptr : t = [`Mem "features"; `Nth 0]
606# to_string first_feature_ptr;;
607- : string = "/features/0"
608```
609610### Pointer Navigation
611612-You can build pointers incrementally using `append`:
613614```ocaml
615-# let db_ptr = of_string "/database";;
616-val db_ptr : t = [`Mem "database"]
617-# let creds_ptr = append db_ptr (`Mem "credentials");;
618-val creds_ptr : t = [`Mem "database"; `Mem "credentials"]
619-# let user_ptr = append creds_ptr (`Mem "username");;
620-val user_ptr : t = [`Mem "database"; `Mem "credentials"; `Mem "username"]
621# to_string user_ptr;;
622- : string = "/database/credentials/username"
623```
···625Or concatenate two pointers:
626627```ocaml
628-# let base = of_string "/api/v1";;
629-val base : t = [`Mem "api"; `Mem "v1"]
630-# let endpoint = of_string "/users/0";;
631-val endpoint : t = [`Mem "users"; `Nth 0]
632# to_string (concat base endpoint);;
633- : string = "/api/v1/users/0"
634```
···660```ocaml
661# let db_host =
662 Jsont.Json.decode
663- (path (of_string "/database/host") Jsont.string)
664 config_json
665 |> Result.get_ok;;
666val db_host : string = "localhost"
667# let db_port =
668 Jsont.Json.decode
669- (path (of_string "/database/port") Jsont.int)
670 config_json
671 |> Result.get_ok;;
672val db_port : int = 5432
···677```ocaml
678# let features =
679 Jsont.Json.decode
680- (path (of_string "/features") Jsont.(list string))
681 config_json
682 |> Result.get_ok;;
683val features : string list = ["auth"; "logging"; "metrics"]
···690```ocaml
691# let timeout =
692 Jsont.Json.decode
693- (path ~absent:30 (of_string "/database/timeout") Jsont.int)
694 config_json
695 |> Result.get_ok;;
696val timeout : int = 30
···710val org_json : Jsont.json =
711 {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}}
712# Jsont.Json.decode
713- (path (of_string "/organization/owner/name") Jsont.string)
714 org_json
715 |> Result.get_ok;;
716- : string = "Alice"
717# Jsont.Json.decode
718- (path (of_string "/organization/members/0/age") Jsont.int)
719 org_json
720 |> Result.get_ok;;
721- : int = 28
···727728```ocaml
729# let raw_port =
730- match get (of_string "/database/port") config_json with
731 | Jsont.Number (f, _) -> int_of_float f
732 | _ -> failwith "expected number";;
733val raw_port : int = 5432
···738```ocaml
739# let typed_port =
740 Jsont.Json.decode
741- (path (of_string "/database/port") Jsont.int)
742 config_json
743 |> Result.get_ok;;
744val typed_port : int = 5432
···7563. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index)
7574. **URI Encoding**: Pointers can be percent-encoded for use in URIs
7585. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates
0759760The `jsont-pointer` library implements all of this with type-safe OCaml
761interfaces, integration with the `jsont` codec system, and proper error
762handling for malformed pointers and missing values.
0000000
···4[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
5the `jsont-pointer` OCaml library through interactive examples.
67+## JSON Pointer vs JSON Path
8+9+Before diving in, it's worth understanding the difference between JSON
10+Pointer and JSON Path, as they serve different purposes:
11+12+**JSON Pointer** (RFC 6901) is an *indicator syntax* that specifies a
13+*single location* within JSON data. It always identifies at most one
14+value.
15+16+**JSON Path** is a *query syntax* that can *search* JSON data and return
17+*multiple* values matching specified criteria.
18+19+Use JSON Pointer when you need to address a single, specific location
20+(like JSON Schema's `$ref`). Use JSON Path when you might need multiple
21+results (like Kubernetes queries).
22+23+The `jsont-pointer` library implements JSON Pointer and integrates with
24+the `Jsont.Path` type for representing navigation indices.
25+26## Setup
2728First, let's set up our environment with helper functions:
2930```ocaml
31# open Jsont_pointer;;
32+# #install_printer Jsont_pointer_top.nav_printer;;
33+# #install_printer Jsont_pointer_top.append_printer;;
34# #install_printer Jsont_pointer_top.json_printer;;
35# #install_printer Jsont_pointer_top.error_printer;;
36# let parse_json s =
···67The JSON Pointer `/users/0/name` refers to the string `"Alice"`:
6869```ocaml
70+# let ptr = of_string_nav "/users/0/name";;
71+val ptr : nav t = [Mem "users"; Nth 0; Mem "name"]
72# get ptr users_json;;
73- : Jsont.json = "Alice"
74```
7576+In OCaml, this is represented by the `'a Jsont_pointer.t` type - a sequence
77+of navigation steps from the document root to a target value. The phantom
78+type parameter `'a` encodes whether this is a navigation pointer or an
79+append pointer (more on this later).
8081## Syntax: Reference Tokens
82···100Let's see this in action:
101102```ocaml
103+# of_string_nav "";;
104+- : nav t = []
105```
106107The empty pointer has no reference tokens - it points to the root.
108109```ocaml
110+# of_string_nav "/foo";;
111+- : nav t = [Mem "foo"]
112```
113114The pointer `/foo` has one token: `foo`. Since it's not a number, it's
115interpreted as an object member name (`Mem`).
116117```ocaml
118+# of_string_nav "/foo/0";;
119+- : nav t = [Mem "foo"; Nth 0]
120```
121122Here we have two tokens: `foo` (a member name) and `0` (interpreted as
123an array index `Nth`).
124125```ocaml
126+# of_string_nav "/foo/bar/baz";;
127+- : nav t = [Mem "foo"; Mem "bar"; Mem "baz"]
128```
129130Multiple tokens navigate deeper into nested structures.
131132### The Index Type
133134+Each reference token is represented using `Jsont.Path.index`:
135136<!-- $MDX skip -->
137```ocaml
138+type index = Jsont.Path.index
139+(* = Jsont.Path.Mem of string * Jsont.Meta.t
140+ | Jsont.Path.Nth of int * Jsont.Meta.t *)
00141```
142143+The `Mem` constructor is for object member access, and `Nth` is for array
144+index access. The member name is **unescaped** - you work with the actual
145+key string (like `"a/b"`) and the library handles any escaping needed
146for the JSON Pointer string representation.
147148### Invalid Syntax
···150What happens if a pointer doesn't start with `/`?
151152```ocaml
153+# of_string_nav "foo";;
154Exception:
155Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo.
156```
···161162```ocaml
163# of_string_result "foo";;
164+- : ([ `Append of append t | `Nav of nav t ], string) result =
165Error "Invalid JSON Pointer: must be empty or start with '/': foo"
166# of_string_result "/valid";;
167+- : ([ `Append of append t | `Nav of nav t ], string) result =
168+Ok (`Nav [Mem "valid"])
169```
170171## Evaluation: Navigating JSON
···212### Object Member Access
213214```ocaml
215+# get (of_string_nav "/foo") rfc_example ;;
216- : Jsont.json = ["bar","baz"]
217```
218···221### Array Index Access
222223```ocaml
224+# get (of_string_nav "/foo/0") rfc_example ;;
225- : Jsont.json = "bar"
226+# get (of_string_nav "/foo/1") rfc_example ;;
227- : Jsont.json = "baz"
228```
229···234JSON allows empty strings as object keys:
235236```ocaml
237+# get (of_string_nav "/") rfc_example ;;
238- : Jsont.json = 0
239```
240···246The RFC example includes keys with `/` and `~` characters:
247248```ocaml
249+# get (of_string_nav "/a~1b") rfc_example ;;
250- : Jsont.json = 1
251```
252···254[below](#escaping-special-characters).
255256```ocaml
257+# get (of_string_nav "/m~0n") rfc_example ;;
258- : Jsont.json = 8
259```
260···264to worry about escaping. The `Mem` variant holds the literal key name:
265266```ocaml
267+# let slash_ptr = make [mem "a/b"];;
268+val slash_ptr : nav t = [Mem "a/b"]
269# to_string slash_ptr;;
270- : string = "/a~1b"
271# get slash_ptr rfc_example ;;
···279Most characters don't need escaping in JSON Pointer strings:
280281```ocaml
282+# get (of_string_nav "/c%d") rfc_example ;;
283- : Jsont.json = 2
284+# get (of_string_nav "/e^f") rfc_example ;;
285- : Jsont.json = 3
286+# get (of_string_nav "/g|h") rfc_example ;;
287- : Jsont.json = 4
288+# get (of_string_nav "/ ") rfc_example ;;
289- : Jsont.json = 7
290```
291···296What happens when we try to access something that doesn't exist?
297298```ocaml
299+# get_result (of_string_nav "/nonexistent") rfc_example;;
300- : (Jsont.json, Jsont.Error.t) result =
301Error JSON Pointer: member 'nonexistent' not found
302File "-":
303+# find (of_string_nav "/nonexistent") rfc_example;;
304- : Jsont.json option = None
305```
306307Or an out-of-bounds array index:
308309```ocaml
310+# find (of_string_nav "/foo/99") rfc_example;;
311- : Jsont.json option = None
312```
313314Or try to index into a non-container:
315316```ocaml
317+# find (of_string_nav "/foo/0/invalid") rfc_example;;
318- : Jsont.json option = None
319```
320···322323<!-- $MDX skip -->
324```ocaml
325+val get : nav t -> Jsont.json -> Jsont.json
326+val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
327+val find : nav t -> Jsont.json -> Jsont.json option
328```
329330### Array Index Rules
···340> note that leading zeros are not allowed
341342```ocaml
343+# of_string_nav "/foo/0";;
344+- : nav t = [Mem "foo"; Nth 0]
345```
346347Zero itself is fine.
348349```ocaml
350+# of_string_nav "/foo/01";;
351+- : nav t = [Mem "foo"; Mem "01"]
352```
353354But `01` has a leading zero, so it's NOT treated as an array index - it
355becomes a member name instead. This protects against accidental octal
356interpretation.
357358+## The End-of-Array Marker: `-` and Type Safety
359360RFC 6901, Section 4 introduces a special token:
361362> exactly the single character "-", making the new referenced value the
363> (nonexistent) member after the last array element.
364365+This `-` marker is unique to JSON Pointer (JSON Path has no equivalent).
366+It's primarily useful for JSON Patch operations (RFC 6902) to append
367+elements to arrays.
368+369+### Navigation vs Append Pointers
370371+The `jsont-pointer` library uses **phantom types** to encode the difference
372+between pointers that can be used for navigation and pointers that target
373+the "append position":
374+375+<!-- $MDX skip -->
376```ocaml
377+type nav (* A pointer to an existing element *)
378+type append (* A pointer ending with "-" (append position) *)
379+type 'a t (* Pointer with phantom type parameter *)
380```
381382+When you parse a pointer, you get either a `nav t` or an `append t`:
000383384```ocaml
385+# of_string "/foo/0";;
386+- : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "foo"; Nth 0]
387+# of_string "/foo/-";;
388+- : [ `Append of append t | `Nav of nav t ] = `Append [Mem "foo"] /-
389```
390391+The `-` creates an `append` pointer. Note that in the internal
392+representation, the append position is tracked separately (shown as `/-`).
393+394+### Why Phantom Types?
395+396+The RFC explains that `-` refers to a *nonexistent* position:
397398> Note that the use of the "-" character to index an array will always
399> result in such an error condition because by definition it refers to
400> a nonexistent array element.
401402+So you **cannot use `get` or `find`** with an append pointer - it makes
403+no sense to retrieve a value from a position that doesn't exist! The
404+library enforces this at compile time:
405+406+```ocaml
407+# (* This won't compile: get requires nav t, not append t *)
408+ (* get (match of_string "/foo/-" with `Append p -> p | _ -> assert false) rfc_example;; *)
409+```
410+411+However, append pointers **are** valid for mutation operations like `add`:
412+413+```ocaml
414+# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
415+val arr_obj : Jsont.json = {"foo":["a","b"]}
416+# match of_string "/foo/-" with
417+ | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
418+ | `Nav _ -> assert false ;;
419+- : Jsont.json = {"foo":["a","b","c"]}
420+```
421+422+For convenience, use `of_string_nav` when you know a pointer shouldn't
423+contain `-`:
424+425+```ocaml
426+# of_string_nav "/foo/0";;
427+- : nav t = [Mem "foo"; Nth 0]
428+# of_string_nav "/foo/-";;
429+Exception:
430+Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.
431+```
432+433+### Creating Append Pointers Programmatically
434+435+You can convert a navigation pointer to an append pointer using `at_end`:
436+437+```ocaml
438+# let nav_ptr = of_string_nav "/foo";;
439+val nav_ptr : nav t = [Mem "foo"]
440+# let app_ptr = at_end nav_ptr;;
441+val app_ptr : append t = [Mem "foo"] /-
442+# to_string app_ptr;;
443+- : string = "/foo/-"
444+```
445446## Mutation Operations
447···449(JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer`
450library provides these operations.
451452+### Which Pointer Type for Which Operation?
453+454+The phantom type system enforces correct usage:
455+456+| Operation | Accepts | Because |
457+|-----------|---------|---------|
458+| `get`, `find` | `nav t` only | Can't retrieve from non-existent position |
459+| `remove` | `nav t` only | Can't remove what doesn't exist |
460+| `replace` | `nav t` only | Can't replace what doesn't exist |
461+| `test` | `nav t` only | Can't test non-existent position |
462+| `add` | `_ t` (both) | Can add at existing position OR append |
463+| `set` | `_ t` (both) | Can set existing position OR append |
464+| `move`, `copy` | `from:nav t`, `path:_ t` | Source must exist, dest can be append |
465+466### Add
467468The `add` operation inserts a value at a location:
···470```ocaml
471# let obj = parse_json {|{"foo":"bar"}|};;
472val obj : Jsont.json = {"foo":"bar"}
473+# add (of_string_nav "/baz") obj ~value:(Jsont.Json.string "qux")
474 ;;
475- : Jsont.json = {"foo":"bar","baz":"qux"}
476```
···480```ocaml
481# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
482val arr_obj : Jsont.json = {"foo":["a","b"]}
483+# add (of_string_nav "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
484 ;;
485- : Jsont.json = {"foo":["a","X","b"]}
486```
487488+This is where the `-` marker and append pointers shine - they append to the end:
489490```ocaml
491+# match of_string "/foo/-" with
492+ | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
493+ | `Nav _ -> assert false ;;
494+- : Jsont.json = {"foo":["a","b","c"]}
495+```
496+497+Or more conveniently using `at_end`:
498+499+```ocaml
500+# add (at_end (of_string_nav "/foo")) arr_obj ~value:(Jsont.Json.string "c")
501 ;;
502- : Jsont.json = {"foo":["a","b","c"]}
503```
504505### Remove
506507+The `remove` operation deletes a value. It only accepts `nav t` because
508+you can only remove something that exists:
509510```ocaml
511# let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};;
512val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
513+# remove (of_string_nav "/baz") two_fields ;;
514- : Jsont.json = {"foo":"bar"}
515```
516···519```ocaml
520# let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
521val three_elem : Jsont.json = {"foo":["a","b","c"]}
522+# remove (of_string_nav "/foo/1") three_elem ;;
523- : Jsont.json = {"foo":["a","c"]}
524```
525···528The `replace` operation updates an existing value:
529530```ocaml
531+# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
532 ;;
533- : Jsont.json = {"foo":"baz"}
534```
535536+Unlike `add`, `replace` requires the target to already exist (hence `nav t`).
537Attempting to replace a nonexistent path raises an error.
538539### Move
540541+The `move` operation relocates a value. The source (`from`) must be a `nav t`
542+(you can only move something that exists), but the destination (`path`) can
543+be either:
544545```ocaml
546# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
547val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
548+# move ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/qux/thud") nested
549 ;;
550- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}
551```
552553### Copy
554555+The `copy` operation duplicates a value (same typing as `move`):
556557```ocaml
558# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
559val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
560+# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/foo/qux") to_copy
561 ;;
562- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}
563```
···567The `test` operation verifies a value (useful in JSON Patch):
568569```ocaml
570+# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
571- : bool = true
572+# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
573- : bool = false
574```
575···596and escaping happens automatically during serialization:
597598```ocaml
599+# let p = make [mem "a/b"];;
600+val p : nav t = [Mem "a/b"]
601# to_string p;;
602- : string = "/a~1b"
603+# of_string_nav "/a~1b";;
604+- : nav t = [Mem "a/b"]
605```
606607### Escaping in Action
···664This adds percent-encoding on top of the `~0`/`~1` escaping:
665666```ocaml
667+# to_uri_fragment (of_string_nav "/foo");;
668- : string = "/foo"
669+# to_uri_fragment (of_string_nav "/a~1b");;
670- : string = "/a~1b"
671+# to_uri_fragment (of_string_nav "/c%d");;
672- : string = "/c%25d"
673+# to_uri_fragment (of_string_nav "/ ");;
674- : string = "/%20"
675```
676···695Instead of parsing strings, you can build pointers from indices:
696697```ocaml
698+# let port_ptr = make [mem "database"; mem "port"];;
699+val port_ptr : nav t = [Mem "database"; Mem "port"]
700# to_string port_ptr;;
701- : string = "/database/port"
702```
703704+For array access, use the `nth` helper:
705706```ocaml
707+# let first_feature_ptr = make [mem "features"; nth 0];;
708+val first_feature_ptr : nav t = [Mem "features"; Nth 0]
709# to_string first_feature_ptr;;
710- : string = "/features/0"
711```
712713### Pointer Navigation
714715+You can build pointers incrementally using the `/` operator (or `append_index`):
716717```ocaml
718+# let db_ptr = of_string_nav "/database";;
719+val db_ptr : nav t = [Mem "database"]
720+# let creds_ptr = db_ptr / mem "credentials";;
721+val creds_ptr : nav t = [Mem "database"; Mem "credentials"]
722+# let user_ptr = creds_ptr / mem "username";;
723+val user_ptr : nav t = [Mem "database"; Mem "credentials"; Mem "username"]
724# to_string user_ptr;;
725- : string = "/database/credentials/username"
726```
···728Or concatenate two pointers:
729730```ocaml
731+# let base = of_string_nav "/api/v1";;
732+val base : nav t = [Mem "api"; Mem "v1"]
733+# let endpoint = of_string_nav "/users/0";;
734+val endpoint : nav t = [Mem "users"; Nth 0]
735# to_string (concat base endpoint);;
736- : string = "/api/v1/users/0"
737```
···763```ocaml
764# let db_host =
765 Jsont.Json.decode
766+ (path (of_string_nav "/database/host") Jsont.string)
767 config_json
768 |> Result.get_ok;;
769val db_host : string = "localhost"
770# let db_port =
771 Jsont.Json.decode
772+ (path (of_string_nav "/database/port") Jsont.int)
773 config_json
774 |> Result.get_ok;;
775val db_port : int = 5432
···780```ocaml
781# let features =
782 Jsont.Json.decode
783+ (path (of_string_nav "/features") Jsont.(list string))
784 config_json
785 |> Result.get_ok;;
786val features : string list = ["auth"; "logging"; "metrics"]
···793```ocaml
794# let timeout =
795 Jsont.Json.decode
796+ (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int)
797 config_json
798 |> Result.get_ok;;
799val timeout : int = 30
···813val org_json : Jsont.json =
814 {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}}
815# Jsont.Json.decode
816+ (path (of_string_nav "/organization/owner/name") Jsont.string)
817 org_json
818 |> Result.get_ok;;
819- : string = "Alice"
820# Jsont.Json.decode
821+ (path (of_string_nav "/organization/members/0/age") Jsont.int)
822 org_json
823 |> Result.get_ok;;
824- : int = 28
···830831```ocaml
832# let raw_port =
833+ match get (of_string_nav "/database/port") config_json with
834 | Jsont.Number (f, _) -> int_of_float f
835 | _ -> failwith "expected number";;
836val raw_port : int = 5432
···841```ocaml
842# let typed_port =
843 Jsont.Json.decode
844+ (path (of_string_nav "/database/port") Jsont.int)
845 config_json
846 |> Result.get_ok;;
847val typed_port : int = 5432
···8593. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index)
8604. **URI Encoding**: Pointers can be percent-encoded for use in URIs
8615. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates
862+6. **Type Safety**: Phantom types (`nav t` vs `append t`) prevent misuse of append pointers with retrieval operations
863864The `jsont-pointer` library implements all of this with type-safe OCaml
865interfaces, integration with the `jsont` codec system, and proper error
866handling for malformed pointers and missing values.
867+868+### Key Points on JSON Pointer vs JSON Path
869+870+- **JSON Pointer** addresses a *single* location (like a file path)
871+- **JSON Path** queries for *multiple* values (like a search)
872+- The `-` token is unique to JSON Pointer - it means "append position" for arrays
873+- The library uses phantom types to enforce that `-` (append) pointers cannot be used with `get`/`find`
+231-216
src/jsont_pointer.ml
···54 else None
55end
5657-(* Index type - represents how a token is interpreted in context *)
58-module Index = struct
59- type t = [ `Mem of string | `Nth of int | `End ]
60-61- let pp ppf = function
62- | `Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
63- | `Nth n -> Format.fprintf ppf "/%d" n
64- | `End -> Format.fprintf ppf "/-"
6566- let equal i1 i2 = match i1, i2 with
67- | `Mem s1, `Mem s2 -> String.equal s1 s2
68- | `Nth n1, `Nth n2 -> Int.equal n1 n2
69- | `End, `End -> true
70- | _ -> false
7172- let compare i1 i2 = match i1, i2 with
73- | `Mem s1, `Mem s2 -> String.compare s1 s2
74- | `Mem _, _ -> -1
75- | _, `Mem _ -> 1
76- | `Nth n1, `Nth n2 -> Int.compare n1 n2
77- | `Nth _, `End -> -1
78- | `End, `Nth _ -> 1
79- | `End, `End -> 0
8081- let of_path_index (idx : Jsont.Path.index) : t =
82- match idx with
83- | Jsont.Path.Mem (s, _meta) -> `Mem s
84- | Jsont.Path.Nth (n, _meta) -> `Nth n
8586- let to_path_index (idx : t) : Jsont.Path.index option =
87- match idx with
88- | `Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none))
89- | `Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none))
90- | `End -> None
91-end
9293-(* Internal representation: raw unescaped tokens.
94- Per RFC 6901, interpretation as member name vs array index
95- depends on the JSON value type at evaluation time. *)
96module Segment = struct
97- type t =
98- | Token of string (* Unescaped reference token *)
99- | End (* The "-" token for end-of-array *)
100-101- let of_escaped_string s =
102- if s = "-" then End
103- else Token (Token.unescape s)
104105- let to_escaped_string = function
106- | Token s -> Token.escape s
107- | End -> "-"
108109- (* Convert to Index for a given JSON value type *)
110- let to_index seg ~for_array =
111- match seg with
112- | End -> `End
113- | Token s ->
114- if for_array then
115- match Token.is_valid_array_index s with
116- | Some n -> `Nth n
117- | None -> `Mem s (* Invalid index becomes member for error msg *)
118- else
119- `Mem s
120121- (* Convert from Index *)
122 let of_index = function
123- | `End -> End
124- | `Mem s -> Token s
125- | `Nth n -> Token (string_of_int n)
0000126end
127128-(* Pointer type - list of segments *)
129-type t = Segment.t list
0130131-let root = []
0000132133-let is_root p = p = []
134135-(* Convert indices to segments *)
136-let make indices = List.map Segment.of_index indices
137138-(* Convert segments to indices, assuming array context for numeric tokens *)
139-let indices p = List.map (fun seg -> Segment.to_index seg ~for_array:true) p
140141-let append p idx = p @ [Segment.of_index idx]
0142143-let concat p1 p2 = p1 @ p2
144145-let parent p = match List.rev p with
0000000146 | [] -> None
147- | _ :: rest -> Some (List.rev rest)
148149-let last p = match List.rev p with
0150 | [] -> None
151- | seg :: _ -> Some (Segment.to_index seg ~for_array:true)
00152153(* Parsing *)
154155-let of_string s =
156- if s = "" then root
157 else if s.[0] <> '/' then
158 Jsont.Error.msgf Jsont.Meta.none
159 "Invalid JSON Pointer: must be empty or start with '/': %s" s
···162 let tokens = String.split_on_char '/' rest in
163 List.map Segment.of_escaped_string tokens
164000000000000000000000000165let of_string_result s =
166 try Ok (of_string s)
167 with Jsont.Error e -> Error (Jsont.Error.to_string e)
···195 in
196 loop 0
197198-let of_uri_fragment s =
199- of_string (percent_decode s)
0200201let of_uri_fragment_result s =
202 try Ok (of_uri_fragment s)
···204205(* Serialization *)
206207-let to_string p =
208- if p = [] then ""
209- else
210- let b = Buffer.create 64 in
211- List.iter (fun seg ->
212- Buffer.add_char b '/';
213- Buffer.add_string b (Segment.to_escaped_string seg)
214- ) p;
215- Buffer.contents b
000216217(* URI fragment percent-encoding *)
218let needs_percent_encoding c =
219- (* RFC 3986 fragment: unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" *)
220- (* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" *)
221- (* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *)
222 not (
223 (c >= 'A' && c <= 'Z') ||
224 (c >= 'a' && c <= 'z') ||
···247 ) s;
248 Buffer.contents b
249250-let to_uri_fragment p =
251- percent_encode (to_string p)
252253-let pp ppf p =
254- Format.pp_print_string ppf (to_string p)
255256-let pp_verbose ppf p =
257- let pp_index ppf = function
258- | `Mem s -> Format.fprintf ppf {|`Mem "%s"|} s
259- | `Nth n -> Format.fprintf ppf "`Nth %d" n
260- | `End -> Format.fprintf ppf "`End"
261 in
262- Format.fprintf ppf "[%a]"
263- (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_index)
264- (indices p)
0265266(* Comparison *)
267268-let segment_equal s1 s2 = match s1, s2 with
269- | Segment.Token t1, Segment.Token t2 -> String.equal t1 t2
270- | Segment.End, Segment.End -> true
271- | _ -> false
272273-let segment_compare s1 s2 = match s1, s2 with
274- | Segment.Token t1, Segment.Token t2 -> String.compare t1 t2
275- | Segment.Token _, Segment.End -> -1
276- | Segment.End, Segment.Token _ -> 1
277- | Segment.End, Segment.End -> 0
278-279-let equal p1 p2 =
280- List.equal segment_equal p1 p2
281-282-let compare p1 p2 =
283- List.compare segment_compare p1 p2
284285(* Path conversion *)
286287-let segment_of_path_index (idx : Jsont.Path.index) : Segment.t =
288- match idx with
289- | Jsont.Path.Mem (s, _meta) -> Segment.Token s
290- | Jsont.Path.Nth (n, _meta) -> Segment.Token (string_of_int n)
291-292-let of_path (p : Jsont.Path.t) : t =
293- List.rev_map segment_of_path_index (Jsont.Path.rev_indices p)
294-295-let to_path p =
296- let rec convert acc = function
297- | [] -> Some acc
298- | Segment.End :: _ -> None
299- | Segment.Token s :: rest ->
300- (* For path conversion, we need to decide if it's a member or index.
301- We use array context for numeric tokens since Jsont.Path distinguishes. *)
302- let acc' = match Token.is_valid_array_index s with
303- | Some n -> Jsont.Path.nth ~meta:Jsont.Meta.none n acc
304- | None -> Jsont.Path.mem ~meta:Jsont.Meta.none s acc
305- in
306- convert acc' rest
307- in
308- convert Jsont.Path.root p
309310-let to_path_exn p =
311- match to_path p with
312- | Some path -> path
313- | None ->
314- Jsont.Error.msgf Jsont.Meta.none
315- "Cannot convert JSON Pointer with '-' index to Jsont.Path"
316317(* Evaluation helpers *)
318···332 if n < 0 || n >= List.length arr then None
333 else Some (List.nth arr n)
334335-(* Evaluation *)
336337-let rec eval_get p json =
338- match p with
339 | [] -> json
340- | Segment.End :: _ ->
341- Jsont.Error.msgf (Jsont.Json.meta json)
342- "JSON Pointer: '-' (end marker) refers to nonexistent array element"
343- | Segment.Token token :: rest ->
344 (match json with
345 | Jsont.Object (members, _) ->
346- (* For objects, token is always a member name *)
347 (match get_member token members with
348 | Some (_, value) -> eval_get rest value
349 | None ->
350 Jsont.Error.msgf (Jsont.Json.meta json)
351 "JSON Pointer: member '%s' not found" token)
352 | Jsont.Array (elements, _) ->
353- (* For arrays, token must be a valid array index *)
354 (match Token.is_valid_array_index token with
355 | Some n ->
356 (match get_nth n elements with
···367 "JSON Pointer: cannot index into %s with '%s'"
368 (json_sort_string json) token)
369370-let get p json = eval_get p json
371372let get_result p json =
373 try Ok (get p json)
···431 Jsont.Error.msgf (Jsont.Json.meta json)
432 "JSON Pointer: cannot navigate through %s" (json_sort_string json)
433434-(* Mutation: set *)
435436-let rec eval_set p value json =
437- match p with
438- | [] -> value
439- | [Segment.End] ->
0440 (match json with
441 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta)
442 | _ ->
443 Jsont.Error.msgf (Jsont.Json.meta json)
444 "JSON Pointer: '-' can only be used on arrays, got %s"
445 (json_sort_string json))
446- | Segment.End :: _ ->
447- Jsont.Error.msgf (Jsont.Json.meta json)
448- "JSON Pointer: '-' (end marker) refers to nonexistent array element"
449- | [Segment.Token token] ->
450 navigate_to_child token json
451 ~on_object:(fun members meta ->
452 if Option.is_some (get_member token members) then
···463 ~on_other:(fun () ->
464 Jsont.Error.msgf (Jsont.Json.meta json)
465 "JSON Pointer: cannot set in %s" (json_sort_string json))
466- | Segment.Token token :: rest ->
0467 navigate_to_child token json
468 ~on_object:(fun members meta ->
469 match get_member token members with
470 | Some (_, child) ->
471- Jsont.Object (set_member token (eval_set rest value child) members, meta)
0472 | None -> error_member_not_found json token)
473 ~on_array:(fun elements meta n ->
474 match get_nth n elements with
475 | Some child ->
476- Jsont.Array (replace_at n (eval_set rest value child) elements, meta)
00000000000000477 | None -> error_index_out_of_bounds json n)
478 ~on_other:(fun () -> error_cannot_navigate json)
479480-let set p json ~value = eval_set p value json
0481482-(* Mutation: add (RFC 6902 semantics) *)
483484-let rec eval_add p value json =
485- match p with
486- | [] -> value
487- | [Segment.End] ->
0488 (match json with
489 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta)
490 | _ ->
491 Jsont.Error.msgf (Jsont.Json.meta json)
492 "JSON Pointer: '-' can only be used on arrays, got %s"
493 (json_sort_string json))
494- | Segment.End :: _ ->
495- Jsont.Error.msgf (Jsont.Json.meta json)
496- "JSON Pointer: '-' in non-final position"
497- | [Segment.Token token] ->
498 navigate_to_child token json
499 ~on_object:(fun members meta ->
500 Jsont.Object (set_member token value members, meta))
···509 ~on_other:(fun () ->
510 Jsont.Error.msgf (Jsont.Json.meta json)
511 "JSON Pointer: cannot add to %s" (json_sort_string json))
512- | Segment.Token token :: rest ->
0513 navigate_to_child token json
514 ~on_object:(fun members meta ->
515 match get_member token members with
516 | Some (_, child) ->
517- Jsont.Object (set_member token (eval_add rest value child) members, meta)
0518 | None -> error_member_not_found json token)
519 ~on_array:(fun elements meta n ->
520 match get_nth n elements with
521 | Some child ->
522- Jsont.Array (replace_at n (eval_add rest value child) elements, meta)
00000000000000523 | None -> error_index_out_of_bounds json n)
524 ~on_other:(fun () -> error_cannot_navigate json)
525526-let add p json ~value = eval_add p value json
0527528-(* Mutation: remove *)
529530-let rec eval_remove p json =
531- match p with
532 | [] ->
533 Jsont.Error.msgf Jsont.Meta.none "JSON Pointer: cannot remove root document"
534- | [Segment.End] ->
535- Jsont.Error.msgf (Jsont.Json.meta json)
536- "JSON Pointer: '-' refers to nonexistent element"
537- | Segment.End :: _ ->
538- Jsont.Error.msgf (Jsont.Json.meta json)
539- "JSON Pointer: '-' in non-final position"
540- | [Segment.Token token] ->
541 navigate_to_child token json
542 ~on_object:(fun members meta ->
543 if Option.is_some (get_member token members) then
···554 ~on_other:(fun () ->
555 Jsont.Error.msgf (Jsont.Json.meta json)
556 "JSON Pointer: cannot remove from %s" (json_sort_string json))
557- | Segment.Token token :: rest ->
558 navigate_to_child token json
559 ~on_object:(fun members meta ->
560 match get_member token members with
561 | Some (_, child) ->
562- Jsont.Object (set_member token (eval_remove rest child) members, meta)
563 | None -> error_member_not_found json token)
564 ~on_array:(fun elements meta n ->
565 match get_nth n elements with
566 | Some child ->
567- Jsont.Array (replace_at n (eval_remove rest child) elements, meta)
568 | None -> error_index_out_of_bounds json n)
569 ~on_other:(fun () -> error_cannot_navigate json)
570571-let remove p json = eval_remove p json
572573-(* Mutation: replace *)
574575-let replace p json ~value =
576- (* Replace requires the target to exist, unlike add *)
577 let _ = get p json in (* Will raise if not found *)
578- eval_set p value json
579580(* Mutation: move *)
581582-let rec is_prefix_of p1 p2 =
583- match p1, p2 with
584- | [], _ -> true
585- | _, [] -> false
586- | h1 :: t1, h2 :: t2 -> segment_equal h1 h2 && is_prefix_of t1 t2
587-588-let move ~from ~path json =
589 (* Check for cycle: path cannot be a proper prefix of from *)
590- if is_prefix_of path from && not (equal path from) then
00000000591 Jsont.Error.msgf Jsont.Meta.none
592 "JSON Pointer: move would create cycle (path is prefix of from)";
593 let value = get from json in
···596597(* Mutation: copy *)
598599-let copy ~from ~path json =
600 let value = get from json in
601 add path json ~value
602603(* Mutation: test *)
604605-let test p json ~expected =
606 Option.fold ~none:false ~some:(Jsont.Json.equal expected) (find p json)
607608(* Jsont codec *)
609610-let jsont : t Jsont.t =
611 let dec _meta s = of_string s in
612- let enc p = to_string p in
000613 Jsont.Base.string (Jsont.Base.map
614 ~kind:"JSON Pointer"
615 ~doc:"RFC 6901 JSON Pointer"
616 ~dec ~enc ())
617618-let jsont_uri_fragment : t Jsont.t =
00000000619 let dec _meta s = of_uri_fragment s in
620- let enc p = to_uri_fragment p in
000621 Jsont.Base.string (Jsont.Base.map
622 ~kind:"JSON Pointer (URI fragment)"
623 ~doc:"RFC 6901 JSON Pointer in URI fragment encoding"
···625626(* Query combinators *)
627628-let path ?absent p t =
629 let dec json =
630 match find p json with
631 | Some value ->
···642 Jsont.map Jsont.json ~dec ~enc:(fun _ ->
643 Jsont.Error.msgf Jsont.Meta.none "path: encode not supported")
644645-let set_path ?(allow_absent = false) t p v =
646 let encoded = match Jsont.Json.encode' t v with
647 | Ok json -> json
648 | Error e -> raise (Jsont.Error e)
···655 in
656 Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
657658-let update_path ?absent p t =
659 let dec json =
660 let value = match find p json with
661 | Some v -> v
···681 in
682 Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
683684-let delete_path ?(allow_absent = false) p =
685 let dec json =
686 if allow_absent then
687 match find p json with
···54 else None
55end
5657+(* Index type - directly reuses Jsont.Path.index *)
58+type index = Jsont.Path.index
0000005960+(* Convenience constructors *)
61+let mem ?(meta = Jsont.Meta.none) s : index = Jsont.Path.Mem (s, meta)
62+let nth ?(meta = Jsont.Meta.none) n : index = Jsont.Path.Nth (n, meta)
006364+let pp_index ppf = function
65+ | Jsont.Path.Mem (s, _) -> Format.fprintf ppf "/%s" (Token.escape s)
66+ | Jsont.Path.Nth (n, _) -> Format.fprintf ppf "/%d" n
000006768+let equal_index i1 i2 = match i1, i2 with
69+ | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.equal s1 s2
70+ | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.equal n1 n2
71+ | _ -> false
7273+let compare_index i1 i2 = match i1, i2 with
74+ | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.compare s1 s2
75+ | Jsont.Path.Mem _, Jsont.Path.Nth _ -> -1
76+ | Jsont.Path.Nth _, Jsont.Path.Mem _ -> 1
77+ | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.compare n1 n2
07879+(* Internal representation: raw unescaped tokens *)
0080module Segment = struct
81+ type t = string (* Unescaped reference token *)
0000008283+ let of_escaped_string s = Token.unescape s
008485+ let to_escaped_string s = Token.escape s
000000000086087 let of_index = function
88+ | Jsont.Path.Mem (s, _) -> s
89+ | Jsont.Path.Nth (n, _) -> string_of_int n
90+91+ let to_index s : index =
92+ match Token.is_valid_array_index s with
93+ | Some n -> nth n
94+ | None -> mem s
95end
9697+(* Phantom types *)
98+type nav
99+type append
100101+(* Pointer type with phantom type parameter *)
102+type _ t = {
103+ segments : Segment.t list;
104+ is_append : bool; (* true if ends with "-" *)
105+}
106107+let root = { segments = []; is_append = false }
108109+let is_root p = p.segments = [] && not p.is_append
0110111+let make indices =
112+ { segments = List.map Segment.of_index indices; is_append = false }
113114+let ( / ) p idx =
115+ { segments = p.segments @ [Segment.of_index idx]; is_append = false }
116117+let append_index = ( / )
118119+let at_end p =
120+ { segments = p.segments; is_append = true }
121+122+let concat p1 p2 =
123+ { segments = p1.segments @ p2.segments; is_append = false }
124+125+let parent p =
126+ match List.rev p.segments with
127 | [] -> None
128+ | _ :: rest -> Some { segments = List.rev rest; is_append = false }
129130+let last p =
131+ match List.rev p.segments with
132 | [] -> None
133+ | seg :: _ -> Some (Segment.to_index seg)
134+135+let indices (type a) (p : a t) = List.map Segment.to_index p.segments
136137(* Parsing *)
138139+let parse_segments s =
140+ if s = "" then []
141 else if s.[0] <> '/' then
142 Jsont.Error.msgf Jsont.Meta.none
143 "Invalid JSON Pointer: must be empty or start with '/': %s" s
···146 let tokens = String.split_on_char '/' rest in
147 List.map Segment.of_escaped_string tokens
148149+let of_string s : [ `Nav of nav t | `Append of append t ] =
150+ let segments = parse_segments s in
151+ (* Check if ends with "-" *)
152+ match List.rev segments with
153+ | "-" :: rest ->
154+ (* Validate that "-" only appears at the end *)
155+ if List.exists (( = ) "-") rest then
156+ Jsont.Error.msgf Jsont.Meta.none
157+ "Invalid JSON Pointer: '-' can only appear at the end";
158+ `Append { segments = List.rev rest; is_append = true }
159+ | _ ->
160+ (* Validate no "-" anywhere *)
161+ if List.exists (( = ) "-") segments then
162+ Jsont.Error.msgf Jsont.Meta.none
163+ "Invalid JSON Pointer: '-' can only appear at the end";
164+ `Nav { segments; is_append = false }
165+166+let of_string_nav s : nav t =
167+ match of_string s with
168+ | `Nav p -> p
169+ | `Append _ ->
170+ Jsont.Error.msgf Jsont.Meta.none
171+ "Invalid JSON Pointer: '-' not allowed in navigation pointer"
172+173let of_string_result s =
174 try Ok (of_string s)
175 with Jsont.Error e -> Error (Jsont.Error.to_string e)
···203 in
204 loop 0
205206+let of_uri_fragment s = of_string (percent_decode s)
207+208+let of_uri_fragment_nav s = of_string_nav (percent_decode s)
209210let of_uri_fragment_result s =
211 try Ok (of_uri_fragment s)
···213214(* Serialization *)
215216+let to_string (type a) (p : a t) =
217+ let base =
218+ if p.segments = [] then ""
219+ else
220+ let b = Buffer.create 64 in
221+ List.iter (fun seg ->
222+ Buffer.add_char b '/';
223+ Buffer.add_string b (Segment.to_escaped_string seg)
224+ ) p.segments;
225+ Buffer.contents b
226+ in
227+ if p.is_append then base ^ "/-" else base
228229(* URI fragment percent-encoding *)
230let needs_percent_encoding c =
000231 not (
232 (c >= 'A' && c <= 'Z') ||
233 (c >= 'a' && c <= 'z') ||
···256 ) s;
257 Buffer.contents b
258259+let to_uri_fragment p = percent_encode (to_string p)
0260261+let pp ppf p = Format.pp_print_string ppf (to_string p)
0262263+let pp_verbose (type a) ppf (p : a t) =
264+ let pp_idx ppf seg =
265+ match Token.is_valid_array_index seg with
266+ | Some n -> Format.fprintf ppf "Nth %d" n
267+ | None -> Format.fprintf ppf {|Mem "%s"|} seg
268 in
269+ Format.fprintf ppf "[%a]%s"
270+ (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_idx)
271+ p.segments
272+ (if p.is_append then " /-" else "")
273274(* Comparison *)
275276+let equal (type a b) (p1 : a t) (p2 : b t) =
277+ List.equal String.equal p1.segments p2.segments &&
278+ p1.is_append = p2.is_append
0279280+let compare (type a b) (p1 : a t) (p2 : b t) =
281+ match List.compare String.compare p1.segments p2.segments with
282+ | 0 -> Bool.compare p1.is_append p2.is_append
283+ | n -> n
0000000284285(* Path conversion *)
286287+let of_path (p : Jsont.Path.t) : nav t =
288+ let segments = List.rev_map Segment.of_index (Jsont.Path.rev_indices p) in
289+ { segments; is_append = false }
0000000000000000000290291+let to_path (p : nav t) : Jsont.Path.t =
292+ List.fold_left (fun acc seg ->
293+ match Token.is_valid_array_index seg with
294+ | Some n -> Jsont.Path.nth n acc
295+ | None -> Jsont.Path.mem seg acc
296+ ) Jsont.Path.root p.segments
297298(* Evaluation helpers *)
299···313 if n < 0 || n >= List.length arr then None
314 else Some (List.nth arr n)
315316+(* Evaluation - only for nav pointers *)
317318+let rec eval_get segments json =
319+ match segments with
320 | [] -> json
321+ | token :: rest ->
000322 (match json with
323 | Jsont.Object (members, _) ->
0324 (match get_member token members with
325 | Some (_, value) -> eval_get rest value
326 | None ->
327 Jsont.Error.msgf (Jsont.Json.meta json)
328 "JSON Pointer: member '%s' not found" token)
329 | Jsont.Array (elements, _) ->
0330 (match Token.is_valid_array_index token with
331 | Some n ->
332 (match get_nth n elements with
···343 "JSON Pointer: cannot index into %s with '%s'"
344 (json_sort_string json) token)
345346+let get (p : nav t) json = eval_get p.segments json
347348let get_result p json =
349 try Ok (get p json)
···407 Jsont.Error.msgf (Jsont.Json.meta json)
408 "JSON Pointer: cannot navigate through %s" (json_sort_string json)
409410+(* Mutation: set - works with any pointer type *)
411412+let rec eval_set_segments segments is_append value json =
413+ match segments, is_append with
414+ | [], false -> value
415+ | [], true ->
416+ (* Append to array *)
417 (match json with
418 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta)
419 | _ ->
420 Jsont.Error.msgf (Jsont.Json.meta json)
421 "JSON Pointer: '-' can only be used on arrays, got %s"
422 (json_sort_string json))
423+ | [token], false ->
000424 navigate_to_child token json
425 ~on_object:(fun members meta ->
426 if Option.is_some (get_member token members) then
···437 ~on_other:(fun () ->
438 Jsont.Error.msgf (Jsont.Json.meta json)
439 "JSON Pointer: cannot set in %s" (json_sort_string json))
440+ | [token], true ->
441+ (* Navigate to token, then append *)
442 navigate_to_child token json
443 ~on_object:(fun members meta ->
444 match get_member token members with
445 | Some (_, child) ->
446+ let child' = eval_set_segments [] true value child in
447+ Jsont.Object (set_member token child' members, meta)
448 | None -> error_member_not_found json token)
449 ~on_array:(fun elements meta n ->
450 match get_nth n elements with
451 | Some child ->
452+ let child' = eval_set_segments [] true value child in
453+ Jsont.Array (replace_at n child' elements, meta)
454+ | None -> error_index_out_of_bounds json n)
455+ ~on_other:(fun () -> error_cannot_navigate json)
456+ | token :: rest, _ ->
457+ navigate_to_child token json
458+ ~on_object:(fun members meta ->
459+ match get_member token members with
460+ | Some (_, child) ->
461+ Jsont.Object (set_member token (eval_set_segments rest is_append value child) members, meta)
462+ | None -> error_member_not_found json token)
463+ ~on_array:(fun elements meta n ->
464+ match get_nth n elements with
465+ | Some child ->
466+ Jsont.Array (replace_at n (eval_set_segments rest is_append value child) elements, meta)
467 | None -> error_index_out_of_bounds json n)
468 ~on_other:(fun () -> error_cannot_navigate json)
469470+let set (type a) (p : a t) json ~value =
471+ eval_set_segments p.segments p.is_append value json
472473+(* Mutation: add (RFC 6902 semantics) - works with any pointer type *)
474475+let rec eval_add_segments segments is_append value json =
476+ match segments, is_append with
477+ | [], false -> value
478+ | [], true ->
479+ (* Append to array *)
480 (match json with
481 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta)
482 | _ ->
483 Jsont.Error.msgf (Jsont.Json.meta json)
484 "JSON Pointer: '-' can only be used on arrays, got %s"
485 (json_sort_string json))
486+ | [token], false ->
000487 navigate_to_child token json
488 ~on_object:(fun members meta ->
489 Jsont.Object (set_member token value members, meta))
···498 ~on_other:(fun () ->
499 Jsont.Error.msgf (Jsont.Json.meta json)
500 "JSON Pointer: cannot add to %s" (json_sort_string json))
501+ | [token], true ->
502+ (* Navigate to token, then append *)
503 navigate_to_child token json
504 ~on_object:(fun members meta ->
505 match get_member token members with
506 | Some (_, child) ->
507+ let child' = eval_add_segments [] true value child in
508+ Jsont.Object (set_member token child' members, meta)
509 | None -> error_member_not_found json token)
510 ~on_array:(fun elements meta n ->
511 match get_nth n elements with
512 | Some child ->
513+ let child' = eval_add_segments [] true value child in
514+ Jsont.Array (replace_at n child' elements, meta)
515+ | None -> error_index_out_of_bounds json n)
516+ ~on_other:(fun () -> error_cannot_navigate json)
517+ | token :: rest, _ ->
518+ navigate_to_child token json
519+ ~on_object:(fun members meta ->
520+ match get_member token members with
521+ | Some (_, child) ->
522+ Jsont.Object (set_member token (eval_add_segments rest is_append value child) members, meta)
523+ | None -> error_member_not_found json token)
524+ ~on_array:(fun elements meta n ->
525+ match get_nth n elements with
526+ | Some child ->
527+ Jsont.Array (replace_at n (eval_add_segments rest is_append value child) elements, meta)
528 | None -> error_index_out_of_bounds json n)
529 ~on_other:(fun () -> error_cannot_navigate json)
530531+let add (type a) (p : a t) json ~value =
532+ eval_add_segments p.segments p.is_append value json
533534+(* Mutation: remove - only for nav pointers *)
535536+let rec eval_remove_segments segments json =
537+ match segments with
538 | [] ->
539 Jsont.Error.msgf Jsont.Meta.none "JSON Pointer: cannot remove root document"
540+ | [token] ->
000000541 navigate_to_child token json
542 ~on_object:(fun members meta ->
543 if Option.is_some (get_member token members) then
···554 ~on_other:(fun () ->
555 Jsont.Error.msgf (Jsont.Json.meta json)
556 "JSON Pointer: cannot remove from %s" (json_sort_string json))
557+ | token :: rest ->
558 navigate_to_child token json
559 ~on_object:(fun members meta ->
560 match get_member token members with
561 | Some (_, child) ->
562+ Jsont.Object (set_member token (eval_remove_segments rest child) members, meta)
563 | None -> error_member_not_found json token)
564 ~on_array:(fun elements meta n ->
565 match get_nth n elements with
566 | Some child ->
567+ Jsont.Array (replace_at n (eval_remove_segments rest child) elements, meta)
568 | None -> error_index_out_of_bounds json n)
569 ~on_other:(fun () -> error_cannot_navigate json)
570571+let remove (p : nav t) json = eval_remove_segments p.segments json
572573+(* Mutation: replace - only for nav pointers *)
574575+let replace (p : nav t) json ~value =
0576 let _ = get p json in (* Will raise if not found *)
577+ eval_set_segments p.segments false value json
578579(* Mutation: move *)
580581+let move ~(from : nav t) ~(path : _ t) json =
000000582 (* Check for cycle: path cannot be a proper prefix of from *)
583+ let from_segs = from.segments in
584+ let path_segs = path.segments in
585+ let rec is_prefix p1 p2 = match p1, p2 with
586+ | [], _ -> true
587+ | _, [] -> false
588+ | h1 :: t1, h2 :: t2 -> String.equal h1 h2 && is_prefix t1 t2
589+ in
590+ if is_prefix path_segs from_segs &&
591+ not (List.equal String.equal path_segs from_segs && path.is_append = false) then
592 Jsont.Error.msgf Jsont.Meta.none
593 "JSON Pointer: move would create cycle (path is prefix of from)";
594 let value = get from json in
···597598(* Mutation: copy *)
599600+let copy ~(from : nav t) ~(path : _ t) json =
601 let value = get from json in
602 add path json ~value
603604(* Mutation: test *)
605606+let test (p : nav t) json ~expected =
607 Option.fold ~none:false ~some:(Jsont.Json.equal expected) (find p json)
608609(* Jsont codec *)
610611+let jsont : [ `Nav of nav t | `Append of append t ] Jsont.t =
612 let dec _meta s = of_string s in
613+ let enc = function
614+ | `Nav p -> to_string p
615+ | `Append p -> to_string p
616+ in
617 Jsont.Base.string (Jsont.Base.map
618 ~kind:"JSON Pointer"
619 ~doc:"RFC 6901 JSON Pointer"
620 ~dec ~enc ())
621622+let jsont_nav : nav t Jsont.t =
623+ let dec _meta s = of_string_nav s in
624+ let enc p = to_string p in
625+ Jsont.Base.string (Jsont.Base.map
626+ ~kind:"JSON Pointer (nav)"
627+ ~doc:"RFC 6901 JSON Pointer (navigation only)"
628+ ~dec ~enc ())
629+630+let jsont_uri_fragment : [ `Nav of nav t | `Append of append t ] Jsont.t =
631 let dec _meta s = of_uri_fragment s in
632+ let enc = function
633+ | `Nav p -> to_uri_fragment p
634+ | `Append p -> to_uri_fragment p
635+ in
636 Jsont.Base.string (Jsont.Base.map
637 ~kind:"JSON Pointer (URI fragment)"
638 ~doc:"RFC 6901 JSON Pointer in URI fragment encoding"
···640641(* Query combinators *)
642643+let path ?absent (p : nav t) t =
644 let dec json =
645 match find p json with
646 | Some value ->
···657 Jsont.map Jsont.json ~dec ~enc:(fun _ ->
658 Jsont.Error.msgf Jsont.Meta.none "path: encode not supported")
659660+let set_path (type a) ?(allow_absent = false) t (p : a t) v =
661 let encoded = match Jsont.Json.encode' t v with
662 | Ok json -> json
663 | Error e -> raise (Jsont.Error e)
···670 in
671 Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
672673+let update_path ?absent (p : nav t) t =
674 let dec json =
675 let value = match find p json with
676 | Some v -> v
···696 in
697 Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
698699+let delete_path ?(allow_absent = false) (p : nav t) =
700 let dec json =
701 if allow_absent then
702 match find p json with
+167-116
src/jsont_pointer.mli
···9 JSON Pointer parsing, serialization, and evaluation compatible with
10 {!Jsont} codecs.
1112- A JSON Pointer is a string syntax for identifying a specific value within
13- a JSON document. For example, given the JSON document:
000000000000000000000014 {v
15 {
16 "foo": ["bar", "baz"],
···27 {- ["/foo/0"] - the string ["bar"]}
28 {- ["/"] - the integer [0] (empty string key)}
29 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])}
30- {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}}
03132 {1:tokens Reference Tokens}
33···6061(** {1 Indices}
6263- Indices represent individual navigation steps in a JSON Pointer.
64- For objects, this is a member name. For arrays, this is either
65- a numeric index or the special end-of-array marker [-]. *)
66-module Index : sig
6768- type t = [
69- | `Mem of string
70- (** [`Mem name] indexes into an object member with the given [name].
71- The name is unescaped (i.e., [/] and [~] appear literally). *)
72- | `Nth of int
73- (** [`Nth n] indexes into an array at position [n] (zero-based).
74- Must be non-negative and without leading zeros in string form
75- (except for [0] itself). *)
76- | `End
77- (** [`End] represents the [-] token, indicating the position after
78- the last element of an array. This is used for append operations
79- in {!Jsont_pointer.add} and similar mutation functions.
80- Evaluating a pointer containing [`End] with {!Jsont_pointer.get}
81- will raise an error since it refers to a nonexistent element. *)
82- ]
8384- val pp : Format.formatter -> t -> unit
85- (** [pp] formats an index in JSON Pointer string notation. *)
008687- val equal : t -> t -> bool
88- (** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *)
008990- val compare : t -> t -> int
91- (** [compare i1 i2] is a total order on indices. *)
9293- (** {2:jsont_conv Conversion with Jsont.Path} *)
09495- val of_path_index : Jsont.Path.index -> t
96- (** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *)
9798- val to_path_index : t -> Jsont.Path.index option
99- (** [to_path_index idx] converts to a {!Jsont.Path.index}.
100- Returns [None] for [`End] since it has no equivalent in
101- {!Jsont.Path}. *)
102-end
103104-(** {1 Pointers} *)
000105106-type t
107-(** The type for JSON Pointers. A pointer is a sequence of {!Index.t}
108- values representing a path from the root of a JSON document to
109- a specific value. *)
00000000000110111-val root : t
112(** [root] is the empty pointer that references the whole document.
113 In string form this is [""]. *)
114115-val is_root : t -> bool
116(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
117118-val make : Index.t list -> t
119-(** [make indices] creates a pointer from a list of indices.
120 The list is ordered from root to target (i.e., the first element
121 is the first step from the root). *)
122123-val indices : t -> Index.t list
124-(** [indices p] returns the indices of [p] from root to target. *)
000125126-val append : t -> Index.t -> t
127-(** [append p idx] appends [idx] to the end of pointer [p]. *)
00128129-val concat : t -> t -> t
130(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
131132-val parent : t -> t option
133(** [parent p] returns the parent pointer of [p], or [None] if [p]
134 is the {!root}. *)
135136-val last : t -> Index.t option
137(** [last p] returns the last index of [p], or [None] if [p] is
138 the {!root}. *)
13900000140(** {2:parsing Parsing} *)
141142-val of_string : string -> t
143(** [of_string s] parses a JSON Pointer from its string representation.
144000145 The string must be either empty (representing the root) or start
146 with [/]. Each segment between [/] characters is unescaped as a
147- reference token. Segments that are valid non-negative integers
148- without leading zeros become [`Nth] indices; the string [-]
149- becomes [`End]; all others become [`Mem].
150151 @raise Jsont.Error if [s] has invalid syntax:
152 - Non-empty string not starting with [/]
153 - Invalid escape sequence ([~] not followed by [0] or [1])
154- - Array index with leading zeros
155- - Array index that overflows [int] *)
0000156157-val of_string_result : string -> (t, string) result
158(** [of_string_result s] is like {!of_string} but returns a result
159 instead of raising. *)
160161-val of_uri_fragment : string -> t
162(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
163164 This is like {!of_string} but first percent-decodes the string
···167168 @raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
169170-val of_uri_fragment_result : string -> (t, string) result
000000171(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
172 a result instead of raising. *)
173174(** {2:serializing Serializing} *)
175176-val to_string : t -> string
177(** [to_string p] serializes [p] to its JSON Pointer string representation.
178179 Returns [""] for the root pointer, otherwise [/] followed by
180- escaped reference tokens joined by [/]. *)
0181182-val to_uri_fragment : t -> string
183(** [to_uri_fragment p] serializes [p] to URI fragment form.
184185 This is like {!to_string} but additionally percent-encodes
186 characters that are not allowed in URI fragments per RFC 3986.
187 The leading [#] is {b not} included in the result. *)
188189-val pp : Format.formatter -> t -> unit
190(** [pp] formats a pointer using {!to_string}. *)
191192-val pp_verbose : Format.formatter -> t -> unit
193(** [pp_verbose] formats a pointer showing its index structure.
194- For example, [/foo/0/-] is formatted as [[`Mem "foo"; `Nth 0; `End]].
0195 Useful for debugging and understanding pointer structure. *)
196197(** {2:comparison Comparison} *)
198199-val equal : t -> t -> bool
200-(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *)
0201202-val compare : t -> t -> int
203(** [compare p1 p2] is a total order on pointers, comparing indices
204- lexicographically. *)
0205206(** {2:jsont_path Conversion with Jsont.Path} *)
207208-val of_path : Jsont.Path.t -> t
209-(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *)
0210211-val to_path : t -> Jsont.Path.t option
212-(** [to_path p] converts to a {!Jsont.Path.t}.
213- Returns [None] if [p] contains an [`End] index. *)
214-215-val to_path_exn : t -> Jsont.Path.t
216-(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error}
217- if conversion fails. *)
218219(** {1 Evaluation}
220221 These functions evaluate a JSON Pointer against a {!Jsont.json} value
222- to retrieve the referenced value. *)
0223224-val get : t -> Jsont.json -> Jsont.json
225(** [get p json] retrieves the value at pointer [p] in [json].
226227 @raise Jsont.Error if:
228 - The pointer references a nonexistent object member
229 - The pointer references an out-of-bounds array index
230- - The pointer contains [`End] (since [-] always refers
231- to a nonexistent element)
232- - An index type doesn't match the JSON value (e.g., [`Nth]
233 on an object) *)
234235-val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
236(** [get_result p json] is like {!get} but returns a result. *)
237238-val find : t -> Jsont.json -> Jsont.json option
239(** [find p json] is like {!get} but returns [None] instead of
240 raising when the pointer doesn't resolve to a value. *)
241···247 operations.
248249 All mutation functions return a new JSON value with the modification
250- applied; they do not mutate the input. *)
0000251252-val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json
253(** [set p json ~value] replaces the value at pointer [p] with [value].
254255- For [`End] on arrays, appends [value] to the end of the array.
256257 @raise Jsont.Error if the pointer doesn't resolve to an existing
258- location (except for [`End] on arrays). *)
259260-val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
261(** [add p json ~value] adds [value] at the location specified by [p].
262263 The behavior depends on the target:
264 {ul
265 {- For objects: If the member exists, it is replaced. If it doesn't
266 exist, a new member is added.}
267- {- For arrays with [`Nth]: Inserts [value] {e before} the
268 specified index, shifting subsequent elements. The index must be
269 valid (0 to length inclusive).}
270- {- For arrays with [`End]: Appends [value] to the array.}}
271272 @raise Jsont.Error if:
273 - The parent of the target location doesn't exist
274- - An array index is out of bounds (except for [`End])
275 - The parent is not an object or array *)
276277-val remove : t -> Jsont.json -> Jsont.json
278(** [remove p json] removes the value at pointer [p].
279280 For objects, removes the member. For arrays, removes the element
···282283 @raise Jsont.Error if:
284 - [p] is the root (cannot remove the root)
285- - The pointer doesn't resolve to an existing value
286- - The pointer contains [`End] *)
287288-val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json
289(** [replace p json ~value] replaces the value at pointer [p] with [value].
290291 Unlike {!add}, this requires the target to exist.
292293- @raise Jsont.Error if:
294- - The pointer doesn't resolve to an existing value
295- - The pointer contains [`End] *)
296297-val move : from:t -> path:t -> Jsont.json -> Jsont.json
298(** [move ~from ~path json] moves the value from [from] to [path].
299300 This is equivalent to {!remove} at [from] followed by {!add}
···302303 @raise Jsont.Error if:
304 - [from] doesn't resolve to a value
305- - [path] is a proper prefix of [from] (would create a cycle)
306- - Either pointer contains [`End] *)
307308-val copy : from:t -> path:t -> Jsont.json -> Jsont.json
309(** [copy ~from ~path json] copies the value from [from] to [path].
310311 This is equivalent to {!get} at [from] followed by {!add}
312 at [path] with the retrieved value.
313314- @raise Jsont.Error if:
315- - [from] doesn't resolve to a value
316- - Either pointer contains [`End] *)
317318-val test : t -> Jsont.json -> expected:Jsont.json -> bool
319(** [test p json ~expected] tests if the value at [p] equals [expected].
320321 Returns [true] if the values are equal according to {!Jsont.Json.equal},
···329 These types and functions integrate JSON Pointers with the {!Jsont}
330 codec system. *)
331332-val jsont : t Jsont.t
333(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
334335 On decode, parses a JSON string as a JSON Pointer using {!of_string}.
336 On encode, serializes a pointer to a JSON string using {!to_string}. *)
337338-val jsont_uri_fragment : t Jsont.t
000000339(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
340341 On decode, parses using {!of_uri_fragment}.
···346 These combinators integrate with jsont's query system, allowing
347 JSON Pointers to be used with jsont codecs for typed access. *)
348349-val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t
350(** [path p t] decodes the value at pointer [p] using codec [t].
351352 If [absent] is provided and the pointer doesn't resolve, returns
···354355 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
356357-val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t
358(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
359360 If [allow_absent] is [true] (default [false]), creates missing
···362363 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
364365-val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t
366(** [update_path p t] recodes the value at pointer [p] with codec [t].
367368 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
369370-val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t
371(** [delete_path p] removes the value at pointer [p].
372373 If [allow_absent] is [true] (default [false]), does nothing if
···9 JSON Pointer parsing, serialization, and evaluation compatible with
10 {!Jsont} codecs.
1112+ {1 JSON Pointer vs JSON Path}
13+14+ JSON Pointer (RFC 6901) and {!Jsont.Path} serve similar purposes but
15+ have important differences:
16+17+ {ul
18+ {- {b JSON Pointer} is a {e string syntax} for addressing JSON values,
19+ designed for use in URIs and JSON documents (like JSON Patch).
20+ It uses [/] as separator and has escape sequences ([~0], [~1]).}
21+ {- {b Jsont.Path} is an {e OCaml data structure} for programmatic
22+ navigation, with no string representation defined.}}
23+24+ A key difference is the [-] token: JSON Pointer's [-] refers to the
25+ (nonexistent) element {e after} the last array element. This is used
26+ for append operations in JSON Patch but is meaningless for retrieval.
27+ {!Jsont.Path} has no equivalent concept.
28+29+ This library uses phantom types to enforce this distinction at compile
30+ time: pointers that may contain [-] ({!append} pointers) cannot be
31+ passed to retrieval functions like {!get}.
32+33+ {2 Example}
34+35+ Given the JSON document:
36 {v
37 {
38 "foo": ["bar", "baz"],
···49 {- ["/foo/0"] - the string ["bar"]}
50 {- ["/"] - the integer [0] (empty string key)}
51 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])}
52+ {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}
53+ {- ["/foo/-"] - nonexistent; only valid for mutations}}
5455 {1:tokens Reference Tokens}
56···8384(** {1 Indices}
8586+ Indices are the individual navigation steps in a JSON Pointer.
87+ This library reuses {!Jsont.Path.index} directly - the JSON Pointer
88+ specific [-] token is handled separately via phantom types on the
89+ pointer type itself. *)
9091+type index = Jsont.Path.index
92+(** The type for navigation indices. This is exactly {!Jsont.Path.index}:
93+ either [Jsont.Path.Mem (name, meta)] for object member access or
94+ [Jsont.Path.Nth (n, meta)] for array index access. *)
000000000009596+val mem : ?meta:Jsont.Meta.t -> string -> index
97+(** [mem ?meta s] is [Jsont.Path.Mem (s, meta)].
98+ Convenience constructor for object member access.
99+ [meta] defaults to {!Jsont.Meta.none}. *)
100101+val nth : ?meta:Jsont.Meta.t -> int -> index
102+(** [nth ?meta n] is [Jsont.Path.Nth (n, meta)].
103+ Convenience constructor for array index access.
104+ [meta] defaults to {!Jsont.Meta.none}. *)
105106+val pp_index : Format.formatter -> index -> unit
107+(** [pp_index] formats an index in JSON Pointer string notation. *)
108109+val equal_index : index -> index -> bool
110+(** [equal_index i1 i2] is [true] iff [i1] and [i2] are the same index. *)
111112+val compare_index : index -> index -> int
113+(** [compare_index i1 i2] is a total order on indices. *)
114115+(** {1 Pointers}
0000116117+ JSON Pointers use phantom types to distinguish between:
118+ {ul
119+ {- {!nav} pointers that reference existing elements (safe for all operations)}
120+ {- {!append} pointers that end with [-] (only valid for {!add} and {!set})}}
121122+ This ensures at compile time that you cannot accidentally try to
123+ retrieve a nonexistent "end of array" position. *)
124+125+type 'a t
126+(** The type for JSON Pointers. The phantom type ['a] indicates whether
127+ the pointer can be used for navigation ([nav]) or only for append
128+ operations ([append]). *)
129+130+type nav
131+(** Phantom type for pointers that reference existing elements.
132+ These can be used with all operations including {!get} and {!find}. *)
133+134+type append
135+(** Phantom type for pointers ending with [-] (the "after last element"
136+ position). These can only be used with {!add} and {!set}. *)
137138+val root : nav t
139(** [root] is the empty pointer that references the whole document.
140 In string form this is [""]. *)
141142+val is_root : _ t -> bool
143(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
144145+val make : index list -> nav t
146+(** [make indices] creates a navigation pointer from a list of indices.
147 The list is ordered from root to target (i.e., the first element
148 is the first step from the root). *)
149150+val ( / ) : nav t -> index -> nav t
151+(** [p / idx] appends [idx] to pointer [p]. Operator form of {!append_index}. *)
152+153+val append_index : nav t -> index -> nav t
154+(** [append_index p idx] appends [idx] to the end of pointer [p]. *)
155156+val at_end : nav t -> append t
157+(** [at_end p] creates an append pointer by adding [-] to [p].
158+ The resulting pointer refers to the position after the last element
159+ of the array at [p]. Only valid for use with {!add} and {!set}. *)
160161+val concat : nav t -> nav t -> nav t
162(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
163164+val parent : nav t -> nav t option
165(** [parent p] returns the parent pointer of [p], or [None] if [p]
166 is the {!root}. *)
167168+val last : nav t -> index option
169(** [last p] returns the last index of [p], or [None] if [p] is
170 the {!root}. *)
171172+val indices : _ t -> index list
173+(** [indices p] returns the indices of [p] from root to target.
174+ Note: for append pointers, this returns the indices of the path
175+ portion; the [-] (append position) is not represented as an index. *)
176+177(** {2:parsing Parsing} *)
178179+val of_string : string -> [ `Nav of nav t | `Append of append t ]
180(** [of_string s] parses a JSON Pointer from its string representation.
181182+ Returns [`Nav p] for pointers without [-], or [`Append p] for
183+ pointers ending with [-].
184+185 The string must be either empty (representing the root) or start
186 with [/]. Each segment between [/] characters is unescaped as a
187+ reference token.
00188189 @raise Jsont.Error if [s] has invalid syntax:
190 - Non-empty string not starting with [/]
191 - Invalid escape sequence ([~] not followed by [0] or [1])
192+ - [-] appears in non-final position *)
193+194+val of_string_nav : string -> nav t
195+(** [of_string_nav s] parses a JSON Pointer that must not contain [-].
196+197+ @raise Jsont.Error if [s] has invalid syntax or contains [-]. *)
198199+val of_string_result : string -> ([ `Nav of nav t | `Append of append t ], string) result
200(** [of_string_result s] is like {!of_string} but returns a result
201 instead of raising. *)
202203+val of_uri_fragment : string -> [ `Nav of nav t | `Append of append t ]
204(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
205206 This is like {!of_string} but first percent-decodes the string
···209210 @raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
211212+val of_uri_fragment_nav : string -> nav t
213+(** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires
214+ the pointer to not contain [-].
215+216+ @raise Jsont.Error if invalid or contains [-]. *)
217+218+val of_uri_fragment_result : string -> ([ `Nav of nav t | `Append of append t ], string) result
219(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
220 a result instead of raising. *)
221222(** {2:serializing Serializing} *)
223224+val to_string : _ t -> string
225(** [to_string p] serializes [p] to its JSON Pointer string representation.
226227 Returns [""] for the root pointer, otherwise [/] followed by
228+ escaped reference tokens joined by [/]. Append pointers include
229+ the trailing [/-]. *)
230231+val to_uri_fragment : _ t -> string
232(** [to_uri_fragment p] serializes [p] to URI fragment form.
233234 This is like {!to_string} but additionally percent-encodes
235 characters that are not allowed in URI fragments per RFC 3986.
236 The leading [#] is {b not} included in the result. *)
237238+val pp : Format.formatter -> _ t -> unit
239(** [pp] formats a pointer using {!to_string}. *)
240241+val pp_verbose : Format.formatter -> _ t -> unit
242(** [pp_verbose] formats a pointer showing its index structure.
243+ For example, [/foo/0] is formatted as [[Mem "foo"; Nth 0]].
244+ Append pointers show [/-] at the end.
245 Useful for debugging and understanding pointer structure. *)
246247(** {2:comparison Comparison} *)
248249+val equal : _ t -> _ t -> bool
250+(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices
251+ and the same append status. *)
252253+val compare : _ t -> _ t -> int
254(** [compare p1 p2] is a total order on pointers, comparing indices
255+ lexicographically. Append pointers sort after nav pointers with
256+ the same prefix. *)
257258(** {2:jsont_path Conversion with Jsont.Path} *)
259260+val of_path : Jsont.Path.t -> nav t
261+(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer.
262+ Always returns a {!nav} pointer since {!Jsont.Path} has no [-] concept. *)
263264+val to_path : nav t -> Jsont.Path.t
265+(** [to_path p] converts a navigation pointer to a {!Jsont.Path.t}. *)
00000266267(** {1 Evaluation}
268269 These functions evaluate a JSON Pointer against a {!Jsont.json} value
270+ to retrieve the referenced value. They only accept {!nav} pointers
271+ since {!append} pointers refer to nonexistent positions. *)
272273+val get : nav t -> Jsont.json -> Jsont.json
274(** [get p json] retrieves the value at pointer [p] in [json].
275276 @raise Jsont.Error if:
277 - The pointer references a nonexistent object member
278 - The pointer references an out-of-bounds array index
279+ - An index type doesn't match the JSON value (e.g., [Nth]
00280 on an object) *)
281282+val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
283(** [get_result p json] is like {!get} but returns a result. *)
284285+val find : nav t -> Jsont.json -> Jsont.json option
286(** [find p json] is like {!get} but returns [None] instead of
287 raising when the pointer doesn't resolve to a value. *)
288···294 operations.
295296 All mutation functions return a new JSON value with the modification
297+ applied; they do not mutate the input.
298+299+ Functions that support the [-] token ({!add}, {!set}) accept any
300+ pointer type ([_ t]). Functions that require an existing element
301+ ({!remove}, {!replace}) only accept {!nav} pointers. *)
302303+val set : _ t -> Jsont.json -> value:Jsont.json -> Jsont.json
304(** [set p json ~value] replaces the value at pointer [p] with [value].
305306+ For {!append} pointers, appends [value] to the end of the array.
307308 @raise Jsont.Error if the pointer doesn't resolve to an existing
309+ location (except for {!append} pointers on arrays). *)
310311+val add : _ t -> Jsont.json -> value:Jsont.json -> Jsont.json
312(** [add p json ~value] adds [value] at the location specified by [p].
313314 The behavior depends on the target:
315 {ul
316 {- For objects: If the member exists, it is replaced. If it doesn't
317 exist, a new member is added.}
318+ {- For arrays with [Nth]: Inserts [value] {e before} the
319 specified index, shifting subsequent elements. The index must be
320 valid (0 to length inclusive).}
321+ {- For {!append} pointers: Appends [value] to the array.}}
322323 @raise Jsont.Error if:
324 - The parent of the target location doesn't exist
325+ - An array index is out of bounds (except for {!append} pointers)
326 - The parent is not an object or array *)
327328+val remove : nav t -> Jsont.json -> Jsont.json
329(** [remove p json] removes the value at pointer [p].
330331 For objects, removes the member. For arrays, removes the element
···333334 @raise Jsont.Error if:
335 - [p] is the root (cannot remove the root)
336+ - The pointer doesn't resolve to an existing value *)
0337338+val replace : nav t -> Jsont.json -> value:Jsont.json -> Jsont.json
339(** [replace p json ~value] replaces the value at pointer [p] with [value].
340341 Unlike {!add}, this requires the target to exist.
342343+ @raise Jsont.Error if the pointer doesn't resolve to an existing value. *)
00344345+val move : from:nav t -> path:_ t -> Jsont.json -> Jsont.json
346(** [move ~from ~path json] moves the value from [from] to [path].
347348 This is equivalent to {!remove} at [from] followed by {!add}
···350351 @raise Jsont.Error if:
352 - [from] doesn't resolve to a value
353+ - [path] is a proper prefix of [from] (would create a cycle) *)
0354355+val copy : from:nav t -> path:_ t -> Jsont.json -> Jsont.json
356(** [copy ~from ~path json] copies the value from [from] to [path].
357358 This is equivalent to {!get} at [from] followed by {!add}
359 at [path] with the retrieved value.
360361+ @raise Jsont.Error if [from] doesn't resolve to a value. *)
00362363+val test : nav t -> Jsont.json -> expected:Jsont.json -> bool
364(** [test p json ~expected] tests if the value at [p] equals [expected].
365366 Returns [true] if the values are equal according to {!Jsont.Json.equal},
···374 These types and functions integrate JSON Pointers with the {!Jsont}
375 codec system. *)
376377+val jsont : [ `Nav of nav t | `Append of append t ] Jsont.t
378(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
379380 On decode, parses a JSON string as a JSON Pointer using {!of_string}.
381 On encode, serializes a pointer to a JSON string using {!to_string}. *)
382383+val jsont_nav : nav t Jsont.t
384+(** [jsont_nav] is a {!Jsont.t} codec for navigation JSON Pointers.
385+386+ On decode, parses using {!of_string_nav} (fails on [-]).
387+ On encode, serializes using {!to_string}. *)
388+389+val jsont_uri_fragment : [ `Nav of nav t | `Append of append t ] Jsont.t
390(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
391392 On decode, parses using {!of_uri_fragment}.
···397 These combinators integrate with jsont's query system, allowing
398 JSON Pointers to be used with jsont codecs for typed access. *)
399400+val path : ?absent:'a -> nav t -> 'a Jsont.t -> 'a Jsont.t
401(** [path p t] decodes the value at pointer [p] using codec [t].
402403 If [absent] is provided and the pointer doesn't resolve, returns
···405406 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
407408+val set_path : ?allow_absent:bool -> 'a Jsont.t -> _ t -> 'a -> Jsont.json Jsont.t
409(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
410411 If [allow_absent] is [true] (default [false]), creates missing
···413414 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
415416+val update_path : ?absent:'a -> nav t -> 'a Jsont.t -> Jsont.json Jsont.t
417(** [update_path p t] recodes the value at pointer [p] with codec [t].
418419 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
420421+val delete_path : ?allow_absent:bool -> nav t -> Jsont.json Jsont.t
422(** [delete_path p] removes the value at pointer [p].
423424 If [allow_absent] is [true] (default [false]), does nothing if
+4-4
test/comprehensive.t
···4 $ ./test_pointer.exe parse "/foo/bar"
5 OK: [Mem:foo, Mem:bar]
6 $ ./test_pointer.exe parse "/foo/-"
7- OK: [Mem:foo, End]
8 $ ./test_pointer.exe parse "/foo/1"
9 OK: [Mem:foo, Nth:1]
10 $ ./test_pointer.exe parse "/foo/~0"
···144 $ ./test_pointer.exe has 'null' ''
145 true
146147-Has with '-' (end marker - should be false for get, points to nonexistent):
148 $ ./test_pointer.exe has '["foo"]' '/-'
149- false
150 $ ./test_pointer.exe has '[]' '/-'
151- false
···4 $ ./test_pointer.exe parse "/foo/bar"
5 OK: [Mem:foo, Mem:bar]
6 $ ./test_pointer.exe parse "/foo/-"
7+ OK: [Mem:foo, /-]
8 $ ./test_pointer.exe parse "/foo/1"
9 OK: [Mem:foo, Nth:1]
10 $ ./test_pointer.exe parse "/foo/~0"
···144 $ ./test_pointer.exe has 'null' ''
145 true
146147+Has with '-' (end marker - now errors because has uses of_string_nav):
148 $ ./test_pointer.exe has '["foo"]' '/-'
149+ ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
150 $ ./test_pointer.exe has '[]' '/-'
151+ ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
+1-2
test/eval.t
···6566Error: end marker not allowed in get:
67 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-"
68- ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element
69- File "-":
7071Error: navigating through primitive (string):
72 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0"
···6566Error: end marker not allowed in get:
67 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-"
68+ ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
06970Error: navigating through primitive (string):
71 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0"
···17 | Ok s -> s
18 | Error e -> failwith e
190000000000000020(* Test: parse pointer and print indices *)
21let test_parse pointer_str =
22 try
23- let p = Jsont_pointer.of_string pointer_str in
24- let indices = Jsont_pointer.indices p in
25 let index_strs = List.map (fun idx ->
26 match idx with
27- | `Mem s -> Printf.sprintf "Mem:%s" s
28- | `Nth n -> Printf.sprintf "Nth:%d" n
29- | `End -> "End"
30 ) indices in
31- Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
032 with Jsont.Error e ->
33 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
3435(* Test: roundtrip pointer string *)
36let test_roundtrip pointer_str =
37 try
38- let p = Jsont_pointer.of_string pointer_str in
39- let s = Jsont_pointer.to_string p in
40 if s = pointer_str then
41 Printf.printf "OK: %s\n" s
42 else
···48let test_eval json_path pointer_str =
49 try
50 let json = parse_json (read_file json_path) in
51- let p = Jsont_pointer.of_string pointer_str in
52 let result = Jsont_pointer.get p json in
53 Printf.printf "OK: %s\n" (json_to_string result)
54 with
···73(* Test: URI fragment roundtrip *)
74let test_uri_fragment pointer_str =
75 try
76- let p = Jsont_pointer.of_string pointer_str in
77- let frag = Jsont_pointer.to_uri_fragment p in
78- let p2 = Jsont_pointer.of_uri_fragment frag in
79- let s2 = Jsont_pointer.to_string p2 in
00080 if s2 = pointer_str then
81 Printf.printf "OK: %s -> %s\n" pointer_str frag
82 else
···88let test_add json_str pointer_str value_str =
89 try
90 let json = parse_json json_str in
91- let p = Jsont_pointer.of_string pointer_str in
92 let value = parse_json value_str in
93- let result = Jsont_pointer.add p json ~value in
00094 Printf.printf "%s\n" (json_to_string result)
95 with Jsont.Error e ->
96 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···99let test_remove json_str pointer_str =
100 try
101 let json = parse_json json_str in
102- let p = Jsont_pointer.of_string pointer_str in
103 let result = Jsont_pointer.remove p json in
104 Printf.printf "%s\n" (json_to_string result)
105 with Jsont.Error e ->
···109let test_replace json_str pointer_str value_str =
110 try
111 let json = parse_json json_str in
112- let p = Jsont_pointer.of_string pointer_str in
113 let value = parse_json value_str in
114 let result = Jsont_pointer.replace p json ~value in
115 Printf.printf "%s\n" (json_to_string result)
···120let test_move json_str from_str path_str =
121 try
122 let json = parse_json json_str in
123- let from = Jsont_pointer.of_string from_str in
124- let path = Jsont_pointer.of_string path_str in
125- let result = Jsont_pointer.move ~from ~path json in
00126 Printf.printf "%s\n" (json_to_string result)
127 with Jsont.Error e ->
128 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···131let test_copy json_str from_str path_str =
132 try
133 let json = parse_json json_str in
134- let from = Jsont_pointer.of_string from_str in
135- let path = Jsont_pointer.of_string path_str in
136- let result = Jsont_pointer.copy ~from ~path json in
00137 Printf.printf "%s\n" (json_to_string result)
138 with Jsont.Error e ->
139 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···142let test_test json_str pointer_str expected_str =
143 try
144 let json = parse_json json_str in
145- let p = Jsont_pointer.of_string pointer_str in
146 let expected = parse_json expected_str in
147 let result = Jsont_pointer.test p json ~expected in
148 Printf.printf "%b\n" result
···153let test_has json_str pointer_str =
154 try
155 let json = parse_json json_str in
156- let p = Jsont_pointer.of_string pointer_str in
157 let result = Jsont_pointer.find p json in
158 Printf.printf "%b\n" (Option.is_some result)
159 with Jsont.Error e ->
···17 | Ok s -> s
18 | Error e -> failwith e
1920+(* Helper to get indices from either nav or append pointer *)
21+let indices_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
22+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
23+ match result with
24+ | `Nav p -> Jsont_pointer.indices p
25+ | `Append p -> Jsont_pointer.indices p
26+27+(* Helper to convert to string from either nav or append pointer *)
28+let to_string_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
29+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
30+ match result with
31+ | `Nav p -> Jsont_pointer.to_string p
32+ | `Append p -> Jsont_pointer.to_string p
33+34(* Test: parse pointer and print indices *)
35let test_parse pointer_str =
36 try
37+ let result = Jsont_pointer.of_string pointer_str in
38+ let indices = indices_of_result result in
39 let index_strs = List.map (fun idx ->
40 match idx with
41+ | Jsont.Path.Mem (s, _) -> Printf.sprintf "Mem:%s" s
42+ | Jsont.Path.Nth (n, _) -> Printf.sprintf "Nth:%d" n
043 ) indices in
44+ let suffix = match result with `Nav _ -> "" | `Append _ -> ", /-" in
45+ Printf.printf "OK: [%s%s]\n" (String.concat ", " index_strs) suffix
46 with Jsont.Error e ->
47 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
4849(* Test: roundtrip pointer string *)
50let test_roundtrip pointer_str =
51 try
52+ let result = Jsont_pointer.of_string pointer_str in
53+ let s = to_string_of_result result in
54 if s = pointer_str then
55 Printf.printf "OK: %s\n" s
56 else
···62let test_eval json_path pointer_str =
63 try
64 let json = parse_json (read_file json_path) in
65+ let p = Jsont_pointer.of_string_nav pointer_str in
66 let result = Jsont_pointer.get p json in
67 Printf.printf "OK: %s\n" (json_to_string result)
68 with
···87(* Test: URI fragment roundtrip *)
88let test_uri_fragment pointer_str =
89 try
90+ let result = Jsont_pointer.of_string pointer_str in
91+ let frag = match result with
92+ | `Nav p -> Jsont_pointer.to_uri_fragment p
93+ | `Append p -> Jsont_pointer.to_uri_fragment p
94+ in
95+ let result2 = Jsont_pointer.of_uri_fragment frag in
96+ let s2 = to_string_of_result result2 in
97 if s2 = pointer_str then
98 Printf.printf "OK: %s -> %s\n" pointer_str frag
99 else
···105let test_add json_str pointer_str value_str =
106 try
107 let json = parse_json json_str in
0108 let value = parse_json value_str in
109+ let result = match Jsont_pointer.of_string pointer_str with
110+ | `Nav p -> Jsont_pointer.add p json ~value
111+ | `Append p -> Jsont_pointer.add p json ~value
112+ in
113 Printf.printf "%s\n" (json_to_string result)
114 with Jsont.Error e ->
115 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···118let test_remove json_str pointer_str =
119 try
120 let json = parse_json json_str in
121+ let p = Jsont_pointer.of_string_nav pointer_str in
122 let result = Jsont_pointer.remove p json in
123 Printf.printf "%s\n" (json_to_string result)
124 with Jsont.Error e ->
···128let test_replace json_str pointer_str value_str =
129 try
130 let json = parse_json json_str in
131+ let p = Jsont_pointer.of_string_nav pointer_str in
132 let value = parse_json value_str in
133 let result = Jsont_pointer.replace p json ~value in
134 Printf.printf "%s\n" (json_to_string result)
···139let test_move json_str from_str path_str =
140 try
141 let json = parse_json json_str in
142+ let from = Jsont_pointer.of_string_nav from_str in
143+ let result = match Jsont_pointer.of_string path_str with
144+ | `Nav path -> Jsont_pointer.move ~from ~path json
145+ | `Append path -> Jsont_pointer.move ~from ~path json
146+ in
147 Printf.printf "%s\n" (json_to_string result)
148 with Jsont.Error e ->
149 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···152let test_copy json_str from_str path_str =
153 try
154 let json = parse_json json_str in
155+ let from = Jsont_pointer.of_string_nav from_str in
156+ let result = match Jsont_pointer.of_string path_str with
157+ | `Nav path -> Jsont_pointer.copy ~from ~path json
158+ | `Append path -> Jsont_pointer.copy ~from ~path json
159+ in
160 Printf.printf "%s\n" (json_to_string result)
161 with Jsont.Error e ->
162 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···165let test_test json_str pointer_str expected_str =
166 try
167 let json = parse_json json_str in
168+ let p = Jsont_pointer.of_string_nav pointer_str in
169 let expected = parse_json expected_str in
170 let result = Jsont_pointer.test p json ~expected in
171 Printf.printf "%b\n" result
···176let test_has json_str pointer_str =
177 try
178 let json = parse_json json_str in
179+ let p = Jsont_pointer.of_string_nav pointer_str in
180 let result = Jsont_pointer.find p json in
181 Printf.printf "%b\n" (Option.is_some result)
182 with Jsont.Error e ->