···1717 | Ok s -> s
1818 | Error e -> failwith e
19192020+(* Helper to get indices from either nav or append pointer *)
2121+let indices_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
2222+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
2323+ match result with
2424+ | `Nav p -> Jsont_pointer.indices p
2525+ | `Append p -> Jsont_pointer.indices p
2626+2727+(* Helper to convert to string from either nav or append pointer *)
2828+let to_string_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
2929+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
3030+ match result with
3131+ | `Nav p -> Jsont_pointer.to_string p
3232+ | `Append p -> Jsont_pointer.to_string p
3333+2034(* Test: parse pointer and print indices *)
2135let test_parse pointer_str =
2236 try
2323- let p = Jsont_pointer.of_string pointer_str in
2424- let indices = Jsont_pointer.indices p in
3737+ let result = Jsont_pointer.of_string pointer_str in
3838+ let indices = indices_of_result result in
2539 let index_strs = List.map (fun idx ->
2640 match idx with
2727- | `Mem s -> Printf.sprintf "Mem:%s" s
2828- | `Nth n -> Printf.sprintf "Nth:%d" n
2929- | `End -> "End"
4141+ | Jsont.Path.Mem (s, _) -> Printf.sprintf "Mem:%s" s
4242+ | Jsont.Path.Nth (n, _) -> Printf.sprintf "Nth:%d" n
3043 ) indices in
3131- Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
4444+ let suffix = match result with `Nav _ -> "" | `Append _ -> ", /-" in
4545+ Printf.printf "OK: [%s%s]\n" (String.concat ", " index_strs) suffix
3246 with Jsont.Error e ->
3347 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
34483549(* Test: roundtrip pointer string *)
3650let test_roundtrip pointer_str =
3751 try
3838- let p = Jsont_pointer.of_string pointer_str in
3939- let s = Jsont_pointer.to_string p in
5252+ let result = Jsont_pointer.of_string pointer_str in
5353+ let s = to_string_of_result result in
4054 if s = pointer_str then
4155 Printf.printf "OK: %s\n" s
4256 else
···4862let test_eval json_path pointer_str =
4963 try
5064 let json = parse_json (read_file json_path) in
5151- let p = Jsont_pointer.of_string pointer_str in
6565+ let p = Jsont_pointer.of_string_nav pointer_str in
5266 let result = Jsont_pointer.get p json in
5367 Printf.printf "OK: %s\n" (json_to_string result)
5468 with
···7387(* Test: URI fragment roundtrip *)
7488let test_uri_fragment pointer_str =
7589 try
7676- let p = Jsont_pointer.of_string pointer_str in
7777- let frag = Jsont_pointer.to_uri_fragment p in
7878- let p2 = Jsont_pointer.of_uri_fragment frag in
7979- let s2 = Jsont_pointer.to_string p2 in
9090+ let result = Jsont_pointer.of_string pointer_str in
9191+ let frag = match result with
9292+ | `Nav p -> Jsont_pointer.to_uri_fragment p
9393+ | `Append p -> Jsont_pointer.to_uri_fragment p
9494+ in
9595+ let result2 = Jsont_pointer.of_uri_fragment frag in
9696+ let s2 = to_string_of_result result2 in
8097 if s2 = pointer_str then
8198 Printf.printf "OK: %s -> %s\n" pointer_str frag
8299 else
···88105let test_add json_str pointer_str value_str =
89106 try
90107 let json = parse_json json_str in
9191- let p = Jsont_pointer.of_string pointer_str in
92108 let value = parse_json value_str in
9393- let result = Jsont_pointer.add p json ~value in
109109+ let result = match Jsont_pointer.of_string pointer_str with
110110+ | `Nav p -> Jsont_pointer.add p json ~value
111111+ | `Append p -> Jsont_pointer.add p json ~value
112112+ in
94113 Printf.printf "%s\n" (json_to_string result)
95114 with Jsont.Error e ->
96115 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···99118let test_remove json_str pointer_str =
100119 try
101120 let json = parse_json json_str in
102102- let p = Jsont_pointer.of_string pointer_str in
121121+ let p = Jsont_pointer.of_string_nav pointer_str in
103122 let result = Jsont_pointer.remove p json in
104123 Printf.printf "%s\n" (json_to_string result)
105124 with Jsont.Error e ->
···109128let test_replace json_str pointer_str value_str =
110129 try
111130 let json = parse_json json_str in
112112- let p = Jsont_pointer.of_string pointer_str in
131131+ let p = Jsont_pointer.of_string_nav pointer_str in
113132 let value = parse_json value_str in
114133 let result = Jsont_pointer.replace p json ~value in
115134 Printf.printf "%s\n" (json_to_string result)
···120139let test_move json_str from_str path_str =
121140 try
122141 let json = parse_json json_str in
123123- let from = Jsont_pointer.of_string from_str in
124124- let path = Jsont_pointer.of_string path_str in
125125- let result = Jsont_pointer.move ~from ~path json in
142142+ let from = Jsont_pointer.of_string_nav from_str in
143143+ let result = match Jsont_pointer.of_string path_str with
144144+ | `Nav path -> Jsont_pointer.move ~from ~path json
145145+ | `Append path -> Jsont_pointer.move ~from ~path json
146146+ in
126147 Printf.printf "%s\n" (json_to_string result)
127148 with Jsont.Error e ->
128149 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···131152let test_copy json_str from_str path_str =
132153 try
133154 let json = parse_json json_str in
134134- let from = Jsont_pointer.of_string from_str in
135135- let path = Jsont_pointer.of_string path_str in
136136- let result = Jsont_pointer.copy ~from ~path json in
155155+ let from = Jsont_pointer.of_string_nav from_str in
156156+ let result = match Jsont_pointer.of_string path_str with
157157+ | `Nav path -> Jsont_pointer.copy ~from ~path json
158158+ | `Append path -> Jsont_pointer.copy ~from ~path json
159159+ in
137160 Printf.printf "%s\n" (json_to_string result)
138161 with Jsont.Error e ->
139162 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···142165let test_test json_str pointer_str expected_str =
143166 try
144167 let json = parse_json json_str in
145145- let p = Jsont_pointer.of_string pointer_str in
168168+ let p = Jsont_pointer.of_string_nav pointer_str in
146169 let expected = parse_json expected_str in
147170 let result = Jsont_pointer.test p json ~expected in
148171 Printf.printf "%b\n" result
···153176let test_has json_str pointer_str =
154177 try
155178 let json = parse_json json_str in
156156- let p = Jsont_pointer.of_string pointer_str in
179179+ let p = Jsont_pointer.of_string_nav pointer_str in
157180 let result = Jsont_pointer.find p json in
158181 Printf.printf "%b\n" (Option.is_some result)
159182 with Jsont.Error e ->
+218-107
doc/tutorial.md
···44[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
55the `jsont-pointer` OCaml library through interactive examples.
6677+## JSON Pointer vs JSON Path
88+99+Before diving in, it's worth understanding the difference between JSON
1010+Pointer and JSON Path, as they serve different purposes:
1111+1212+**JSON Pointer** (RFC 6901) is an *indicator syntax* that specifies a
1313+*single location* within JSON data. It always identifies at most one
1414+value.
1515+1616+**JSON Path** is a *query syntax* that can *search* JSON data and return
1717+*multiple* values matching specified criteria.
1818+1919+Use JSON Pointer when you need to address a single, specific location
2020+(like JSON Schema's `$ref`). Use JSON Path when you might need multiple
2121+results (like Kubernetes queries).
2222+2323+The `jsont-pointer` library implements JSON Pointer and integrates with
2424+the `Jsont.Path` type for representing navigation indices.
2525+726## Setup
827928First, let's set up our environment with helper functions:
10291130```ocaml
1231# open Jsont_pointer;;
1313-# #install_printer Jsont_pointer_top.printer;;
3232+# #install_printer Jsont_pointer_top.nav_printer;;
3333+# #install_printer Jsont_pointer_top.append_printer;;
1434# #install_printer Jsont_pointer_top.json_printer;;
1535# #install_printer Jsont_pointer_top.error_printer;;
1636# let parse_json s =
···4767The JSON Pointer `/users/0/name` refers to the string `"Alice"`:
48684969```ocaml
5050-# let ptr = of_string "/users/0/name";;
5151-val ptr : t = [`Mem "users"; `Nth 0; `Mem "name"]
7070+# let ptr = of_string_nav "/users/0/name";;
7171+val ptr : nav t = [Mem "users"; Nth 0; Mem "name"]
5272# get ptr users_json;;
5373- : Jsont.json = "Alice"
5474```
55755656-In OCaml, this is represented by the `Jsont_pointer.t` type - a sequence
5757-of navigation steps from the document root to a target value.
7676+In OCaml, this is represented by the `'a Jsont_pointer.t` type - a sequence
7777+of navigation steps from the document root to a target value. The phantom
7878+type parameter `'a` encodes whether this is a navigation pointer or an
7979+append pointer (more on this later).
58805981## Syntax: Reference Tokens
6082···78100Let's see this in action:
7910180102```ocaml
8181-# of_string "";;
8282-- : t = []
103103+# of_string_nav "";;
104104+- : nav t = []
83105```
8410685107The empty pointer has no reference tokens - it points to the root.
8610887109```ocaml
8888-# of_string "/foo";;
8989-- : t = [`Mem "foo"]
110110+# of_string_nav "/foo";;
111111+- : nav t = [Mem "foo"]
90112```
9111392114The pointer `/foo` has one token: `foo`. Since it's not a number, it's
93115interpreted as an object member name (`Mem`).
9411695117```ocaml
9696-# of_string "/foo/0";;
9797-- : t = [`Mem "foo"; `Nth 0]
118118+# of_string_nav "/foo/0";;
119119+- : nav t = [Mem "foo"; Nth 0]
98120```
99121100122Here we have two tokens: `foo` (a member name) and `0` (interpreted as
101123an array index `Nth`).
102124103125```ocaml
104104-# of_string "/foo/bar/baz";;
105105-- : t = [`Mem "foo"; `Mem "bar"; `Mem "baz"]
126126+# of_string_nav "/foo/bar/baz";;
127127+- : nav t = [Mem "foo"; Mem "bar"; Mem "baz"]
106128```
107129108130Multiple tokens navigate deeper into nested structures.
109131110132### The Index Type
111133112112-Each reference token becomes an `Index.t` value in the library:
134134+Each reference token is represented using `Jsont.Path.index`:
113135114136<!-- $MDX skip -->
115137```ocaml
116116-type t = [
117117- | `Mem of string (* Object member access *)
118118- | `Nth of int (* Array index access *)
119119- | `End (* The special "-" marker for append operations *)
120120-]
138138+type index = Jsont.Path.index
139139+(* = Jsont.Path.Mem of string * Jsont.Meta.t
140140+ | Jsont.Path.Nth of int * Jsont.Meta.t *)
121141```
122142123123-The `Mem` variant holds the **unescaped** member name - you work with the
124124-actual key string (like `"a/b"`) and the library handles any escaping needed
143143+The `Mem` constructor is for object member access, and `Nth` is for array
144144+index access. The member name is **unescaped** - you work with the actual
145145+key string (like `"a/b"`) and the library handles any escaping needed
125146for the JSON Pointer string representation.
126147127148### Invalid Syntax
···129150What happens if a pointer doesn't start with `/`?
130151131152```ocaml
132132-# of_string "foo";;
153153+# of_string_nav "foo";;
133154Exception:
134155Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo.
135156```
···140161141162```ocaml
142163# of_string_result "foo";;
143143-- : (t, string) result =
164164+- : ([ `Append of append t | `Nav of nav t ], string) result =
144165Error "Invalid JSON Pointer: must be empty or start with '/': foo"
145166# of_string_result "/valid";;
146146-- : (t, string) result = Ok [`Mem "valid"]
167167+- : ([ `Append of append t | `Nav of nav t ], string) result =
168168+Ok (`Nav [Mem "valid"])
147169```
148170149171## Evaluation: Navigating JSON
···190212### Object Member Access
191213192214```ocaml
193193-# get (of_string "/foo") rfc_example ;;
215215+# get (of_string_nav "/foo") rfc_example ;;
194216- : Jsont.json = ["bar","baz"]
195217```
196218···199221### Array Index Access
200222201223```ocaml
202202-# get (of_string "/foo/0") rfc_example ;;
224224+# get (of_string_nav "/foo/0") rfc_example ;;
203225- : Jsont.json = "bar"
204204-# get (of_string "/foo/1") rfc_example ;;
226226+# get (of_string_nav "/foo/1") rfc_example ;;
205227- : Jsont.json = "baz"
206228```
207229···212234JSON allows empty strings as object keys:
213235214236```ocaml
215215-# get (of_string "/") rfc_example ;;
237237+# get (of_string_nav "/") rfc_example ;;
216238- : Jsont.json = 0
217239```
218240···224246The RFC example includes keys with `/` and `~` characters:
225247226248```ocaml
227227-# get (of_string "/a~1b") rfc_example ;;
249249+# get (of_string_nav "/a~1b") rfc_example ;;
228250- : Jsont.json = 1
229251```
230252···232254[below](#escaping-special-characters).
233255234256```ocaml
235235-# get (of_string "/m~0n") rfc_example ;;
257257+# get (of_string_nav "/m~0n") rfc_example ;;
236258- : Jsont.json = 8
237259```
238260···242264to worry about escaping. The `Mem` variant holds the literal key name:
243265244266```ocaml
245245-# let slash_ptr = make [`Mem "a/b"];;
246246-val slash_ptr : t = [`Mem "a/b"]
267267+# let slash_ptr = make [mem "a/b"];;
268268+val slash_ptr : nav t = [Mem "a/b"]
247269# to_string slash_ptr;;
248270- : string = "/a~1b"
249271# get slash_ptr rfc_example ;;
···257279Most characters don't need escaping in JSON Pointer strings:
258280259281```ocaml
260260-# get (of_string "/c%d") rfc_example ;;
282282+# get (of_string_nav "/c%d") rfc_example ;;
261283- : Jsont.json = 2
262262-# get (of_string "/e^f") rfc_example ;;
284284+# get (of_string_nav "/e^f") rfc_example ;;
263285- : Jsont.json = 3
264264-# get (of_string "/g|h") rfc_example ;;
286286+# get (of_string_nav "/g|h") rfc_example ;;
265287- : Jsont.json = 4
266266-# get (of_string "/ ") rfc_example ;;
288288+# get (of_string_nav "/ ") rfc_example ;;
267289- : Jsont.json = 7
268290```
269291···274296What happens when we try to access something that doesn't exist?
275297276298```ocaml
277277-# get_result (of_string "/nonexistent") rfc_example;;
299299+# get_result (of_string_nav "/nonexistent") rfc_example;;
278300- : (Jsont.json, Jsont.Error.t) result =
279301Error JSON Pointer: member 'nonexistent' not found
280302File "-":
281281-# find (of_string "/nonexistent") rfc_example;;
303303+# find (of_string_nav "/nonexistent") rfc_example;;
282304- : Jsont.json option = None
283305```
284306285307Or an out-of-bounds array index:
286308287309```ocaml
288288-# find (of_string "/foo/99") rfc_example;;
310310+# find (of_string_nav "/foo/99") rfc_example;;
289311- : Jsont.json option = None
290312```
291313292314Or try to index into a non-container:
293315294316```ocaml
295295-# find (of_string "/foo/0/invalid") rfc_example;;
317317+# find (of_string_nav "/foo/0/invalid") rfc_example;;
296318- : Jsont.json option = None
297319```
298320···300322301323<!-- $MDX skip -->
302324```ocaml
303303-val get : t -> Jsont.json -> Jsont.json
304304-val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
305305-val find : t -> Jsont.json -> Jsont.json option
325325+val get : nav t -> Jsont.json -> Jsont.json
326326+val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
327327+val find : nav t -> Jsont.json -> Jsont.json option
306328```
307329308330### Array Index Rules
···318340> note that leading zeros are not allowed
319341320342```ocaml
321321-# of_string "/foo/0";;
322322-- : t = [`Mem "foo"; `Nth 0]
343343+# of_string_nav "/foo/0";;
344344+- : nav t = [Mem "foo"; Nth 0]
323345```
324346325347Zero itself is fine.
326348327349```ocaml
328328-# of_string "/foo/01";;
329329-- : t = [`Mem "foo"; `Mem "01"]
350350+# of_string_nav "/foo/01";;
351351+- : nav t = [Mem "foo"; Mem "01"]
330352```
331353332354But `01` has a leading zero, so it's NOT treated as an array index - it
333355becomes a member name instead. This protects against accidental octal
334356interpretation.
335357336336-## The End-of-Array Marker: `-`
358358+## The End-of-Array Marker: `-` and Type Safety
337359338360RFC 6901, Section 4 introduces a special token:
339361340362> exactly the single character "-", making the new referenced value the
341363> (nonexistent) member after the last array element.
342364343343-This is primarily useful for JSON Patch operations (RFC 6902). Let's see
344344-how it parses:
365365+This `-` marker is unique to JSON Pointer (JSON Path has no equivalent).
366366+It's primarily useful for JSON Patch operations (RFC 6902) to append
367367+elements to arrays.
368368+369369+### Navigation vs Append Pointers
345370371371+The `jsont-pointer` library uses **phantom types** to encode the difference
372372+between pointers that can be used for navigation and pointers that target
373373+the "append position":
374374+375375+<!-- $MDX skip -->
346376```ocaml
347347-# of_string "/foo/-";;
348348-- : t = [`Mem "foo"; `End]
377377+type nav (* A pointer to an existing element *)
378378+type append (* A pointer ending with "-" (append position) *)
379379+type 'a t (* Pointer with phantom type parameter *)
349380```
350381351351-The `-` is recognized as a special `End` index.
352352-353353-However, you cannot evaluate a pointer containing `-` because it refers
354354-to a position that doesn't exist:
382382+When you parse a pointer, you get either a `nav t` or an `append t`:
355383356384```ocaml
357357-# find (of_string "/foo/-") rfc_example;;
358358-- : Jsont.json option = None
385385+# of_string "/foo/0";;
386386+- : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "foo"; Nth 0]
387387+# of_string "/foo/-";;
388388+- : [ `Append of append t | `Nav of nav t ] = `Append [Mem "foo"] /-
359389```
360390361361-The RFC explains this:
391391+The `-` creates an `append` pointer. Note that in the internal
392392+representation, the append position is tracked separately (shown as `/-`).
393393+394394+### Why Phantom Types?
395395+396396+The RFC explains that `-` refers to a *nonexistent* position:
362397363398> Note that the use of the "-" character to index an array will always
364399> result in such an error condition because by definition it refers to
365400> a nonexistent array element.
366401367367-But we'll see later that `-` is very useful for mutation operations!
402402+So you **cannot use `get` or `find`** with an append pointer - it makes
403403+no sense to retrieve a value from a position that doesn't exist! The
404404+library enforces this at compile time:
405405+406406+```ocaml
407407+# (* This won't compile: get requires nav t, not append t *)
408408+ (* get (match of_string "/foo/-" with `Append p -> p | _ -> assert false) rfc_example;; *)
409409+```
410410+411411+However, append pointers **are** valid for mutation operations like `add`:
412412+413413+```ocaml
414414+# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
415415+val arr_obj : Jsont.json = {"foo":["a","b"]}
416416+# match of_string "/foo/-" with
417417+ | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
418418+ | `Nav _ -> assert false ;;
419419+- : Jsont.json = {"foo":["a","b","c"]}
420420+```
421421+422422+For convenience, use `of_string_nav` when you know a pointer shouldn't
423423+contain `-`:
424424+425425+```ocaml
426426+# of_string_nav "/foo/0";;
427427+- : nav t = [Mem "foo"; Nth 0]
428428+# of_string_nav "/foo/-";;
429429+Exception:
430430+Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.
431431+```
432432+433433+### Creating Append Pointers Programmatically
434434+435435+You can convert a navigation pointer to an append pointer using `at_end`:
436436+437437+```ocaml
438438+# let nav_ptr = of_string_nav "/foo";;
439439+val nav_ptr : nav t = [Mem "foo"]
440440+# let app_ptr = at_end nav_ptr;;
441441+val app_ptr : append t = [Mem "foo"] /-
442442+# to_string app_ptr;;
443443+- : string = "/foo/-"
444444+```
368445369446## Mutation Operations
370447···372449(JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer`
373450library provides these operations.
374451452452+### Which Pointer Type for Which Operation?
453453+454454+The phantom type system enforces correct usage:
455455+456456+| Operation | Accepts | Because |
457457+|-----------|---------|---------|
458458+| `get`, `find` | `nav t` only | Can't retrieve from non-existent position |
459459+| `remove` | `nav t` only | Can't remove what doesn't exist |
460460+| `replace` | `nav t` only | Can't replace what doesn't exist |
461461+| `test` | `nav t` only | Can't test non-existent position |
462462+| `add` | `_ t` (both) | Can add at existing position OR append |
463463+| `set` | `_ t` (both) | Can set existing position OR append |
464464+| `move`, `copy` | `from:nav t`, `path:_ t` | Source must exist, dest can be append |
465465+375466### Add
376467377468The `add` operation inserts a value at a location:
···379470```ocaml
380471# let obj = parse_json {|{"foo":"bar"}|};;
381472val obj : Jsont.json = {"foo":"bar"}
382382-# add (of_string "/baz") obj ~value:(Jsont.Json.string "qux")
473473+# add (of_string_nav "/baz") obj ~value:(Jsont.Json.string "qux")
383474 ;;
384475- : Jsont.json = {"foo":"bar","baz":"qux"}
385476```
···389480```ocaml
390481# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
391482val arr_obj : Jsont.json = {"foo":["a","b"]}
392392-# add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
483483+# add (of_string_nav "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
393484 ;;
394485- : Jsont.json = {"foo":["a","X","b"]}
395486```
396487397397-This is where the `-` marker shines - it appends to the end:
488488+This is where the `-` marker and append pointers shine - they append to the end:
398489399490```ocaml
400400-# add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c")
491491+# match of_string "/foo/-" with
492492+ | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
493493+ | `Nav _ -> assert false ;;
494494+- : Jsont.json = {"foo":["a","b","c"]}
495495+```
496496+497497+Or more conveniently using `at_end`:
498498+499499+```ocaml
500500+# add (at_end (of_string_nav "/foo")) arr_obj ~value:(Jsont.Json.string "c")
401501 ;;
402502- : Jsont.json = {"foo":["a","b","c"]}
403503```
404504405505### Remove
406506407407-The `remove` operation deletes a value:
507507+The `remove` operation deletes a value. It only accepts `nav t` because
508508+you can only remove something that exists:
408509409510```ocaml
410511# let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};;
411512val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
412412-# remove (of_string "/baz") two_fields ;;
513513+# remove (of_string_nav "/baz") two_fields ;;
413514- : Jsont.json = {"foo":"bar"}
414515```
415516···418519```ocaml
419520# let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
420521val three_elem : Jsont.json = {"foo":["a","b","c"]}
421421-# remove (of_string "/foo/1") three_elem ;;
522522+# remove (of_string_nav "/foo/1") three_elem ;;
422523- : Jsont.json = {"foo":["a","c"]}
423524```
424525···427528The `replace` operation updates an existing value:
428529429530```ocaml
430430-# replace (of_string "/foo") obj ~value:(Jsont.Json.string "baz")
531531+# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
431532 ;;
432533- : Jsont.json = {"foo":"baz"}
433534```
434535435435-Unlike `add`, `replace` requires the target to already exist.
536536+Unlike `add`, `replace` requires the target to already exist (hence `nav t`).
436537Attempting to replace a nonexistent path raises an error.
437538438539### Move
439540440440-The `move` operation relocates a value:
541541+The `move` operation relocates a value. The source (`from`) must be a `nav t`
542542+(you can only move something that exists), but the destination (`path`) can
543543+be either:
441544442545```ocaml
443546# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
444547val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
445445-# move ~from:(of_string "/foo/bar") ~path:(of_string "/qux/thud") nested
548548+# move ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/qux/thud") nested
446549 ;;
447550- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}
448551```
449552450553### Copy
451554452452-The `copy` operation duplicates a value:
555555+The `copy` operation duplicates a value (same typing as `move`):
453556454557```ocaml
455558# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
456559val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
457457-# copy ~from:(of_string "/foo/bar") ~path:(of_string "/foo/qux") to_copy
560560+# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/foo/qux") to_copy
458561 ;;
459562- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}
460563```
···464567The `test` operation verifies a value (useful in JSON Patch):
465568466569```ocaml
467467-# test (of_string "/foo") obj ~expected:(Jsont.Json.string "bar");;
570570+# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
468571- : bool = true
469469-# test (of_string "/foo") obj ~expected:(Jsont.Json.string "wrong");;
572572+# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
470573- : bool = false
471574```
472575···493596and escaping happens automatically during serialization:
494597495598```ocaml
496496-# let p = make [`Mem "a/b"];;
497497-val p : t = [`Mem "a/b"]
599599+# let p = make [mem "a/b"];;
600600+val p : nav t = [Mem "a/b"]
498601# to_string p;;
499602- : string = "/a~1b"
500500-# of_string "/a~1b";;
501501-- : t = [`Mem "a/b"]
603603+# of_string_nav "/a~1b";;
604604+- : nav t = [Mem "a/b"]
502605```
503606504607### Escaping in Action
···561664This adds percent-encoding on top of the `~0`/`~1` escaping:
562665563666```ocaml
564564-# to_uri_fragment (of_string "/foo");;
667667+# to_uri_fragment (of_string_nav "/foo");;
565668- : string = "/foo"
566566-# to_uri_fragment (of_string "/a~1b");;
669669+# to_uri_fragment (of_string_nav "/a~1b");;
567670- : string = "/a~1b"
568568-# to_uri_fragment (of_string "/c%d");;
671671+# to_uri_fragment (of_string_nav "/c%d");;
569672- : string = "/c%25d"
570570-# to_uri_fragment (of_string "/ ");;
673673+# to_uri_fragment (of_string_nav "/ ");;
571674- : string = "/%20"
572675```
573676···592695Instead of parsing strings, you can build pointers from indices:
593696594697```ocaml
595595-# let port_ptr = make [`Mem "database"; `Mem "port"];;
596596-val port_ptr : t = [`Mem "database"; `Mem "port"]
698698+# let port_ptr = make [mem "database"; mem "port"];;
699699+val port_ptr : nav t = [Mem "database"; Mem "port"]
597700# to_string port_ptr;;
598701- : string = "/database/port"
599702```
600703601601-For array access, use `Nth`:
704704+For array access, use the `nth` helper:
602705603706```ocaml
604604-# let first_feature_ptr = make [`Mem "features"; `Nth 0];;
605605-val first_feature_ptr : t = [`Mem "features"; `Nth 0]
707707+# let first_feature_ptr = make [mem "features"; nth 0];;
708708+val first_feature_ptr : nav t = [Mem "features"; Nth 0]
606709# to_string first_feature_ptr;;
607710- : string = "/features/0"
608711```
609712610713### Pointer Navigation
611714612612-You can build pointers incrementally using `append`:
715715+You can build pointers incrementally using the `/` operator (or `append_index`):
613716614717```ocaml
615615-# let db_ptr = of_string "/database";;
616616-val db_ptr : t = [`Mem "database"]
617617-# let creds_ptr = append db_ptr (`Mem "credentials");;
618618-val creds_ptr : t = [`Mem "database"; `Mem "credentials"]
619619-# let user_ptr = append creds_ptr (`Mem "username");;
620620-val user_ptr : t = [`Mem "database"; `Mem "credentials"; `Mem "username"]
718718+# let db_ptr = of_string_nav "/database";;
719719+val db_ptr : nav t = [Mem "database"]
720720+# let creds_ptr = db_ptr / mem "credentials";;
721721+val creds_ptr : nav t = [Mem "database"; Mem "credentials"]
722722+# let user_ptr = creds_ptr / mem "username";;
723723+val user_ptr : nav t = [Mem "database"; Mem "credentials"; Mem "username"]
621724# to_string user_ptr;;
622725- : string = "/database/credentials/username"
623726```
···625728Or concatenate two pointers:
626729627730```ocaml
628628-# let base = of_string "/api/v1";;
629629-val base : t = [`Mem "api"; `Mem "v1"]
630630-# let endpoint = of_string "/users/0";;
631631-val endpoint : t = [`Mem "users"; `Nth 0]
731731+# let base = of_string_nav "/api/v1";;
732732+val base : nav t = [Mem "api"; Mem "v1"]
733733+# let endpoint = of_string_nav "/users/0";;
734734+val endpoint : nav t = [Mem "users"; Nth 0]
632735# to_string (concat base endpoint);;
633736- : string = "/api/v1/users/0"
634737```
···660763```ocaml
661764# let db_host =
662765 Jsont.Json.decode
663663- (path (of_string "/database/host") Jsont.string)
766766+ (path (of_string_nav "/database/host") Jsont.string)
664767 config_json
665768 |> Result.get_ok;;
666769val db_host : string = "localhost"
667770# let db_port =
668771 Jsont.Json.decode
669669- (path (of_string "/database/port") Jsont.int)
772772+ (path (of_string_nav "/database/port") Jsont.int)
670773 config_json
671774 |> Result.get_ok;;
672775val db_port : int = 5432
···677780```ocaml
678781# let features =
679782 Jsont.Json.decode
680680- (path (of_string "/features") Jsont.(list string))
783783+ (path (of_string_nav "/features") Jsont.(list string))
681784 config_json
682785 |> Result.get_ok;;
683786val features : string list = ["auth"; "logging"; "metrics"]
···690793```ocaml
691794# let timeout =
692795 Jsont.Json.decode
693693- (path ~absent:30 (of_string "/database/timeout") Jsont.int)
796796+ (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int)
694797 config_json
695798 |> Result.get_ok;;
696799val timeout : int = 30
···710813val org_json : Jsont.json =
711814 {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}}
712815# Jsont.Json.decode
713713- (path (of_string "/organization/owner/name") Jsont.string)
816816+ (path (of_string_nav "/organization/owner/name") Jsont.string)
714817 org_json
715818 |> Result.get_ok;;
716819- : string = "Alice"
717820# Jsont.Json.decode
718718- (path (of_string "/organization/members/0/age") Jsont.int)
821821+ (path (of_string_nav "/organization/members/0/age") Jsont.int)
719822 org_json
720823 |> Result.get_ok;;
721824- : int = 28
···727830728831```ocaml
729832# let raw_port =
730730- match get (of_string "/database/port") config_json with
833833+ match get (of_string_nav "/database/port") config_json with
731834 | Jsont.Number (f, _) -> int_of_float f
732835 | _ -> failwith "expected number";;
733836val raw_port : int = 5432
···738841```ocaml
739842# let typed_port =
740843 Jsont.Json.decode
741741- (path (of_string "/database/port") Jsont.int)
844844+ (path (of_string_nav "/database/port") Jsont.int)
742845 config_json
743846 |> Result.get_ok;;
744847val typed_port : int = 5432
···7568593. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index)
7578604. **URI Encoding**: Pointers can be percent-encoded for use in URIs
7588615. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates
862862+6. **Type Safety**: Phantom types (`nav t` vs `append t`) prevent misuse of append pointers with retrieval operations
759863760864The `jsont-pointer` library implements all of this with type-safe OCaml
761865interfaces, integration with the `jsont` codec system, and proper error
762866handling for malformed pointers and missing values.
867867+868868+### Key Points on JSON Pointer vs JSON Path
869869+870870+- **JSON Pointer** addresses a *single* location (like a file path)
871871+- **JSON Path** queries for *multiple* values (like a search)
872872+- The `-` token is unique to JSON Pointer - it means "append position" for arrays
873873+- The library uses phantom types to enforce that `-` (append) pointers cannot be used with `get`/`find`
+231-216
src/jsont_pointer.ml
···5454 else None
5555end
56565757-(* Index type - represents how a token is interpreted in context *)
5858-module Index = struct
5959- type t = [ `Mem of string | `Nth of int | `End ]
6060-6161- let pp ppf = function
6262- | `Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
6363- | `Nth n -> Format.fprintf ppf "/%d" n
6464- | `End -> Format.fprintf ppf "/-"
5757+(* Index type - directly reuses Jsont.Path.index *)
5858+type index = Jsont.Path.index
65596666- let equal i1 i2 = match i1, i2 with
6767- | `Mem s1, `Mem s2 -> String.equal s1 s2
6868- | `Nth n1, `Nth n2 -> Int.equal n1 n2
6969- | `End, `End -> true
7070- | _ -> false
6060+(* Convenience constructors *)
6161+let mem ?(meta = Jsont.Meta.none) s : index = Jsont.Path.Mem (s, meta)
6262+let nth ?(meta = Jsont.Meta.none) n : index = Jsont.Path.Nth (n, meta)
71637272- let compare i1 i2 = match i1, i2 with
7373- | `Mem s1, `Mem s2 -> String.compare s1 s2
7474- | `Mem _, _ -> -1
7575- | _, `Mem _ -> 1
7676- | `Nth n1, `Nth n2 -> Int.compare n1 n2
7777- | `Nth _, `End -> -1
7878- | `End, `Nth _ -> 1
7979- | `End, `End -> 0
6464+let pp_index ppf = function
6565+ | Jsont.Path.Mem (s, _) -> Format.fprintf ppf "/%s" (Token.escape s)
6666+ | Jsont.Path.Nth (n, _) -> Format.fprintf ppf "/%d" n
80678181- let of_path_index (idx : Jsont.Path.index) : t =
8282- match idx with
8383- | Jsont.Path.Mem (s, _meta) -> `Mem s
8484- | Jsont.Path.Nth (n, _meta) -> `Nth n
6868+let equal_index i1 i2 = match i1, i2 with
6969+ | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.equal s1 s2
7070+ | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.equal n1 n2
7171+ | _ -> false
85728686- let to_path_index (idx : t) : Jsont.Path.index option =
8787- match idx with
8888- | `Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none))
8989- | `Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none))
9090- | `End -> None
9191-end
7373+let compare_index i1 i2 = match i1, i2 with
7474+ | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.compare s1 s2
7575+ | Jsont.Path.Mem _, Jsont.Path.Nth _ -> -1
7676+ | Jsont.Path.Nth _, Jsont.Path.Mem _ -> 1
7777+ | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.compare n1 n2
92789393-(* Internal representation: raw unescaped tokens.
9494- Per RFC 6901, interpretation as member name vs array index
9595- depends on the JSON value type at evaluation time. *)
7979+(* Internal representation: raw unescaped tokens *)
9680module Segment = struct
9797- type t =
9898- | Token of string (* Unescaped reference token *)
9999- | End (* The "-" token for end-of-array *)
100100-101101- let of_escaped_string s =
102102- if s = "-" then End
103103- else Token (Token.unescape s)
8181+ type t = string (* Unescaped reference token *)
10482105105- let to_escaped_string = function
106106- | Token s -> Token.escape s
107107- | End -> "-"
8383+ let of_escaped_string s = Token.unescape s
10884109109- (* Convert to Index for a given JSON value type *)
110110- let to_index seg ~for_array =
111111- match seg with
112112- | End -> `End
113113- | Token s ->
114114- if for_array then
115115- match Token.is_valid_array_index s with
116116- | Some n -> `Nth n
117117- | None -> `Mem s (* Invalid index becomes member for error msg *)
118118- else
119119- `Mem s
8585+ let to_escaped_string s = Token.escape s
12086121121- (* Convert from Index *)
12287 let of_index = function
123123- | `End -> End
124124- | `Mem s -> Token s
125125- | `Nth n -> Token (string_of_int n)
8888+ | Jsont.Path.Mem (s, _) -> s
8989+ | Jsont.Path.Nth (n, _) -> string_of_int n
9090+9191+ let to_index s : index =
9292+ match Token.is_valid_array_index s with
9393+ | Some n -> nth n
9494+ | None -> mem s
12695end
12796128128-(* Pointer type - list of segments *)
129129-type t = Segment.t list
9797+(* Phantom types *)
9898+type nav
9999+type append
130100131131-let root = []
101101+(* Pointer type with phantom type parameter *)
102102+type _ t = {
103103+ segments : Segment.t list;
104104+ is_append : bool; (* true if ends with "-" *)
105105+}
132106133133-let is_root p = p = []
107107+let root = { segments = []; is_append = false }
134108135135-(* Convert indices to segments *)
136136-let make indices = List.map Segment.of_index indices
109109+let is_root p = p.segments = [] && not p.is_append
137110138138-(* Convert segments to indices, assuming array context for numeric tokens *)
139139-let indices p = List.map (fun seg -> Segment.to_index seg ~for_array:true) p
111111+let make indices =
112112+ { segments = List.map Segment.of_index indices; is_append = false }
140113141141-let append p idx = p @ [Segment.of_index idx]
114114+let ( / ) p idx =
115115+ { segments = p.segments @ [Segment.of_index idx]; is_append = false }
142116143143-let concat p1 p2 = p1 @ p2
117117+let append_index = ( / )
144118145145-let parent p = match List.rev p with
119119+let at_end p =
120120+ { segments = p.segments; is_append = true }
121121+122122+let concat p1 p2 =
123123+ { segments = p1.segments @ p2.segments; is_append = false }
124124+125125+let parent p =
126126+ match List.rev p.segments with
146127 | [] -> None
147147- | _ :: rest -> Some (List.rev rest)
128128+ | _ :: rest -> Some { segments = List.rev rest; is_append = false }
148129149149-let last p = match List.rev p with
130130+let last p =
131131+ match List.rev p.segments with
150132 | [] -> None
151151- | seg :: _ -> Some (Segment.to_index seg ~for_array:true)
133133+ | seg :: _ -> Some (Segment.to_index seg)
134134+135135+let indices (type a) (p : a t) = List.map Segment.to_index p.segments
152136153137(* Parsing *)
154138155155-let of_string s =
156156- if s = "" then root
139139+let parse_segments s =
140140+ if s = "" then []
157141 else if s.[0] <> '/' then
158142 Jsont.Error.msgf Jsont.Meta.none
159143 "Invalid JSON Pointer: must be empty or start with '/': %s" s
···162146 let tokens = String.split_on_char '/' rest in
163147 List.map Segment.of_escaped_string tokens
164148149149+let of_string s : [ `Nav of nav t | `Append of append t ] =
150150+ let segments = parse_segments s in
151151+ (* Check if ends with "-" *)
152152+ match List.rev segments with
153153+ | "-" :: rest ->
154154+ (* Validate that "-" only appears at the end *)
155155+ if List.exists (( = ) "-") rest then
156156+ Jsont.Error.msgf Jsont.Meta.none
157157+ "Invalid JSON Pointer: '-' can only appear at the end";
158158+ `Append { segments = List.rev rest; is_append = true }
159159+ | _ ->
160160+ (* Validate no "-" anywhere *)
161161+ if List.exists (( = ) "-") segments then
162162+ Jsont.Error.msgf Jsont.Meta.none
163163+ "Invalid JSON Pointer: '-' can only appear at the end";
164164+ `Nav { segments; is_append = false }
165165+166166+let of_string_nav s : nav t =
167167+ match of_string s with
168168+ | `Nav p -> p
169169+ | `Append _ ->
170170+ Jsont.Error.msgf Jsont.Meta.none
171171+ "Invalid JSON Pointer: '-' not allowed in navigation pointer"
172172+165173let of_string_result s =
166174 try Ok (of_string s)
167175 with Jsont.Error e -> Error (Jsont.Error.to_string e)
···195203 in
196204 loop 0
197205198198-let of_uri_fragment s =
199199- of_string (percent_decode s)
206206+let of_uri_fragment s = of_string (percent_decode s)
207207+208208+let of_uri_fragment_nav s = of_string_nav (percent_decode s)
200209201210let of_uri_fragment_result s =
202211 try Ok (of_uri_fragment s)
···204213205214(* Serialization *)
206215207207-let to_string p =
208208- if p = [] then ""
209209- else
210210- let b = Buffer.create 64 in
211211- List.iter (fun seg ->
212212- Buffer.add_char b '/';
213213- Buffer.add_string b (Segment.to_escaped_string seg)
214214- ) p;
215215- Buffer.contents b
216216+let to_string (type a) (p : a t) =
217217+ let base =
218218+ if p.segments = [] then ""
219219+ else
220220+ let b = Buffer.create 64 in
221221+ List.iter (fun seg ->
222222+ Buffer.add_char b '/';
223223+ Buffer.add_string b (Segment.to_escaped_string seg)
224224+ ) p.segments;
225225+ Buffer.contents b
226226+ in
227227+ if p.is_append then base ^ "/-" else base
216228217229(* URI fragment percent-encoding *)
218230let needs_percent_encoding c =
219219- (* RFC 3986 fragment: unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" *)
220220- (* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" *)
221221- (* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *)
222231 not (
223232 (c >= 'A' && c <= 'Z') ||
224233 (c >= 'a' && c <= 'z') ||
···247256 ) s;
248257 Buffer.contents b
249258250250-let to_uri_fragment p =
251251- percent_encode (to_string p)
259259+let to_uri_fragment p = percent_encode (to_string p)
252260253253-let pp ppf p =
254254- Format.pp_print_string ppf (to_string p)
261261+let pp ppf p = Format.pp_print_string ppf (to_string p)
255262256256-let pp_verbose ppf p =
257257- let pp_index ppf = function
258258- | `Mem s -> Format.fprintf ppf {|`Mem "%s"|} s
259259- | `Nth n -> Format.fprintf ppf "`Nth %d" n
260260- | `End -> Format.fprintf ppf "`End"
263263+let pp_verbose (type a) ppf (p : a t) =
264264+ let pp_idx ppf seg =
265265+ match Token.is_valid_array_index seg with
266266+ | Some n -> Format.fprintf ppf "Nth %d" n
267267+ | None -> Format.fprintf ppf {|Mem "%s"|} seg
261268 in
262262- Format.fprintf ppf "[%a]"
263263- (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_index)
264264- (indices p)
269269+ Format.fprintf ppf "[%a]%s"
270270+ (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_idx)
271271+ p.segments
272272+ (if p.is_append then " /-" else "")
265273266274(* Comparison *)
267275268268-let segment_equal s1 s2 = match s1, s2 with
269269- | Segment.Token t1, Segment.Token t2 -> String.equal t1 t2
270270- | Segment.End, Segment.End -> true
271271- | _ -> false
276276+let equal (type a b) (p1 : a t) (p2 : b t) =
277277+ List.equal String.equal p1.segments p2.segments &&
278278+ p1.is_append = p2.is_append
272279273273-let segment_compare s1 s2 = match s1, s2 with
274274- | Segment.Token t1, Segment.Token t2 -> String.compare t1 t2
275275- | Segment.Token _, Segment.End -> -1
276276- | Segment.End, Segment.Token _ -> 1
277277- | Segment.End, Segment.End -> 0
278278-279279-let equal p1 p2 =
280280- List.equal segment_equal p1 p2
281281-282282-let compare p1 p2 =
283283- List.compare segment_compare p1 p2
280280+let compare (type a b) (p1 : a t) (p2 : b t) =
281281+ match List.compare String.compare p1.segments p2.segments with
282282+ | 0 -> Bool.compare p1.is_append p2.is_append
283283+ | n -> n
284284285285(* Path conversion *)
286286287287-let segment_of_path_index (idx : Jsont.Path.index) : Segment.t =
288288- match idx with
289289- | Jsont.Path.Mem (s, _meta) -> Segment.Token s
290290- | Jsont.Path.Nth (n, _meta) -> Segment.Token (string_of_int n)
291291-292292-let of_path (p : Jsont.Path.t) : t =
293293- List.rev_map segment_of_path_index (Jsont.Path.rev_indices p)
294294-295295-let to_path p =
296296- let rec convert acc = function
297297- | [] -> Some acc
298298- | Segment.End :: _ -> None
299299- | Segment.Token s :: rest ->
300300- (* For path conversion, we need to decide if it's a member or index.
301301- We use array context for numeric tokens since Jsont.Path distinguishes. *)
302302- let acc' = match Token.is_valid_array_index s with
303303- | Some n -> Jsont.Path.nth ~meta:Jsont.Meta.none n acc
304304- | None -> Jsont.Path.mem ~meta:Jsont.Meta.none s acc
305305- in
306306- convert acc' rest
307307- in
308308- convert Jsont.Path.root p
287287+let of_path (p : Jsont.Path.t) : nav t =
288288+ let segments = List.rev_map Segment.of_index (Jsont.Path.rev_indices p) in
289289+ { segments; is_append = false }
309290310310-let to_path_exn p =
311311- match to_path p with
312312- | Some path -> path
313313- | None ->
314314- Jsont.Error.msgf Jsont.Meta.none
315315- "Cannot convert JSON Pointer with '-' index to Jsont.Path"
291291+let to_path (p : nav t) : Jsont.Path.t =
292292+ List.fold_left (fun acc seg ->
293293+ match Token.is_valid_array_index seg with
294294+ | Some n -> Jsont.Path.nth n acc
295295+ | None -> Jsont.Path.mem seg acc
296296+ ) Jsont.Path.root p.segments
316297317298(* Evaluation helpers *)
318299···332313 if n < 0 || n >= List.length arr then None
333314 else Some (List.nth arr n)
334315335335-(* Evaluation *)
316316+(* Evaluation - only for nav pointers *)
336317337337-let rec eval_get p json =
338338- match p with
318318+let rec eval_get segments json =
319319+ match segments with
339320 | [] -> json
340340- | Segment.End :: _ ->
341341- Jsont.Error.msgf (Jsont.Json.meta json)
342342- "JSON Pointer: '-' (end marker) refers to nonexistent array element"
343343- | Segment.Token token :: rest ->
321321+ | token :: rest ->
344322 (match json with
345323 | Jsont.Object (members, _) ->
346346- (* For objects, token is always a member name *)
347324 (match get_member token members with
348325 | Some (_, value) -> eval_get rest value
349326 | None ->
350327 Jsont.Error.msgf (Jsont.Json.meta json)
351328 "JSON Pointer: member '%s' not found" token)
352329 | Jsont.Array (elements, _) ->
353353- (* For arrays, token must be a valid array index *)
354330 (match Token.is_valid_array_index token with
355331 | Some n ->
356332 (match get_nth n elements with
···367343 "JSON Pointer: cannot index into %s with '%s'"
368344 (json_sort_string json) token)
369345370370-let get p json = eval_get p json
346346+let get (p : nav t) json = eval_get p.segments json
371347372348let get_result p json =
373349 try Ok (get p json)
···431407 Jsont.Error.msgf (Jsont.Json.meta json)
432408 "JSON Pointer: cannot navigate through %s" (json_sort_string json)
433409434434-(* Mutation: set *)
410410+(* Mutation: set - works with any pointer type *)
435411436436-let rec eval_set p value json =
437437- match p with
438438- | [] -> value
439439- | [Segment.End] ->
412412+let rec eval_set_segments segments is_append value json =
413413+ match segments, is_append with
414414+ | [], false -> value
415415+ | [], true ->
416416+ (* Append to array *)
440417 (match json with
441418 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta)
442419 | _ ->
443420 Jsont.Error.msgf (Jsont.Json.meta json)
444421 "JSON Pointer: '-' can only be used on arrays, got %s"
445422 (json_sort_string json))
446446- | Segment.End :: _ ->
447447- Jsont.Error.msgf (Jsont.Json.meta json)
448448- "JSON Pointer: '-' (end marker) refers to nonexistent array element"
449449- | [Segment.Token token] ->
423423+ | [token], false ->
450424 navigate_to_child token json
451425 ~on_object:(fun members meta ->
452426 if Option.is_some (get_member token members) then
···463437 ~on_other:(fun () ->
464438 Jsont.Error.msgf (Jsont.Json.meta json)
465439 "JSON Pointer: cannot set in %s" (json_sort_string json))
466466- | Segment.Token token :: rest ->
440440+ | [token], true ->
441441+ (* Navigate to token, then append *)
467442 navigate_to_child token json
468443 ~on_object:(fun members meta ->
469444 match get_member token members with
470445 | Some (_, child) ->
471471- Jsont.Object (set_member token (eval_set rest value child) members, meta)
446446+ let child' = eval_set_segments [] true value child in
447447+ Jsont.Object (set_member token child' members, meta)
472448 | None -> error_member_not_found json token)
473449 ~on_array:(fun elements meta n ->
474450 match get_nth n elements with
475451 | Some child ->
476476- Jsont.Array (replace_at n (eval_set rest value child) elements, meta)
452452+ let child' = eval_set_segments [] true value child in
453453+ Jsont.Array (replace_at n child' elements, meta)
454454+ | None -> error_index_out_of_bounds json n)
455455+ ~on_other:(fun () -> error_cannot_navigate json)
456456+ | token :: rest, _ ->
457457+ navigate_to_child token json
458458+ ~on_object:(fun members meta ->
459459+ match get_member token members with
460460+ | Some (_, child) ->
461461+ Jsont.Object (set_member token (eval_set_segments rest is_append value child) members, meta)
462462+ | None -> error_member_not_found json token)
463463+ ~on_array:(fun elements meta n ->
464464+ match get_nth n elements with
465465+ | Some child ->
466466+ Jsont.Array (replace_at n (eval_set_segments rest is_append value child) elements, meta)
477467 | None -> error_index_out_of_bounds json n)
478468 ~on_other:(fun () -> error_cannot_navigate json)
479469480480-let set p json ~value = eval_set p value json
470470+let set (type a) (p : a t) json ~value =
471471+ eval_set_segments p.segments p.is_append value json
481472482482-(* Mutation: add (RFC 6902 semantics) *)
473473+(* Mutation: add (RFC 6902 semantics) - works with any pointer type *)
483474484484-let rec eval_add p value json =
485485- match p with
486486- | [] -> value
487487- | [Segment.End] ->
475475+let rec eval_add_segments segments is_append value json =
476476+ match segments, is_append with
477477+ | [], false -> value
478478+ | [], true ->
479479+ (* Append to array *)
488480 (match json with
489481 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta)
490482 | _ ->
491483 Jsont.Error.msgf (Jsont.Json.meta json)
492484 "JSON Pointer: '-' can only be used on arrays, got %s"
493485 (json_sort_string json))
494494- | Segment.End :: _ ->
495495- Jsont.Error.msgf (Jsont.Json.meta json)
496496- "JSON Pointer: '-' in non-final position"
497497- | [Segment.Token token] ->
486486+ | [token], false ->
498487 navigate_to_child token json
499488 ~on_object:(fun members meta ->
500489 Jsont.Object (set_member token value members, meta))
···509498 ~on_other:(fun () ->
510499 Jsont.Error.msgf (Jsont.Json.meta json)
511500 "JSON Pointer: cannot add to %s" (json_sort_string json))
512512- | Segment.Token token :: rest ->
501501+ | [token], true ->
502502+ (* Navigate to token, then append *)
513503 navigate_to_child token json
514504 ~on_object:(fun members meta ->
515505 match get_member token members with
516506 | Some (_, child) ->
517517- Jsont.Object (set_member token (eval_add rest value child) members, meta)
507507+ let child' = eval_add_segments [] true value child in
508508+ Jsont.Object (set_member token child' members, meta)
518509 | None -> error_member_not_found json token)
519510 ~on_array:(fun elements meta n ->
520511 match get_nth n elements with
521512 | Some child ->
522522- Jsont.Array (replace_at n (eval_add rest value child) elements, meta)
513513+ let child' = eval_add_segments [] true value child in
514514+ Jsont.Array (replace_at n child' elements, meta)
515515+ | None -> error_index_out_of_bounds json n)
516516+ ~on_other:(fun () -> error_cannot_navigate json)
517517+ | token :: rest, _ ->
518518+ navigate_to_child token json
519519+ ~on_object:(fun members meta ->
520520+ match get_member token members with
521521+ | Some (_, child) ->
522522+ Jsont.Object (set_member token (eval_add_segments rest is_append value child) members, meta)
523523+ | None -> error_member_not_found json token)
524524+ ~on_array:(fun elements meta n ->
525525+ match get_nth n elements with
526526+ | Some child ->
527527+ Jsont.Array (replace_at n (eval_add_segments rest is_append value child) elements, meta)
523528 | None -> error_index_out_of_bounds json n)
524529 ~on_other:(fun () -> error_cannot_navigate json)
525530526526-let add p json ~value = eval_add p value json
531531+let add (type a) (p : a t) json ~value =
532532+ eval_add_segments p.segments p.is_append value json
527533528528-(* Mutation: remove *)
534534+(* Mutation: remove - only for nav pointers *)
529535530530-let rec eval_remove p json =
531531- match p with
536536+let rec eval_remove_segments segments json =
537537+ match segments with
532538 | [] ->
533539 Jsont.Error.msgf Jsont.Meta.none "JSON Pointer: cannot remove root document"
534534- | [Segment.End] ->
535535- Jsont.Error.msgf (Jsont.Json.meta json)
536536- "JSON Pointer: '-' refers to nonexistent element"
537537- | Segment.End :: _ ->
538538- Jsont.Error.msgf (Jsont.Json.meta json)
539539- "JSON Pointer: '-' in non-final position"
540540- | [Segment.Token token] ->
540540+ | [token] ->
541541 navigate_to_child token json
542542 ~on_object:(fun members meta ->
543543 if Option.is_some (get_member token members) then
···554554 ~on_other:(fun () ->
555555 Jsont.Error.msgf (Jsont.Json.meta json)
556556 "JSON Pointer: cannot remove from %s" (json_sort_string json))
557557- | Segment.Token token :: rest ->
557557+ | token :: rest ->
558558 navigate_to_child token json
559559 ~on_object:(fun members meta ->
560560 match get_member token members with
561561 | Some (_, child) ->
562562- Jsont.Object (set_member token (eval_remove rest child) members, meta)
562562+ Jsont.Object (set_member token (eval_remove_segments rest child) members, meta)
563563 | None -> error_member_not_found json token)
564564 ~on_array:(fun elements meta n ->
565565 match get_nth n elements with
566566 | Some child ->
567567- Jsont.Array (replace_at n (eval_remove rest child) elements, meta)
567567+ Jsont.Array (replace_at n (eval_remove_segments rest child) elements, meta)
568568 | None -> error_index_out_of_bounds json n)
569569 ~on_other:(fun () -> error_cannot_navigate json)
570570571571-let remove p json = eval_remove p json
571571+let remove (p : nav t) json = eval_remove_segments p.segments json
572572573573-(* Mutation: replace *)
573573+(* Mutation: replace - only for nav pointers *)
574574575575-let replace p json ~value =
576576- (* Replace requires the target to exist, unlike add *)
575575+let replace (p : nav t) json ~value =
577576 let _ = get p json in (* Will raise if not found *)
578578- eval_set p value json
577577+ eval_set_segments p.segments false value json
579578580579(* Mutation: move *)
581580582582-let rec is_prefix_of p1 p2 =
583583- match p1, p2 with
584584- | [], _ -> true
585585- | _, [] -> false
586586- | h1 :: t1, h2 :: t2 -> segment_equal h1 h2 && is_prefix_of t1 t2
587587-588588-let move ~from ~path json =
581581+let move ~(from : nav t) ~(path : _ t) json =
589582 (* Check for cycle: path cannot be a proper prefix of from *)
590590- if is_prefix_of path from && not (equal path from) then
583583+ let from_segs = from.segments in
584584+ let path_segs = path.segments in
585585+ let rec is_prefix p1 p2 = match p1, p2 with
586586+ | [], _ -> true
587587+ | _, [] -> false
588588+ | h1 :: t1, h2 :: t2 -> String.equal h1 h2 && is_prefix t1 t2
589589+ in
590590+ if is_prefix path_segs from_segs &&
591591+ not (List.equal String.equal path_segs from_segs && path.is_append = false) then
591592 Jsont.Error.msgf Jsont.Meta.none
592593 "JSON Pointer: move would create cycle (path is prefix of from)";
593594 let value = get from json in
···596597597598(* Mutation: copy *)
598599599599-let copy ~from ~path json =
600600+let copy ~(from : nav t) ~(path : _ t) json =
600601 let value = get from json in
601602 add path json ~value
602603603604(* Mutation: test *)
604605605605-let test p json ~expected =
606606+let test (p : nav t) json ~expected =
606607 Option.fold ~none:false ~some:(Jsont.Json.equal expected) (find p json)
607608608609(* Jsont codec *)
609610610610-let jsont : t Jsont.t =
611611+let jsont : [ `Nav of nav t | `Append of append t ] Jsont.t =
611612 let dec _meta s = of_string s in
612612- let enc p = to_string p in
613613+ let enc = function
614614+ | `Nav p -> to_string p
615615+ | `Append p -> to_string p
616616+ in
613617 Jsont.Base.string (Jsont.Base.map
614618 ~kind:"JSON Pointer"
615619 ~doc:"RFC 6901 JSON Pointer"
616620 ~dec ~enc ())
617621618618-let jsont_uri_fragment : t Jsont.t =
622622+let jsont_nav : nav t Jsont.t =
623623+ let dec _meta s = of_string_nav s in
624624+ let enc p = to_string p in
625625+ Jsont.Base.string (Jsont.Base.map
626626+ ~kind:"JSON Pointer (nav)"
627627+ ~doc:"RFC 6901 JSON Pointer (navigation only)"
628628+ ~dec ~enc ())
629629+630630+let jsont_uri_fragment : [ `Nav of nav t | `Append of append t ] Jsont.t =
619631 let dec _meta s = of_uri_fragment s in
620620- let enc p = to_uri_fragment p in
632632+ let enc = function
633633+ | `Nav p -> to_uri_fragment p
634634+ | `Append p -> to_uri_fragment p
635635+ in
621636 Jsont.Base.string (Jsont.Base.map
622637 ~kind:"JSON Pointer (URI fragment)"
623638 ~doc:"RFC 6901 JSON Pointer in URI fragment encoding"
···625640626641(* Query combinators *)
627642628628-let path ?absent p t =
643643+let path ?absent (p : nav t) t =
629644 let dec json =
630645 match find p json with
631646 | Some value ->
···642657 Jsont.map Jsont.json ~dec ~enc:(fun _ ->
643658 Jsont.Error.msgf Jsont.Meta.none "path: encode not supported")
644659645645-let set_path ?(allow_absent = false) t p v =
660660+let set_path (type a) ?(allow_absent = false) t (p : a t) v =
646661 let encoded = match Jsont.Json.encode' t v with
647662 | Ok json -> json
648663 | Error e -> raise (Jsont.Error e)
···655670 in
656671 Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
657672658658-let update_path ?absent p t =
673673+let update_path ?absent (p : nav t) t =
659674 let dec json =
660675 let value = match find p json with
661676 | Some v -> v
···681696 in
682697 Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
683698684684-let delete_path ?(allow_absent = false) p =
699699+let delete_path ?(allow_absent = false) (p : nav t) =
685700 let dec json =
686701 if allow_absent then
687702 match find p json with
+167-116
src/jsont_pointer.mli
···99 JSON Pointer parsing, serialization, and evaluation compatible with
1010 {!Jsont} codecs.
11111212- A JSON Pointer is a string syntax for identifying a specific value within
1313- a JSON document. For example, given the JSON document:
1212+ {1 JSON Pointer vs JSON Path}
1313+1414+ JSON Pointer (RFC 6901) and {!Jsont.Path} serve similar purposes but
1515+ have important differences:
1616+1717+ {ul
1818+ {- {b JSON Pointer} is a {e string syntax} for addressing JSON values,
1919+ designed for use in URIs and JSON documents (like JSON Patch).
2020+ It uses [/] as separator and has escape sequences ([~0], [~1]).}
2121+ {- {b Jsont.Path} is an {e OCaml data structure} for programmatic
2222+ navigation, with no string representation defined.}}
2323+2424+ A key difference is the [-] token: JSON Pointer's [-] refers to the
2525+ (nonexistent) element {e after} the last array element. This is used
2626+ for append operations in JSON Patch but is meaningless for retrieval.
2727+ {!Jsont.Path} has no equivalent concept.
2828+2929+ This library uses phantom types to enforce this distinction at compile
3030+ time: pointers that may contain [-] ({!append} pointers) cannot be
3131+ passed to retrieval functions like {!get}.
3232+3333+ {2 Example}
3434+3535+ Given the JSON document:
1436 {v
1537 {
1638 "foo": ["bar", "baz"],
···2749 {- ["/foo/0"] - the string ["bar"]}
2850 {- ["/"] - the integer [0] (empty string key)}
2951 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])}
3030- {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}}
5252+ {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}
5353+ {- ["/foo/-"] - nonexistent; only valid for mutations}}
31543255 {1:tokens Reference Tokens}
3356···60836184(** {1 Indices}
62856363- Indices represent individual navigation steps in a JSON Pointer.
6464- For objects, this is a member name. For arrays, this is either
6565- a numeric index or the special end-of-array marker [-]. *)
6666-module Index : sig
8686+ Indices are the individual navigation steps in a JSON Pointer.
8787+ This library reuses {!Jsont.Path.index} directly - the JSON Pointer
8888+ specific [-] token is handled separately via phantom types on the
8989+ pointer type itself. *)
67906868- type t = [
6969- | `Mem of string
7070- (** [`Mem name] indexes into an object member with the given [name].
7171- The name is unescaped (i.e., [/] and [~] appear literally). *)
7272- | `Nth of int
7373- (** [`Nth n] indexes into an array at position [n] (zero-based).
7474- Must be non-negative and without leading zeros in string form
7575- (except for [0] itself). *)
7676- | `End
7777- (** [`End] represents the [-] token, indicating the position after
7878- the last element of an array. This is used for append operations
7979- in {!Jsont_pointer.add} and similar mutation functions.
8080- Evaluating a pointer containing [`End] with {!Jsont_pointer.get}
8181- will raise an error since it refers to a nonexistent element. *)
8282- ]
9191+type index = Jsont.Path.index
9292+(** The type for navigation indices. This is exactly {!Jsont.Path.index}:
9393+ either [Jsont.Path.Mem (name, meta)] for object member access or
9494+ [Jsont.Path.Nth (n, meta)] for array index access. *)
83958484- val pp : Format.formatter -> t -> unit
8585- (** [pp] formats an index in JSON Pointer string notation. *)
9696+val mem : ?meta:Jsont.Meta.t -> string -> index
9797+(** [mem ?meta s] is [Jsont.Path.Mem (s, meta)].
9898+ Convenience constructor for object member access.
9999+ [meta] defaults to {!Jsont.Meta.none}. *)
861008787- val equal : t -> t -> bool
8888- (** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *)
101101+val nth : ?meta:Jsont.Meta.t -> int -> index
102102+(** [nth ?meta n] is [Jsont.Path.Nth (n, meta)].
103103+ Convenience constructor for array index access.
104104+ [meta] defaults to {!Jsont.Meta.none}. *)
891059090- val compare : t -> t -> int
9191- (** [compare i1 i2] is a total order on indices. *)
106106+val pp_index : Format.formatter -> index -> unit
107107+(** [pp_index] formats an index in JSON Pointer string notation. *)
921089393- (** {2:jsont_conv Conversion with Jsont.Path} *)
109109+val equal_index : index -> index -> bool
110110+(** [equal_index i1 i2] is [true] iff [i1] and [i2] are the same index. *)
941119595- val of_path_index : Jsont.Path.index -> t
9696- (** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *)
112112+val compare_index : index -> index -> int
113113+(** [compare_index i1 i2] is a total order on indices. *)
971149898- val to_path_index : t -> Jsont.Path.index option
9999- (** [to_path_index idx] converts to a {!Jsont.Path.index}.
100100- Returns [None] for [`End] since it has no equivalent in
101101- {!Jsont.Path}. *)
102102-end
115115+(** {1 Pointers}
103116104104-(** {1 Pointers} *)
117117+ JSON Pointers use phantom types to distinguish between:
118118+ {ul
119119+ {- {!nav} pointers that reference existing elements (safe for all operations)}
120120+ {- {!append} pointers that end with [-] (only valid for {!add} and {!set})}}
105121106106-type t
107107-(** The type for JSON Pointers. A pointer is a sequence of {!Index.t}
108108- values representing a path from the root of a JSON document to
109109- a specific value. *)
122122+ This ensures at compile time that you cannot accidentally try to
123123+ retrieve a nonexistent "end of array" position. *)
124124+125125+type 'a t
126126+(** The type for JSON Pointers. The phantom type ['a] indicates whether
127127+ the pointer can be used for navigation ([nav]) or only for append
128128+ operations ([append]). *)
129129+130130+type nav
131131+(** Phantom type for pointers that reference existing elements.
132132+ These can be used with all operations including {!get} and {!find}. *)
133133+134134+type append
135135+(** Phantom type for pointers ending with [-] (the "after last element"
136136+ position). These can only be used with {!add} and {!set}. *)
110137111111-val root : t
138138+val root : nav t
112139(** [root] is the empty pointer that references the whole document.
113140 In string form this is [""]. *)
114141115115-val is_root : t -> bool
142142+val is_root : _ t -> bool
116143(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
117144118118-val make : Index.t list -> t
119119-(** [make indices] creates a pointer from a list of indices.
145145+val make : index list -> nav t
146146+(** [make indices] creates a navigation pointer from a list of indices.
120147 The list is ordered from root to target (i.e., the first element
121148 is the first step from the root). *)
122149123123-val indices : t -> Index.t list
124124-(** [indices p] returns the indices of [p] from root to target. *)
150150+val ( / ) : nav t -> index -> nav t
151151+(** [p / idx] appends [idx] to pointer [p]. Operator form of {!append_index}. *)
152152+153153+val append_index : nav t -> index -> nav t
154154+(** [append_index p idx] appends [idx] to the end of pointer [p]. *)
125155126126-val append : t -> Index.t -> t
127127-(** [append p idx] appends [idx] to the end of pointer [p]. *)
156156+val at_end : nav t -> append t
157157+(** [at_end p] creates an append pointer by adding [-] to [p].
158158+ The resulting pointer refers to the position after the last element
159159+ of the array at [p]. Only valid for use with {!add} and {!set}. *)
128160129129-val concat : t -> t -> t
161161+val concat : nav t -> nav t -> nav t
130162(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
131163132132-val parent : t -> t option
164164+val parent : nav t -> nav t option
133165(** [parent p] returns the parent pointer of [p], or [None] if [p]
134166 is the {!root}. *)
135167136136-val last : t -> Index.t option
168168+val last : nav t -> index option
137169(** [last p] returns the last index of [p], or [None] if [p] is
138170 the {!root}. *)
139171172172+val indices : _ t -> index list
173173+(** [indices p] returns the indices of [p] from root to target.
174174+ Note: for append pointers, this returns the indices of the path
175175+ portion; the [-] (append position) is not represented as an index. *)
176176+140177(** {2:parsing Parsing} *)
141178142142-val of_string : string -> t
179179+val of_string : string -> [ `Nav of nav t | `Append of append t ]
143180(** [of_string s] parses a JSON Pointer from its string representation.
144181182182+ Returns [`Nav p] for pointers without [-], or [`Append p] for
183183+ pointers ending with [-].
184184+145185 The string must be either empty (representing the root) or start
146186 with [/]. Each segment between [/] characters is unescaped as a
147147- reference token. Segments that are valid non-negative integers
148148- without leading zeros become [`Nth] indices; the string [-]
149149- becomes [`End]; all others become [`Mem].
187187+ reference token.
150188151189 @raise Jsont.Error if [s] has invalid syntax:
152190 - Non-empty string not starting with [/]
153191 - Invalid escape sequence ([~] not followed by [0] or [1])
154154- - Array index with leading zeros
155155- - Array index that overflows [int] *)
192192+ - [-] appears in non-final position *)
193193+194194+val of_string_nav : string -> nav t
195195+(** [of_string_nav s] parses a JSON Pointer that must not contain [-].
196196+197197+ @raise Jsont.Error if [s] has invalid syntax or contains [-]. *)
156198157157-val of_string_result : string -> (t, string) result
199199+val of_string_result : string -> ([ `Nav of nav t | `Append of append t ], string) result
158200(** [of_string_result s] is like {!of_string} but returns a result
159201 instead of raising. *)
160202161161-val of_uri_fragment : string -> t
203203+val of_uri_fragment : string -> [ `Nav of nav t | `Append of append t ]
162204(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
163205164206 This is like {!of_string} but first percent-decodes the string
···167209168210 @raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
169211170170-val of_uri_fragment_result : string -> (t, string) result
212212+val of_uri_fragment_nav : string -> nav t
213213+(** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires
214214+ the pointer to not contain [-].
215215+216216+ @raise Jsont.Error if invalid or contains [-]. *)
217217+218218+val of_uri_fragment_result : string -> ([ `Nav of nav t | `Append of append t ], string) result
171219(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
172220 a result instead of raising. *)
173221174222(** {2:serializing Serializing} *)
175223176176-val to_string : t -> string
224224+val to_string : _ t -> string
177225(** [to_string p] serializes [p] to its JSON Pointer string representation.
178226179227 Returns [""] for the root pointer, otherwise [/] followed by
180180- escaped reference tokens joined by [/]. *)
228228+ escaped reference tokens joined by [/]. Append pointers include
229229+ the trailing [/-]. *)
181230182182-val to_uri_fragment : t -> string
231231+val to_uri_fragment : _ t -> string
183232(** [to_uri_fragment p] serializes [p] to URI fragment form.
184233185234 This is like {!to_string} but additionally percent-encodes
186235 characters that are not allowed in URI fragments per RFC 3986.
187236 The leading [#] is {b not} included in the result. *)
188237189189-val pp : Format.formatter -> t -> unit
238238+val pp : Format.formatter -> _ t -> unit
190239(** [pp] formats a pointer using {!to_string}. *)
191240192192-val pp_verbose : Format.formatter -> t -> unit
241241+val pp_verbose : Format.formatter -> _ t -> unit
193242(** [pp_verbose] formats a pointer showing its index structure.
194194- For example, [/foo/0/-] is formatted as [[`Mem "foo"; `Nth 0; `End]].
243243+ For example, [/foo/0] is formatted as [[Mem "foo"; Nth 0]].
244244+ Append pointers show [/-] at the end.
195245 Useful for debugging and understanding pointer structure. *)
196246197247(** {2:comparison Comparison} *)
198248199199-val equal : t -> t -> bool
200200-(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *)
249249+val equal : _ t -> _ t -> bool
250250+(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices
251251+ and the same append status. *)
201252202202-val compare : t -> t -> int
253253+val compare : _ t -> _ t -> int
203254(** [compare p1 p2] is a total order on pointers, comparing indices
204204- lexicographically. *)
255255+ lexicographically. Append pointers sort after nav pointers with
256256+ the same prefix. *)
205257206258(** {2:jsont_path Conversion with Jsont.Path} *)
207259208208-val of_path : Jsont.Path.t -> t
209209-(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *)
260260+val of_path : Jsont.Path.t -> nav t
261261+(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer.
262262+ Always returns a {!nav} pointer since {!Jsont.Path} has no [-] concept. *)
210263211211-val to_path : t -> Jsont.Path.t option
212212-(** [to_path p] converts to a {!Jsont.Path.t}.
213213- Returns [None] if [p] contains an [`End] index. *)
214214-215215-val to_path_exn : t -> Jsont.Path.t
216216-(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error}
217217- if conversion fails. *)
264264+val to_path : nav t -> Jsont.Path.t
265265+(** [to_path p] converts a navigation pointer to a {!Jsont.Path.t}. *)
218266219267(** {1 Evaluation}
220268221269 These functions evaluate a JSON Pointer against a {!Jsont.json} value
222222- to retrieve the referenced value. *)
270270+ to retrieve the referenced value. They only accept {!nav} pointers
271271+ since {!append} pointers refer to nonexistent positions. *)
223272224224-val get : t -> Jsont.json -> Jsont.json
273273+val get : nav t -> Jsont.json -> Jsont.json
225274(** [get p json] retrieves the value at pointer [p] in [json].
226275227276 @raise Jsont.Error if:
228277 - The pointer references a nonexistent object member
229278 - The pointer references an out-of-bounds array index
230230- - The pointer contains [`End] (since [-] always refers
231231- to a nonexistent element)
232232- - An index type doesn't match the JSON value (e.g., [`Nth]
279279+ - An index type doesn't match the JSON value (e.g., [Nth]
233280 on an object) *)
234281235235-val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
282282+val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
236283(** [get_result p json] is like {!get} but returns a result. *)
237284238238-val find : t -> Jsont.json -> Jsont.json option
285285+val find : nav t -> Jsont.json -> Jsont.json option
239286(** [find p json] is like {!get} but returns [None] instead of
240287 raising when the pointer doesn't resolve to a value. *)
241288···247294 operations.
248295249296 All mutation functions return a new JSON value with the modification
250250- applied; they do not mutate the input. *)
297297+ applied; they do not mutate the input.
298298+299299+ Functions that support the [-] token ({!add}, {!set}) accept any
300300+ pointer type ([_ t]). Functions that require an existing element
301301+ ({!remove}, {!replace}) only accept {!nav} pointers. *)
251302252252-val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json
303303+val set : _ t -> Jsont.json -> value:Jsont.json -> Jsont.json
253304(** [set p json ~value] replaces the value at pointer [p] with [value].
254305255255- For [`End] on arrays, appends [value] to the end of the array.
306306+ For {!append} pointers, appends [value] to the end of the array.
256307257308 @raise Jsont.Error if the pointer doesn't resolve to an existing
258258- location (except for [`End] on arrays). *)
309309+ location (except for {!append} pointers on arrays). *)
259310260260-val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
311311+val add : _ t -> Jsont.json -> value:Jsont.json -> Jsont.json
261312(** [add p json ~value] adds [value] at the location specified by [p].
262313263314 The behavior depends on the target:
264315 {ul
265316 {- For objects: If the member exists, it is replaced. If it doesn't
266317 exist, a new member is added.}
267267- {- For arrays with [`Nth]: Inserts [value] {e before} the
318318+ {- For arrays with [Nth]: Inserts [value] {e before} the
268319 specified index, shifting subsequent elements. The index must be
269320 valid (0 to length inclusive).}
270270- {- For arrays with [`End]: Appends [value] to the array.}}
321321+ {- For {!append} pointers: Appends [value] to the array.}}
271322272323 @raise Jsont.Error if:
273324 - The parent of the target location doesn't exist
274274- - An array index is out of bounds (except for [`End])
325325+ - An array index is out of bounds (except for {!append} pointers)
275326 - The parent is not an object or array *)
276327277277-val remove : t -> Jsont.json -> Jsont.json
328328+val remove : nav t -> Jsont.json -> Jsont.json
278329(** [remove p json] removes the value at pointer [p].
279330280331 For objects, removes the member. For arrays, removes the element
···282333283334 @raise Jsont.Error if:
284335 - [p] is the root (cannot remove the root)
285285- - The pointer doesn't resolve to an existing value
286286- - The pointer contains [`End] *)
336336+ - The pointer doesn't resolve to an existing value *)
287337288288-val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json
338338+val replace : nav t -> Jsont.json -> value:Jsont.json -> Jsont.json
289339(** [replace p json ~value] replaces the value at pointer [p] with [value].
290340291341 Unlike {!add}, this requires the target to exist.
292342293293- @raise Jsont.Error if:
294294- - The pointer doesn't resolve to an existing value
295295- - The pointer contains [`End] *)
343343+ @raise Jsont.Error if the pointer doesn't resolve to an existing value. *)
296344297297-val move : from:t -> path:t -> Jsont.json -> Jsont.json
345345+val move : from:nav t -> path:_ t -> Jsont.json -> Jsont.json
298346(** [move ~from ~path json] moves the value from [from] to [path].
299347300348 This is equivalent to {!remove} at [from] followed by {!add}
···302350303351 @raise Jsont.Error if:
304352 - [from] doesn't resolve to a value
305305- - [path] is a proper prefix of [from] (would create a cycle)
306306- - Either pointer contains [`End] *)
353353+ - [path] is a proper prefix of [from] (would create a cycle) *)
307354308308-val copy : from:t -> path:t -> Jsont.json -> Jsont.json
355355+val copy : from:nav t -> path:_ t -> Jsont.json -> Jsont.json
309356(** [copy ~from ~path json] copies the value from [from] to [path].
310357311358 This is equivalent to {!get} at [from] followed by {!add}
312359 at [path] with the retrieved value.
313360314314- @raise Jsont.Error if:
315315- - [from] doesn't resolve to a value
316316- - Either pointer contains [`End] *)
361361+ @raise Jsont.Error if [from] doesn't resolve to a value. *)
317362318318-val test : t -> Jsont.json -> expected:Jsont.json -> bool
363363+val test : nav t -> Jsont.json -> expected:Jsont.json -> bool
319364(** [test p json ~expected] tests if the value at [p] equals [expected].
320365321366 Returns [true] if the values are equal according to {!Jsont.Json.equal},
···329374 These types and functions integrate JSON Pointers with the {!Jsont}
330375 codec system. *)
331376332332-val jsont : t Jsont.t
377377+val jsont : [ `Nav of nav t | `Append of append t ] Jsont.t
333378(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
334379335380 On decode, parses a JSON string as a JSON Pointer using {!of_string}.
336381 On encode, serializes a pointer to a JSON string using {!to_string}. *)
337382338338-val jsont_uri_fragment : t Jsont.t
383383+val jsont_nav : nav t Jsont.t
384384+(** [jsont_nav] is a {!Jsont.t} codec for navigation JSON Pointers.
385385+386386+ On decode, parses using {!of_string_nav} (fails on [-]).
387387+ On encode, serializes using {!to_string}. *)
388388+389389+val jsont_uri_fragment : [ `Nav of nav t | `Append of append t ] Jsont.t
339390(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
340391341392 On decode, parses using {!of_uri_fragment}.
···346397 These combinators integrate with jsont's query system, allowing
347398 JSON Pointers to be used with jsont codecs for typed access. *)
348399349349-val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t
400400+val path : ?absent:'a -> nav t -> 'a Jsont.t -> 'a Jsont.t
350401(** [path p t] decodes the value at pointer [p] using codec [t].
351402352403 If [absent] is provided and the pointer doesn't resolve, returns
···354405355406 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
356407357357-val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t
408408+val set_path : ?allow_absent:bool -> 'a Jsont.t -> _ t -> 'a -> Jsont.json Jsont.t
358409(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
359410360411 If [allow_absent] is [true] (default [false]), creates missing
···362413363414 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
364415365365-val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t
416416+val update_path : ?absent:'a -> nav t -> 'a Jsont.t -> Jsont.json Jsont.t
366417(** [update_path p t] recodes the value at pointer [p] with codec [t].
367418368419 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
369420370370-val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t
421421+val delete_path : ?allow_absent:bool -> nav t -> Jsont.json Jsont.t
371422(** [delete_path p] removes the value at pointer [p].
372423373424 If [allow_absent] is [true] (default [false]), does nothing if
+4-4
test/comprehensive.t
···44 $ ./test_pointer.exe parse "/foo/bar"
55 OK: [Mem:foo, Mem:bar]
66 $ ./test_pointer.exe parse "/foo/-"
77- OK: [Mem:foo, End]
77+ OK: [Mem:foo, /-]
88 $ ./test_pointer.exe parse "/foo/1"
99 OK: [Mem:foo, Nth:1]
1010 $ ./test_pointer.exe parse "/foo/~0"
···144144 $ ./test_pointer.exe has 'null' ''
145145 true
146146147147-Has with '-' (end marker - should be false for get, points to nonexistent):
147147+Has with '-' (end marker - now errors because has uses of_string_nav):
148148 $ ./test_pointer.exe has '["foo"]' '/-'
149149- false
149149+ ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
150150 $ ./test_pointer.exe has '[]' '/-'
151151- false
151151+ ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
+1-2
test/eval.t
···65656666Error: end marker not allowed in get:
6767 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-"
6868- ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element
6969- File "-":
6868+ ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
70697170Error: navigating through primitive (string):
7271 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0"
···1717 | Ok s -> s
1818 | Error e -> failwith e
19192020+(* Helper to get indices from either nav or append pointer *)
2121+let indices_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
2222+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
2323+ match result with
2424+ | `Nav p -> Jsont_pointer.indices p
2525+ | `Append p -> Jsont_pointer.indices p
2626+2727+(* Helper to convert to string from either nav or append pointer *)
2828+let to_string_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t
2929+ | `Append of Jsont_pointer.append Jsont_pointer.t ]) =
3030+ match result with
3131+ | `Nav p -> Jsont_pointer.to_string p
3232+ | `Append p -> Jsont_pointer.to_string p
3333+2034(* Test: parse pointer and print indices *)
2135let test_parse pointer_str =
2236 try
2323- let p = Jsont_pointer.of_string pointer_str in
2424- let indices = Jsont_pointer.indices p in
3737+ let result = Jsont_pointer.of_string pointer_str in
3838+ let indices = indices_of_result result in
2539 let index_strs = List.map (fun idx ->
2640 match idx with
2727- | `Mem s -> Printf.sprintf "Mem:%s" s
2828- | `Nth n -> Printf.sprintf "Nth:%d" n
2929- | `End -> "End"
4141+ | Jsont.Path.Mem (s, _) -> Printf.sprintf "Mem:%s" s
4242+ | Jsont.Path.Nth (n, _) -> Printf.sprintf "Nth:%d" n
3043 ) indices in
3131- Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
4444+ let suffix = match result with `Nav _ -> "" | `Append _ -> ", /-" in
4545+ Printf.printf "OK: [%s%s]\n" (String.concat ", " index_strs) suffix
3246 with Jsont.Error e ->
3347 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
34483549(* Test: roundtrip pointer string *)
3650let test_roundtrip pointer_str =
3751 try
3838- let p = Jsont_pointer.of_string pointer_str in
3939- let s = Jsont_pointer.to_string p in
5252+ let result = Jsont_pointer.of_string pointer_str in
5353+ let s = to_string_of_result result in
4054 if s = pointer_str then
4155 Printf.printf "OK: %s\n" s
4256 else
···4862let test_eval json_path pointer_str =
4963 try
5064 let json = parse_json (read_file json_path) in
5151- let p = Jsont_pointer.of_string pointer_str in
6565+ let p = Jsont_pointer.of_string_nav pointer_str in
5266 let result = Jsont_pointer.get p json in
5367 Printf.printf "OK: %s\n" (json_to_string result)
5468 with
···7387(* Test: URI fragment roundtrip *)
7488let test_uri_fragment pointer_str =
7589 try
7676- let p = Jsont_pointer.of_string pointer_str in
7777- let frag = Jsont_pointer.to_uri_fragment p in
7878- let p2 = Jsont_pointer.of_uri_fragment frag in
7979- let s2 = Jsont_pointer.to_string p2 in
9090+ let result = Jsont_pointer.of_string pointer_str in
9191+ let frag = match result with
9292+ | `Nav p -> Jsont_pointer.to_uri_fragment p
9393+ | `Append p -> Jsont_pointer.to_uri_fragment p
9494+ in
9595+ let result2 = Jsont_pointer.of_uri_fragment frag in
9696+ let s2 = to_string_of_result result2 in
8097 if s2 = pointer_str then
8198 Printf.printf "OK: %s -> %s\n" pointer_str frag
8299 else
···88105let test_add json_str pointer_str value_str =
89106 try
90107 let json = parse_json json_str in
9191- let p = Jsont_pointer.of_string pointer_str in
92108 let value = parse_json value_str in
9393- let result = Jsont_pointer.add p json ~value in
109109+ let result = match Jsont_pointer.of_string pointer_str with
110110+ | `Nav p -> Jsont_pointer.add p json ~value
111111+ | `Append p -> Jsont_pointer.add p json ~value
112112+ in
94113 Printf.printf "%s\n" (json_to_string result)
95114 with Jsont.Error e ->
96115 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···99118let test_remove json_str pointer_str =
100119 try
101120 let json = parse_json json_str in
102102- let p = Jsont_pointer.of_string pointer_str in
121121+ let p = Jsont_pointer.of_string_nav pointer_str in
103122 let result = Jsont_pointer.remove p json in
104123 Printf.printf "%s\n" (json_to_string result)
105124 with Jsont.Error e ->
···109128let test_replace json_str pointer_str value_str =
110129 try
111130 let json = parse_json json_str in
112112- let p = Jsont_pointer.of_string pointer_str in
131131+ let p = Jsont_pointer.of_string_nav pointer_str in
113132 let value = parse_json value_str in
114133 let result = Jsont_pointer.replace p json ~value in
115134 Printf.printf "%s\n" (json_to_string result)
···120139let test_move json_str from_str path_str =
121140 try
122141 let json = parse_json json_str in
123123- let from = Jsont_pointer.of_string from_str in
124124- let path = Jsont_pointer.of_string path_str in
125125- let result = Jsont_pointer.move ~from ~path json in
142142+ let from = Jsont_pointer.of_string_nav from_str in
143143+ let result = match Jsont_pointer.of_string path_str with
144144+ | `Nav path -> Jsont_pointer.move ~from ~path json
145145+ | `Append path -> Jsont_pointer.move ~from ~path json
146146+ in
126147 Printf.printf "%s\n" (json_to_string result)
127148 with Jsont.Error e ->
128149 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···131152let test_copy json_str from_str path_str =
132153 try
133154 let json = parse_json json_str in
134134- let from = Jsont_pointer.of_string from_str in
135135- let path = Jsont_pointer.of_string path_str in
136136- let result = Jsont_pointer.copy ~from ~path json in
155155+ let from = Jsont_pointer.of_string_nav from_str in
156156+ let result = match Jsont_pointer.of_string path_str with
157157+ | `Nav path -> Jsont_pointer.copy ~from ~path json
158158+ | `Append path -> Jsont_pointer.copy ~from ~path json
159159+ in
137160 Printf.printf "%s\n" (json_to_string result)
138161 with Jsont.Error e ->
139162 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
···142165let test_test json_str pointer_str expected_str =
143166 try
144167 let json = parse_json json_str in
145145- let p = Jsont_pointer.of_string pointer_str in
168168+ let p = Jsont_pointer.of_string_nav pointer_str in
146169 let expected = parse_json expected_str in
147170 let result = Jsont_pointer.test p json ~expected in
148171 Printf.printf "%b\n" result
···153176let test_has json_str pointer_str =
154177 try
155178 let json = parse_json json_str in
156156- let p = Jsont_pointer.of_string pointer_str in
179179+ let p = Jsont_pointer.of_string_nav pointer_str in
157180 let result = Jsont_pointer.find p json in
158181 Printf.printf "%b\n" (Option.is_some result)
159182 with Jsont.Error e ->