···2424 let indices = Jsont_pointer.indices p in
2525 let index_strs = List.map (fun idx ->
2626 match idx with
2727- | Jsont_pointer.Index.Mem s -> Printf.sprintf "Mem:%s" s
2828- | Jsont_pointer.Index.Nth n -> Printf.sprintf "Nth:%d" n
2929- | Jsont_pointer.Index.End -> "End"
2727+ | `Mem s -> Printf.sprintf "Mem:%s" s
2828+ | `Nth n -> Printf.sprintf "Nth:%d" n
2929+ | `End -> "End"
3030 ) indices in
3131 Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
3232 with Jsont.Error e ->
···44[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
55the `jsont-pointer` OCaml library through interactive examples.
6677+## Setup
88+99+First, let's set up our environment with helper functions:
1010+1111+```ocaml
1212+# open Jsont_pointer;;
1313+# let parse_json s =
1414+ match Jsont_bytesrw.decode_string Jsont.json s with
1515+ | Ok json -> json
1616+ | Error e -> failwith e;;
1717+val parse_json : string -> Jsont.json = <fun>
1818+# let json_to_string json =
1919+ match Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json with
2020+ | Ok s -> s
2121+ | Error e -> failwith e;;
2222+val json_to_string : Jsont.json -> string = <fun>
2323+# let show_pointer p =
2424+ let idxs = indices p in
2525+ let show_index = function
2626+ | `Mem s -> "Mem:" ^ s
2727+ | `Nth n -> "Nth:" ^ string_of_int n
2828+ | `End -> "End"
2929+ in
3030+ "[" ^ String.concat ", " (List.map show_index idxs) ^ "]";;
3131+val show_pointer : t -> string = <fun>
3232+```
3333+734## What is JSON Pointer?
835936From RFC 6901, Section 1:
···17441845For example, given this JSON document:
19462020-```json
2121-{
2222- "users": [
2323- {"name": "Alice", "age": 30},
2424- {"name": "Bob", "age": 25}
2525- ]
2626-}
4747+```ocaml
4848+# let users_json = parse_json {|{
4949+ "users": [
5050+ {"name": "Alice", "age": 30},
5151+ {"name": "Bob", "age": 25}
5252+ ]
5353+ }|};;
5454+val users_json : Jsont.json =
5555+ Jsont.Object
5656+ ([(("users", <abstr>),
5757+ Jsont.Array
5858+ ([Jsont.Object
5959+ ([(("name", <abstr>), Jsont.String ("Alice", <abstr>));
6060+ (("age", <abstr>), Jsont.Number (30., <abstr>))],
6161+ <abstr>);
6262+ Jsont.Object
6363+ ([(("name", <abstr>), Jsont.String ("Bob", <abstr>));
6464+ (("age", <abstr>), Jsont.Number (25., <abstr>))],
6565+ <abstr>)],
6666+ <abstr>))],
6767+ <abstr>)
2768```
28692929-The JSON Pointer `/users/0/name` refers to the string `"Alice"`.
7070+The JSON Pointer `/users/0/name` refers to the string `"Alice"`:
7171+7272+```ocaml
7373+# let ptr = of_string "/users/0/name";;
7474+val ptr : t = <abstr>
7575+# get ptr users_json;;
7676+- : Jsont.json = Jsont.String ("Alice", <abstr>)
7777+```
30783179In OCaml, this is represented by the `Jsont_pointer.t` type - a sequence
3280of navigation steps from the document root to a target value.
···5098- Every non-empty pointer starts with `/`
5199- Everything between `/` characters is a "reference token"
521005353-Let's see this in action. We can parse pointers and see their structure:
101101+Let's see this in action:
541025555-```sh
5656-$ jsonpp parse ""
5757-OK: []
103103+```ocaml
104104+# let empty_ptr = of_string "";;
105105+val empty_ptr : t = <abstr>
106106+# show_pointer empty_ptr;;
107107+- : string = "[]"
58108```
5910960110The empty pointer has no reference tokens - it points to the root.
611116262-```sh
6363-$ jsonpp parse "/foo"
6464-OK: [Mem:foo]
112112+```ocaml
113113+# let foo_ptr = of_string "/foo";;
114114+val foo_ptr : t = <abstr>
115115+# show_pointer foo_ptr;;
116116+- : string = "[Mem:foo]"
65117```
6611867119The pointer `/foo` has one token: `foo`. Since it's not a number, it's
68120interpreted as an object member name (`Mem`).
691217070-```sh
7171-$ jsonpp parse "/foo/0"
7272-OK: [Mem:foo, Nth:0]
122122+```ocaml
123123+# let foo_0_ptr = of_string "/foo/0";;
124124+val foo_0_ptr : t = <abstr>
125125+# show_pointer foo_0_ptr;;
126126+- : string = "[Mem:foo, Nth:0]"
73127```
7412875129Here we have two tokens: `foo` (a member name) and `0` (interpreted as
76130an array index `Nth`).
771317878-```sh
7979-$ jsonpp parse "/foo/bar/baz"
8080-OK: [Mem:foo, Mem:bar, Mem:baz]
132132+```ocaml
133133+# show_pointer (of_string "/foo/bar/baz");;
134134+- : string = "[Mem:foo, Mem:bar, Mem:baz]"
81135```
8213683137Multiple tokens navigate deeper into nested structures.
···8814289143<!-- $MDX skip -->
90144```ocaml
9191-type t =
9292- | Mem of string (* Object member access *)
9393- | Nth of int (* Array index access *)
9494- | End (* The special "-" marker for append operations *)
145145+type t = [
146146+ | `Mem of string (* Object member access *)
147147+ | `Nth of int (* Array index access *)
148148+ | `End (* The special "-" marker for append operations *)
149149+]
95150```
9615197152The `Mem` variant holds the **unescaped** member name - you work with the
···102157103158What happens if a pointer doesn't start with `/`?
104159105105-```sh
106106-$ jsonpp parse "foo"
107107-ERROR: Invalid JSON Pointer: must be empty or start with '/': foo
160160+```ocaml
161161+# of_string "foo";;
162162+Exception: Jsont.Error ([], <abstr>, <abstr>).
108163```
109164110165The RFC is strict: non-empty pointers MUST start with `/`.
111166167167+For safer parsing, use `of_string_result`:
168168+169169+```ocaml
170170+# of_string_result "foo";;
171171+- : (t, string) result =
172172+Error "Invalid JSON Pointer: must be empty or start with '/': foo"
173173+# of_string_result "/valid";;
174174+- : (t, string) result = Ok <abstr>
175175+```
176176+112177## Evaluation: Navigating JSON
113178114179Now we come to the heart of JSON Pointer: evaluation. RFC 6901, Section 4
···119184> the document. Each reference token in the JSON Pointer is evaluated
120185> sequentially.
121186122122-In the library, this is the `Jsont_pointer.get` function:
187187+Let's use the example JSON document from RFC 6901, Section 5:
123188124124-<!-- $MDX skip -->
125189```ocaml
126126-val get : t -> Jsont.json -> Jsont.json
127127-```
128128-129129-Let's use the example JSON document from RFC 6901, Section 5:
130130-131131-```sh
132132-$ cat rfc6901_example.json
133133-{
134134- "foo": ["bar", "baz"],
135135- "": 0,
136136- "a/b": 1,
137137- "c%d": 2,
138138- "e^f": 3,
139139- "g|h": 4,
140140- "i\\j": 5,
141141- "k\"l": 6,
142142- " ": 7,
143143- "m~n": 8
144144-}
190190+# let rfc_example = parse_json {|{
191191+ "foo": ["bar", "baz"],
192192+ "": 0,
193193+ "a/b": 1,
194194+ "c%d": 2,
195195+ "e^f": 3,
196196+ "g|h": 4,
197197+ "i\\j": 5,
198198+ "k\"l": 6,
199199+ " ": 7,
200200+ "m~n": 8
201201+ }|};;
202202+val rfc_example : Jsont.json =
203203+ Jsont.Object
204204+ ([(("foo", <abstr>),
205205+ Jsont.Array
206206+ ([Jsont.String ("bar", <abstr>); Jsont.String ("baz", <abstr>)],
207207+ <abstr>));
208208+ (("", <abstr>), Jsont.Number (0., <abstr>));
209209+ (("a/b", <abstr>), Jsont.Number (1., <abstr>));
210210+ (("c%d", <abstr>), Jsont.Number (2., <abstr>));
211211+ (("e^f", <abstr>), Jsont.Number (3., <abstr>));
212212+ (("g|h", <abstr>), Jsont.Number (4., <abstr>));
213213+ (("i\\j", <abstr>), Jsont.Number (5., <abstr>));
214214+ (("k\"l", <abstr>), Jsont.Number (6., <abstr>));
215215+ ((" ", <abstr>), Jsont.Number (7., <abstr>));
216216+ (("m~n", <abstr>), Jsont.Number (8., <abstr>))],
217217+ <abstr>)
145218```
146219147220This document is carefully constructed to exercise various edge cases!
148221149222### The Root Pointer
150223151151-```sh
152152-$ jsonpp eval rfc6901_example.json ""
153153-OK: {"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8}
154154-```
155155-156156-The empty pointer returns the whole document. In OCaml, this is
157157-`Jsont_pointer.root`:
158158-159159-<!-- $MDX skip -->
160224```ocaml
161161-val root : t
162162-(** The empty pointer that references the whole document. *)
225225+# get root rfc_example |> json_to_string;;
226226+- : string =
227227+"{\"foo\":[\"bar\",\"baz\"],\"\":0,\"a/b\":1,\"c%d\":2,\"e^f\":3,\"g|h\":4,\"i\\\\j\":5,\"k\\\"l\":6,\" \":7,\"m~n\":8}"
163228```
164229230230+The empty pointer (`root`) returns the whole document.
231231+165232### Object Member Access
166233167167-```sh
168168-$ jsonpp eval rfc6901_example.json "/foo"
169169-OK: ["bar","baz"]
234234+```ocaml
235235+# get (of_string "/foo") rfc_example |> json_to_string;;
236236+- : string = "[\"bar\",\"baz\"]"
170237```
171238172239`/foo` accesses the member named `foo`, which is an array.
173240174241### Array Index Access
175242176176-```sh
177177-$ jsonpp eval rfc6901_example.json "/foo/0"
178178-OK: "bar"
243243+```ocaml
244244+# get (of_string "/foo/0") rfc_example |> json_to_string;;
245245+- : string = "\"bar\""
246246+# get (of_string "/foo/1") rfc_example |> json_to_string;;
247247+- : string = "\"baz\""
179248```
180249181250`/foo/0` first goes to `foo`, then accesses index 0 of the array.
182251183183-```sh
184184-$ jsonpp eval rfc6901_example.json "/foo/1"
185185-OK: "baz"
186186-```
187187-188188-Index 1 gives us the second element.
189189-190252### Empty String as Key
191253192254JSON allows empty strings as object keys:
193255194194-```sh
195195-$ jsonpp eval rfc6901_example.json "/"
196196-OK: 0
256256+```ocaml
257257+# get (of_string "/") rfc_example |> json_to_string;;
258258+- : string = "0"
197259```
198260199261The pointer `/` has one token: the empty string. This accesses the member
···203265204266The RFC example includes keys with `/` and `~` characters:
205267206206-```sh
207207-$ jsonpp eval rfc6901_example.json "/a~1b"
208208-OK: 1
268268+```ocaml
269269+# get (of_string "/a~1b") rfc_example |> json_to_string;;
270270+- : string = "1"
209271```
210272211273The token `a~1b` refers to the key `a/b`. We'll explain this escaping
212274[below](#escaping-special-characters).
213275214214-```sh
215215-$ jsonpp eval rfc6901_example.json "/m~0n"
216216-OK: 8
276276+```ocaml
277277+# get (of_string "/m~0n") rfc_example |> json_to_string;;
278278+- : string = "8"
217279```
218280219281The token `m~0n` refers to the key `m~n`.
220282221283**Important**: When using the OCaml library programmatically, you don't need
222222-to worry about escaping. The `Index.Mem` variant holds the literal key name:
284284+to worry about escaping. The `Mem` variant holds the literal key name:
223285224224-<!-- $MDX skip -->
225286```ocaml
226226-(* To access the key "a/b", just use the literal string *)
227227-let pointer = Jsont_pointer.make [Mem "a/b"]
287287+# let slash_ptr = make [`Mem "a/b"];;
288288+val slash_ptr : t = <abstr>
289289+# to_string slash_ptr;;
290290+- : string = "/a~1b"
291291+# get slash_ptr rfc_example |> json_to_string;;
292292+- : string = "1"
293293+```
228294229229-(* The library escapes it when converting to string *)
230230-let s = Jsont_pointer.to_string pointer (* "/a~1b" *)
231231-```
295295+The library escapes it when converting to string.
232296233297### Other Special Characters (No Escaping Needed)
234298235299Most characters don't need escaping in JSON Pointer strings:
236300237237-```sh
238238-$ jsonpp eval rfc6901_example.json "/c%d"
239239-OK: 2
240240-```
241241-242242-```sh
243243-$ jsonpp eval rfc6901_example.json "/e^f"
244244-OK: 3
245245-```
246246-247247-```sh
248248-$ jsonpp eval rfc6901_example.json "/g|h"
249249-OK: 4
250250-```
251251-252252-```sh
253253-$ jsonpp eval rfc6901_example.json "/ "
254254-OK: 7
301301+```ocaml
302302+# get (of_string "/c%d") rfc_example |> json_to_string;;
303303+- : string = "2"
304304+# get (of_string "/e^f") rfc_example |> json_to_string;;
305305+- : string = "3"
306306+# get (of_string "/g|h") rfc_example |> json_to_string;;
307307+- : string = "4"
308308+# get (of_string "/ ") rfc_example |> json_to_string;;
309309+- : string = "7"
255310```
256311257312Even a space is a valid key character!
···260315261316What happens when we try to access something that doesn't exist?
262317263263-```sh
264264-$ jsonpp eval rfc6901_example.json "/nonexistent"
265265-ERROR: JSON Pointer: member 'nonexistent' not found
266266-File "-":
318318+```ocaml
319319+# get_result (of_string "/nonexistent") rfc_example;;
320320+- : (Jsont.json, Jsont.Error.t) result = Error ([], <abstr>, <abstr>)
321321+# find (of_string "/nonexistent") rfc_example;;
322322+- : Jsont.json option = None
267323```
268324269325Or an out-of-bounds array index:
270326271271-```sh
272272-$ jsonpp eval rfc6901_example.json "/foo/99"
273273-ERROR: JSON Pointer: index 99 out of bounds (array has 2 elements)
274274-File "-":
327327+```ocaml
328328+# find (of_string "/foo/99") rfc_example;;
329329+- : Jsont.json option = None
275330```
276331277332Or try to index into a non-container:
278333279279-```sh
280280-$ jsonpp eval rfc6901_example.json "/foo/0/invalid"
281281-ERROR: JSON Pointer: cannot index into string with 'invalid'
282282-File "-":
334334+```ocaml
335335+# find (of_string "/foo/0/invalid") rfc_example;;
336336+- : Jsont.json option = None
283337```
284338285339The library provides both exception-raising and result-returning variants:
···303357304358> note that leading zeros are not allowed
305359306306-```sh
307307-$ jsonpp parse "/foo/0"
308308-OK: [Mem:foo, Nth:0]
360360+```ocaml
361361+# show_pointer (of_string "/foo/0");;
362362+- : string = "[Mem:foo, Nth:0]"
309363```
310364311365Zero itself is fine.
312366313313-```sh
314314-$ jsonpp parse "/foo/01"
315315-OK: [Mem:foo, Mem:01]
367367+```ocaml
368368+# show_pointer (of_string "/foo/01");;
369369+- : string = "[Mem:foo, Mem:01]"
316370```
317371318372But `01` has a leading zero, so it's NOT treated as an array index - it
···329383This is primarily useful for JSON Patch operations (RFC 6902). Let's see
330384how it parses:
331385332332-```sh
333333-$ jsonpp parse "/foo/-"
334334-OK: [Mem:foo, End]
386386+```ocaml
387387+# show_pointer (of_string "/foo/-");;
388388+- : string = "[Mem:foo, End]"
335389```
336390337391The `-` is recognized as a special `End` index.
···339393However, you cannot evaluate a pointer containing `-` because it refers
340394to a position that doesn't exist:
341395342342-```sh
343343-$ jsonpp eval rfc6901_example.json "/foo/-"
344344-ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element
345345-File "-":
396396+```ocaml
397397+# find (of_string "/foo/-") rfc_example;;
398398+- : Jsont.json option = None
346399```
347400348401The RFC explains this:
···363416364417The `add` operation inserts a value at a location:
365418366366-```sh
367367-$ jsonpp add '{"foo":"bar"}' '/baz' '"qux"'
368368-{"foo":"bar","baz":"qux"}
369369-```
370370-371371-In OCaml:
372372-373373-<!-- $MDX skip -->
374419```ocaml
375375-val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
420420+# let obj = parse_json {|{"foo":"bar"}|};;
421421+val obj : Jsont.json =
422422+ Jsont.Object ([(("foo", <abstr>), Jsont.String ("bar", <abstr>))], <abstr>)
423423+# add (of_string "/baz") obj ~value:(Jsont.Json.string "qux")
424424+ |> json_to_string;;
425425+- : string = "{\"foo\":\"bar\",\"baz\":\"qux\"}"
376426```
377427378428For arrays, `add` inserts BEFORE the specified index:
379429380380-```sh
381381-$ jsonpp add '{"foo":["a","b"]}' '/foo/1' '"X"'
382382-{"foo":["a","X","b"]}
430430+```ocaml
431431+# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
432432+val arr_obj : Jsont.json =
433433+ Jsont.Object
434434+ ([(("foo", <abstr>),
435435+ Jsont.Array
436436+ ([Jsont.String ("a", <abstr>); Jsont.String ("b", <abstr>)], <abstr>))],
437437+ <abstr>)
438438+# add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
439439+ |> json_to_string;;
440440+- : string = "{\"foo\":[\"a\",\"X\",\"b\"]}"
383441```
384442385443This is where the `-` marker shines - it appends to the end:
386444387387-```sh
388388-$ jsonpp add '{"foo":["a","b"]}' '/foo/-' '"c"'
389389-{"foo":["a","b","c"]}
445445+```ocaml
446446+# add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c")
447447+ |> json_to_string;;
448448+- : string = "{\"foo\":[\"a\",\"b\",\"c\"]}"
390449```
391450392451### Remove
393452394453The `remove` operation deletes a value:
395454396396-```sh
397397-$ jsonpp remove '{"foo":"bar","baz":"qux"}' '/baz'
398398-{"foo":"bar"}
455455+```ocaml
456456+# let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};;
457457+val two_fields : Jsont.json =
458458+ Jsont.Object
459459+ ([(("foo", <abstr>), Jsont.String ("bar", <abstr>));
460460+ (("baz", <abstr>), Jsont.String ("qux", <abstr>))],
461461+ <abstr>)
462462+# remove (of_string "/baz") two_fields |> json_to_string;;
463463+- : string = "{\"foo\":\"bar\"}"
399464```
400465401466For arrays, it removes and shifts:
402467403403-```sh
404404-$ jsonpp remove '{"foo":["a","b","c"]}' '/foo/1'
405405-{"foo":["a","c"]}
468468+```ocaml
469469+# let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
470470+val three_elem : Jsont.json =
471471+ Jsont.Object
472472+ ([(("foo", <abstr>),
473473+ Jsont.Array
474474+ ([Jsont.String ("a", <abstr>); Jsont.String ("b", <abstr>);
475475+ Jsont.String ("c", <abstr>)],
476476+ <abstr>))],
477477+ <abstr>)
478478+# remove (of_string "/foo/1") three_elem |> json_to_string;;
479479+- : string = "{\"foo\":[\"a\",\"c\"]}"
406480```
407481408482### Replace
409483410484The `replace` operation updates an existing value:
411485412412-```sh
413413-$ jsonpp replace '{"foo":"bar"}' '/foo' '"baz"'
414414-{"foo":"baz"}
486486+```ocaml
487487+# replace (of_string "/foo") obj ~value:(Jsont.Json.string "baz")
488488+ |> json_to_string;;
489489+- : string = "{\"foo\":\"baz\"}"
415490```
416491417417-Unlike `add`, `replace` requires the target to already exist:
418418-419419-```sh
420420-$ jsonpp replace '{"foo":"bar"}' '/nonexistent' '"value"'
421421-ERROR: JSON Pointer: member 'nonexistent' not found
422422-File "-":
423423-```
492492+Unlike `add`, `replace` requires the target to already exist.
493493+Attempting to replace a nonexistent path raises an error.
424494425495### Move
426496427497The `move` operation relocates a value:
428498429429-```sh
430430-$ jsonpp move '{"foo":{"bar":"baz"},"qux":{}}' '/foo/bar' '/qux/thud'
431431-{"foo":{},"qux":{"thud":"baz"}}
499499+```ocaml
500500+# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
501501+val nested : Jsont.json =
502502+ Jsont.Object
503503+ ([(("foo", <abstr>),
504504+ Jsont.Object
505505+ ([(("bar", <abstr>), Jsont.String ("baz", <abstr>))], <abstr>));
506506+ (("qux", <abstr>), Jsont.Object ([], <abstr>))],
507507+ <abstr>)
508508+# move ~from:(of_string "/foo/bar") ~path:(of_string "/qux/thud") nested
509509+ |> json_to_string;;
510510+- : string = "{\"foo\":{},\"qux\":{\"thud\":\"baz\"}}"
432511```
433512434513### Copy
435514436515The `copy` operation duplicates a value:
437516438438-```sh
439439-$ jsonpp copy '{"foo":{"bar":"baz"}}' '/foo/bar' '/foo/qux'
440440-{"foo":{"bar":"baz","qux":"baz"}}
517517+```ocaml
518518+# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
519519+val to_copy : Jsont.json =
520520+ Jsont.Object
521521+ ([(("foo", <abstr>),
522522+ Jsont.Object
523523+ ([(("bar", <abstr>), Jsont.String ("baz", <abstr>))], <abstr>))],
524524+ <abstr>)
525525+# copy ~from:(of_string "/foo/bar") ~path:(of_string "/foo/qux") to_copy
526526+ |> json_to_string;;
527527+- : string = "{\"foo\":{\"bar\":\"baz\",\"qux\":\"baz\"}}"
441528```
442529443530### Test
444531445532The `test` operation verifies a value (useful in JSON Patch):
446533447447-```sh
448448-$ jsonpp test '{"foo":"bar"}' '/foo' '"bar"'
449449-true
450450-```
451451-452452-```sh
453453-$ jsonpp test '{"foo":"bar"}' '/foo' '"baz"'
454454-false
534534+```ocaml
535535+# test (of_string "/foo") obj ~expected:(Jsont.Json.string "bar");;
536536+- : bool = true
537537+# test (of_string "/foo") obj ~expected:(Jsont.Json.string "wrong");;
538538+- : bool = false
455539```
456540457541## Escaping Special Characters
···473557### The Library Handles Escaping Automatically
474558475559**Important**: When using `jsont-pointer` programmatically, you rarely need
476476-to think about escaping. The `Index.Mem` variant stores unescaped strings,
560560+to think about escaping. The `Mem` variant stores unescaped strings,
477561and escaping happens automatically during serialization:
478562479479-<!-- $MDX skip -->
480563```ocaml
481481-(* Create a pointer to key "a/b" - no escaping needed *)
482482-let p = Jsont_pointer.make [Mem "a/b"]
483483-484484-(* Serialize to string - escaping happens automatically *)
485485-let s = Jsont_pointer.to_string p (* Returns "/a~1b" *)
486486-487487-(* Parse from string - unescaping happens automatically *)
488488-let p' = Jsont_pointer.of_string "/a~1b"
489489-(* p' contains [Mem "a/b"] - the unescaped key *)
490490-```
491491-492492-The `Token` module exposes the escaping functions if you need them:
493493-494494-<!-- $MDX skip -->
495495-```ocaml
496496-module Token : sig
497497- val escape : string -> string (* "a/b" -> "a~1b" *)
498498- val unescape : string -> string (* "a~1b" -> "a/b" *)
499499-end
564564+# let p = make [`Mem "a/b"];;
565565+val p : t = <abstr>
566566+# to_string p;;
567567+- : string = "/a~1b"
568568+# let p' = of_string "/a~1b";;
569569+val p' : t = <abstr>
570570+# show_pointer p';;
571571+- : string = "[Mem:a/b]"
500572```
501573502574### Escaping in Action
503575504504-Let's see escaping with the CLI tool:
505505-506506-```sh
507507-$ jsonpp escape "hello"
508508-hello
509509-```
510510-511511-No special characters, no escaping needed.
512512-513513-```sh
514514-$ jsonpp escape "a/b"
515515-a~1b
516516-```
576576+The `Token` module exposes the escaping functions:
517577518518-The `/` becomes `~1`.
519519-520520-```sh
521521-$ jsonpp escape "a~b"
522522-a~0b
578578+```ocaml
579579+# Token.escape "hello";;
580580+- : string = "hello"
581581+# Token.escape "a/b";;
582582+- : string = "a~1b"
583583+# Token.escape "a~b";;
584584+- : string = "a~0b"
585585+# Token.escape "~/";;
586586+- : string = "~0~1"
523587```
524588525525-The `~` becomes `~0`.
526526-527527-```sh
528528-$ jsonpp escape "~/"
529529-~0~1
530530-```
531531-532532-Both characters are escaped.
533533-534589### Unescaping
535590536591And the reverse process:
537592538538-```sh
539539-$ jsonpp unescape "a~1b"
540540-OK: a/b
541541-```
542542-543543-```sh
544544-$ jsonpp unescape "a~0b"
545545-OK: a~b
593593+```ocaml
594594+# Token.unescape "a~1b";;
595595+- : string = "a/b"
596596+# Token.unescape "a~0b";;
597597+- : string = "a~b"
546598```
547599548600### The Order Matters!
···559611560612Let's verify this tricky case:
561613562562-```sh
563563-$ jsonpp unescape "~01"
564564-OK: ~1
614614+```ocaml
615615+# Token.unescape "~01";;
616616+- : string = "~1"
565617```
566618567619If we unescaped `~0` first, `~01` would become `~1`, which would then become
568620`/`. But that's wrong! The sequence `~01` should become the literal string
569621`~1` (a tilde followed by the digit one).
570622571571-Invalid escape sequences are rejected:
572572-573573-```sh
574574-$ jsonpp unescape "~2"
575575-ERROR: Invalid JSON Pointer: invalid escape sequence ~2
576576-```
577577-578578-```sh
579579-$ jsonpp unescape "hello~"
580580-ERROR: Invalid JSON Pointer: incomplete escape sequence at end
581581-```
582582-583623## URI Fragment Encoding
584624585625JSON Pointers can be embedded in URIs. RFC 6901, Section 6 explains:
···590630591631This adds percent-encoding on top of the `~0`/`~1` escaping:
592632593593-```sh
594594-$ jsonpp uri-fragment "/foo"
595595-OK: /foo -> /foo
633633+```ocaml
634634+# to_uri_fragment (of_string "/foo");;
635635+- : string = "/foo"
636636+# to_uri_fragment (of_string "/a~1b");;
637637+- : string = "/a~1b"
638638+# to_uri_fragment (of_string "/c%d");;
639639+- : string = "/c%25d"
640640+# to_uri_fragment (of_string "/ ");;
641641+- : string = "/%20"
596642```
597643598598-Simple pointers often don't need percent-encoding.
599599-600600-```sh
601601-$ jsonpp uri-fragment "/a~1b"
602602-OK: /a~1b -> /a~1b
603603-```
604604-605605-The `~1` escape stays as-is (it's valid in URI fragments).
606606-607607-```sh
608608-$ jsonpp uri-fragment "/c%d"
609609-OK: /c%d -> /c%25d
610610-```
611611-612612-The `%` character must be percent-encoded as `%25` in URIs!
613613-614614-```sh
615615-$ jsonpp uri-fragment "/ "
616616-OK: / -> /%20
617617-```
618618-619619-Spaces become `%20`.
620620-621621-The library provides functions for URI fragment encoding:
622622-623623-<!-- $MDX skip -->
624624-```ocaml
625625-val to_uri_fragment : t -> string
626626-val of_uri_fragment : string -> t
627627-val jsont_uri_fragment : t Jsont.t
628628-```
644644+The `%` character must be percent-encoded as `%25` in URIs, and
645645+spaces become `%20`.
629646630647Here's the RFC example showing the URI fragment forms:
631648···640657| `"/ "` | `#/%20` | `7` |
641658| `"/m~0n"` | `#/m~0n` | `8` |
642659643643-## Deeply Nested Structures
660660+## Building Pointers Programmatically
644661645645-JSON Pointer handles arbitrarily deep nesting:
662662+Instead of parsing strings, you can build pointers from indices:
646663647647-```sh
648648-$ jsonpp eval rfc6901_example.json "/foo/0"
649649-OK: "bar"
664664+```ocaml
665665+# let port_ptr = make [`Mem "database"; `Mem "port"];;
666666+val port_ptr : t = <abstr>
667667+# to_string port_ptr;;
668668+- : string = "/database/port"
650669```
651670652652-For deeper structures, just add more path segments. With nested objects:
671671+For array access, use `Nth`:
653672654654-```sh
655655-$ jsonpp add '{"a":{"b":{"c":"d"}}}' '/a/b/x' '"y"'
656656-{"a":{"b":{"c":"d","x":"y"}}}
673673+```ocaml
674674+# let first_feature_ptr = make [`Mem "features"; `Nth 0];;
675675+val first_feature_ptr : t = <abstr>
676676+# to_string first_feature_ptr;;
677677+- : string = "/features/0"
657678```
658679659659-With nested arrays:
680680+### Pointer Navigation
660681661661-```sh
662662-$ jsonpp add '{"arr":[[1,2],[3,4]]}' '/arr/0/1' '99'
663663-{"arr":[[1,99,2],[3,4]]}
682682+You can build pointers incrementally using `append`:
683683+684684+```ocaml
685685+# let db_ptr = of_string "/database";;
686686+val db_ptr : t = <abstr>
687687+# let creds_ptr = append db_ptr (`Mem "credentials");;
688688+val creds_ptr : t = <abstr>
689689+# let user_ptr = append creds_ptr (`Mem "username");;
690690+val user_ptr : t = <abstr>
691691+# to_string user_ptr;;
692692+- : string = "/database/credentials/username"
693693+```
694694+695695+Or concatenate two pointers:
696696+697697+```ocaml
698698+# let base = of_string "/api/v1";;
699699+val base : t = <abstr>
700700+# let endpoint = of_string "/users/0";;
701701+val endpoint : t = <abstr>
702702+# to_string (concat base endpoint);;
703703+- : string = "/api/v1/users/0"
664704```
665705666706## Jsont Integration
···670710because you can point to a location in a JSON document and decode it
671711directly to an OCaml type.
672712673673-Let's set up our OCaml environment and explore these features:
674674-675675-```ocaml
676676-# open Jsont_pointer;;
677677-# let parse_json s =
678678- match Jsont_bytesrw.decode_string Jsont.json s with
679679- | Ok json -> json
680680- | Error e -> failwith e;;
681681-val parse_json : string -> Jsont.json = <fun>
682682-# let json_to_string json =
683683- match Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json with
684684- | Ok s -> s
685685- | Error e -> failwith e;;
686686-val json_to_string : Jsont.json -> string = <fun>
687687-```
688688-689689-### Working with JSON Values
690690-691691-Let's create a sample configuration document:
692692-693713```ocaml
694714# let config_json = parse_json {|{
695715 "database": {
···719739 <abstr>)
720740```
721741722722-### Creating and Using Pointers
723723-724724-Create a pointer and use it to extract values:
725725-726726-```ocaml
727727-# let host_ptr = of_string "/database/host";;
728728-val host_ptr : t = <abstr>
729729-# let host_value = get host_ptr config_json;;
730730-val host_value : Jsont.json = Jsont.String ("localhost", <abstr>)
731731-# match host_value with
732732- | Jsont.String (s, _) -> s
733733- | _ -> failwith "expected string";;
734734-- : string = "localhost"
735735-```
736736-737737-### Building Pointers Programmatically
738738-739739-Instead of parsing strings, you can build pointers from indices:
740740-741741-```ocaml
742742-# let port_ptr = make [Mem "database"; Mem "port"];;
743743-val port_ptr : t = <abstr>
744744-# to_string port_ptr;;
745745-- : string = "/database/port"
746746-# match get port_ptr config_json with
747747- | Jsont.Number (n, _) -> int_of_float n
748748- | _ -> failwith "expected number";;
749749-- : int = 5432
750750-```
751751-752752-For array access, use `Nth`:
753753-754754-```ocaml
755755-# let first_feature_ptr = make [Mem "features"; Nth 0];;
756756-val first_feature_ptr : t = <abstr>
757757-# match get first_feature_ptr config_json with
758758- | Jsont.String (s, _) -> s
759759- | _ -> failwith "expected string";;
760760-- : string = "auth"
761761-```
762762-763763-### Pointer Navigation
764764-765765-You can build pointers incrementally using `append`:
766766-767767-```ocaml
768768-# let db_ptr = of_string "/database";;
769769-val db_ptr : t = <abstr>
770770-# let creds_ptr = append db_ptr (Mem "credentials");;
771771-val creds_ptr : t = <abstr>
772772-# let user_ptr = append creds_ptr (Mem "username");;
773773-val user_ptr : t = <abstr>
774774-# to_string user_ptr;;
775775-- : string = "/database/credentials/username"
776776-# match get user_ptr config_json with
777777- | Jsont.String (s, _) -> s
778778- | _ -> failwith "expected string";;
779779-- : string = "admin"
780780-```
781781-782782-### Safe Access with `find`
783783-784784-Use `find` when you're not sure if a path exists:
785785-786786-```ocaml
787787-# find (of_string "/database/timeout") config_json;;
788788-- : Jsont.json option = None
789789-# find (of_string "/database/host") config_json |> Option.is_some;;
790790-- : bool = true
791791-```
792792-793742### Typed Access with `path`
794743795744The `path` combinator combines pointer navigation with typed decoding:
···831780 config_json
832781 |> Result.get_ok;;
833782val timeout : int = 30
834834-```
835835-836836-### Mutation Operations
837837-838838-The library provides mutation functions for modifying JSON:
839839-840840-```ocaml
841841-# let sample = parse_json {|{"name": "Alice", "scores": [85, 92, 78]}|};;
842842-val sample : Jsont.json =
843843- Jsont.Object
844844- ([(("name", <abstr>), Jsont.String ("Alice", <abstr>));
845845- (("scores", <abstr>),
846846- Jsont.Array
847847- ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>);
848848- Jsont.Number (78., <abstr>)],
849849- <abstr>))],
850850- <abstr>)
851851-```
852852-853853-**Add** a new field:
854854-855855-```ocaml
856856-# let with_email = add (of_string "/email") sample
857857- ~value:(Jsont.Json.string "alice@example.com");;
858858-val with_email : Jsont.json =
859859- Jsont.Object
860860- ([(("name", <abstr>), Jsont.String ("Alice", <abstr>));
861861- (("scores", <abstr>),
862862- Jsont.Array
863863- ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>);
864864- Jsont.Number (78., <abstr>)],
865865- <abstr>));
866866- (("email", <abstr>), Jsont.String ("alice@example.com", <abstr>))],
867867- <abstr>)
868868-# json_to_string with_email;;
869869-- : string =
870870-"{\"name\":\"Alice\",\"scores\":[85,92,78],\"email\":\"alice@example.com\"}"
871871-```
872872-873873-**Add** to an array using `-` (append):
874874-875875-```ocaml
876876-# let with_new_score = add (of_string "/scores/-") sample
877877- ~value:(Jsont.Json.number 95.);;
878878-val with_new_score : Jsont.json =
879879- Jsont.Object
880880- ([(("name", <abstr>), Jsont.String ("Alice", <abstr>));
881881- (("scores", <abstr>),
882882- Jsont.Array
883883- ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>);
884884- Jsont.Number (78., <abstr>); Jsont.Number (95., <abstr>)],
885885- <abstr>))],
886886- <abstr>)
887887-# json_to_string with_new_score;;
888888-- : string = "{\"name\":\"Alice\",\"scores\":[85,92,78,95]}"
889889-```
890890-891891-**Replace** an existing value:
892892-893893-```ocaml
894894-# let renamed = replace (of_string "/name") sample
895895- ~value:(Jsont.Json.string "Bob");;
896896-val renamed : Jsont.json =
897897- Jsont.Object
898898- ([(("name", <abstr>), Jsont.String ("Bob", <abstr>));
899899- (("scores", <abstr>),
900900- Jsont.Array
901901- ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>);
902902- Jsont.Number (78., <abstr>)],
903903- <abstr>))],
904904- <abstr>)
905905-# json_to_string renamed;;
906906-- : string = "{\"name\":\"Bob\",\"scores\":[85,92,78]}"
907907-```
908908-909909-**Remove** a value:
910910-911911-```ocaml
912912-# let without_first = remove (of_string "/scores/0") sample;;
913913-val without_first : Jsont.json =
914914- Jsont.Object
915915- ([(("name", <abstr>), Jsont.String ("Alice", <abstr>));
916916- (("scores", <abstr>),
917917- Jsont.Array
918918- ([Jsont.Number (92., <abstr>); Jsont.Number (78., <abstr>)], <abstr>))],
919919- <abstr>)
920920-# json_to_string without_first;;
921921-- : string = "{\"name\":\"Alice\",\"scores\":[92,78]}"
922783```
923784924785### Nested Path Extraction
+26-29
src/jsont_pointer.ml
···56565757(* Index type - represents how a token is interpreted in context *)
5858module Index = struct
5959- type t =
6060- | Mem of string
6161- | Nth of int
6262- | End
5959+ type t = [ `Mem of string | `Nth of int | `End ]
63606461 let pp ppf = function
6565- | Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
6666- | Nth n -> Format.fprintf ppf "/%d" n
6767- | End -> Format.fprintf ppf "/-"
6262+ | `Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
6363+ | `Nth n -> Format.fprintf ppf "/%d" n
6464+ | `End -> Format.fprintf ppf "/-"
68656966 let equal i1 i2 = match i1, i2 with
7070- | Mem s1, Mem s2 -> String.equal s1 s2
7171- | Nth n1, Nth n2 -> Int.equal n1 n2
7272- | End, End -> true
6767+ | `Mem s1, `Mem s2 -> String.equal s1 s2
6868+ | `Nth n1, `Nth n2 -> Int.equal n1 n2
6969+ | `End, `End -> true
7370 | _ -> false
74717572 let compare i1 i2 = match i1, i2 with
7676- | Mem s1, Mem s2 -> String.compare s1 s2
7777- | Mem _, _ -> -1
7878- | _, Mem _ -> 1
7979- | Nth n1, Nth n2 -> Int.compare n1 n2
8080- | Nth _, End -> -1
8181- | End, Nth _ -> 1
8282- | End, End -> 0
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
83808481 let of_path_index (idx : Jsont.Path.index) : t =
8582 match idx with
8686- | Jsont.Path.Mem (s, _meta) -> Mem s
8787- | Jsont.Path.Nth (n, _meta) -> Nth n
8383+ | Jsont.Path.Mem (s, _meta) -> `Mem s
8484+ | Jsont.Path.Nth (n, _meta) -> `Nth n
88858986 let to_path_index (idx : t) : Jsont.Path.index option =
9087 match idx with
9191- | Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none))
9292- | Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none))
9393- | End -> None
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
9491end
95929693(* Internal representation: raw unescaped tokens.
···112109 (* Convert to Index for a given JSON value type *)
113110 let to_index seg ~for_array =
114111 match seg with
115115- | End -> Index.End
112112+ | End -> `End
116113 | Token s ->
117114 if for_array then
118115 match Token.is_valid_array_index s with
119119- | Some n -> Index.Nth n
120120- | None -> Index.Mem s (* Invalid index becomes member for error msg *)
116116+ | Some n -> `Nth n
117117+ | None -> `Mem s (* Invalid index becomes member for error msg *)
121118 else
122122- Index.Mem s
119119+ `Mem s
123120124121 (* Convert from Index *)
125122 let of_index = function
126126- | Index.End -> End
127127- | Index.Mem s -> Token s
128128- | Index.Nth n -> Token (string_of_int n)
123123+ | `End -> End
124124+ | `Mem s -> Token s
125125+ | `Nth n -> Token (string_of_int n)
129126end
130127131128(* Pointer type - list of segments *)
+24-23
src/jsont_pointer.mli
···6565 a numeric index or the special end-of-array marker [-]. *)
6666module Index : sig
67676868- type t =
6969- | Mem of string
7070- (** [Mem name] indexes into an object member with the given [name].
6868+ 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).
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
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}
8080+ Evaluating a pointer containing [`End] with {!Jsont_pointer.get}
8181 will raise an error since it refers to a nonexistent element. *)
8282+ ]
82838384 val pp : Format.formatter -> t -> unit
8485 (** [pp] formats an index in JSON Pointer string notation. *)
···96979798 val to_path_index : t -> Jsont.Path.index option
9899 (** [to_path_index idx] converts to a {!Jsont.Path.index}.
9999- Returns [None] for {!End} since it has no equivalent in
100100+ Returns [None] for [`End] since it has no equivalent in
100101 {!Jsont.Path}. *)
101102end
102103···144145 The string must be either empty (representing the root) or start
145146 with [/]. Each segment between [/] characters is unescaped as a
146147 reference token. Segments that are valid non-negative integers
147147- without leading zeros become {!Index.Nth} indices; the string [-]
148148- becomes {!Index.End}; all others become {!Index.Mem}.
148148+ without leading zeros become [`Nth] indices; the string [-]
149149+ becomes [`End]; all others become [`Mem].
149150150151 @raise Jsont.Error if [s] has invalid syntax:
151152 - Non-empty string not starting with [/]
···204205205206val to_path : t -> Jsont.Path.t option
206207(** [to_path p] converts to a {!Jsont.Path.t}.
207207- Returns [None] if [p] contains an {!Index.End} index. *)
208208+ Returns [None] if [p] contains an [`End] index. *)
208209209210val to_path_exn : t -> Jsont.Path.t
210211(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error}
···221222 @raise Jsont.Error if:
222223 - The pointer references a nonexistent object member
223224 - The pointer references an out-of-bounds array index
224224- - The pointer contains {!Index.End} (since [-] always refers
225225+ - The pointer contains [`End] (since [-] always refers
225226 to a nonexistent element)
226226- - An index type doesn't match the JSON value (e.g., {!Index.Nth}
227227+ - An index type doesn't match the JSON value (e.g., [`Nth]
227228 on an object) *)
228229229230val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
···246247val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json
247248(** [set p json ~value] replaces the value at pointer [p] with [value].
248249249249- For {!Index.End} on arrays, appends [value] to the end of the array.
250250+ For [`End] on arrays, appends [value] to the end of the array.
250251251252 @raise Jsont.Error if the pointer doesn't resolve to an existing
252252- location (except for {!Index.End} on arrays). *)
253253+ location (except for [`End] on arrays). *)
253254254255val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
255256(** [add p json ~value] adds [value] at the location specified by [p].
···258259 {ul
259260 {- For objects: If the member exists, it is replaced. If it doesn't
260261 exist, a new member is added.}
261261- {- For arrays with {!Index.Nth}: Inserts [value] {e before} the
262262+ {- For arrays with [`Nth]: Inserts [value] {e before} the
262263 specified index, shifting subsequent elements. The index must be
263264 valid (0 to length inclusive).}
264264- {- For arrays with {!Index.End}: Appends [value] to the array.}}
265265+ {- For arrays with [`End]: Appends [value] to the array.}}
265266266267 @raise Jsont.Error if:
267268 - The parent of the target location doesn't exist
268268- - An array index is out of bounds (except for {!Index.End})
269269+ - An array index is out of bounds (except for [`End])
269270 - The parent is not an object or array *)
270271271272val remove : t -> Jsont.json -> Jsont.json
···277278 @raise Jsont.Error if:
278279 - [p] is the root (cannot remove the root)
279280 - The pointer doesn't resolve to an existing value
280280- - The pointer contains {!Index.End} *)
281281+ - The pointer contains [`End] *)
281282282283val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json
283284(** [replace p json ~value] replaces the value at pointer [p] with [value].
···286287287288 @raise Jsont.Error if:
288289 - The pointer doesn't resolve to an existing value
289289- - The pointer contains {!Index.End} *)
290290+ - The pointer contains [`End] *)
290291291292val move : from:t -> path:t -> Jsont.json -> Jsont.json
292293(** [move ~from ~path json] moves the value from [from] to [path].
···297298 @raise Jsont.Error if:
298299 - [from] doesn't resolve to a value
299300 - [path] is a proper prefix of [from] (would create a cycle)
300300- - Either pointer contains {!Index.End} *)
301301+ - Either pointer contains [`End] *)
301302302303val copy : from:t -> path:t -> Jsont.json -> Jsont.json
303304(** [copy ~from ~path json] copies the value from [from] to [path].
···307308308309 @raise Jsont.Error if:
309310 - [from] doesn't resolve to a value
310310- - Either pointer contains {!Index.End} *)
311311+ - Either pointer contains [`End] *)
311312312313val test : t -> Jsont.json -> expected:Jsont.json -> bool
313314(** [test p json ~expected] tests if the value at [p] equals [expected].
+3-3
test/test_pointer.ml
···2424 let indices = Jsont_pointer.indices p in
2525 let index_strs = List.map (fun idx ->
2626 match idx with
2727- | Jsont_pointer.Index.Mem s -> Printf.sprintf "Mem:%s" s
2828- | Jsont_pointer.Index.Nth n -> Printf.sprintf "Nth:%d" n
2929- | Jsont_pointer.Index.End -> "End"
2727+ | `Mem s -> Printf.sprintf "Mem:%s" s
2828+ | `Nth n -> Printf.sprintf "Nth:%d" n
2929+ | `End -> "End"
3030 ) indices in
3131 Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
3232 with Jsont.Error e ->