RFC6901 JSON Pointer implementation in OCaml using jsont
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** RFC 6901 JSON Pointer implementation for jsont.
7
8 This module provides {{:https://www.rfc-editor.org/rfc/rfc6901}RFC 6901}
9 JSON Pointer parsing, serialization, and evaluation compatible with
10 {!Jsont} codecs.
11
12 {1 JSON Pointer vs JSON Path}
13
14 JSON Pointer (RFC 6901) and {!Jsont.Path} serve similar purposes but
15 have important differences:
16
17 {ul
18 {- {b JSON Pointer} is a {e string syntax} for addressing JSON values,
19 designed for use in URIs and JSON documents (like JSON Patch).
20 It uses [/] as separator and has escape sequences ([~0], [~1]).}
21 {- {b Jsont.Path} is an {e OCaml data structure} for programmatic
22 navigation, with no string representation defined.}}
23
24 A key difference is the [-] token: JSON Pointer's [-] refers to the
25 (nonexistent) element {e after} the last array element. This is used
26 for append operations in JSON Patch but is meaningless for retrieval.
27 {!Jsont.Path} has no equivalent concept.
28
29 This library uses phantom types to enforce this distinction at compile
30 time: pointers that may contain [-] ({!append} pointers) cannot be
31 passed to retrieval functions like {!get}.
32
33 {2 Example}
34
35 Given the JSON document:
36 {v
37 {
38 "foo": ["bar", "baz"],
39 "": 0,
40 "a/b": 1,
41 "m~n": 2
42 }
43 v}
44
45 The following JSON Pointers evaluate to:
46 {ul
47 {- [""] - the whole document}
48 {- ["/foo"] - the array [\["bar", "baz"\]]}
49 {- ["/foo/0"] - the string ["bar"]}
50 {- ["/"] - the integer [0] (empty string key)}
51 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])}
52 {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}
53 {- ["/foo/-"] - nonexistent; only valid for mutations}}
54
55 {1:tokens Reference Tokens}
56
57 JSON Pointer uses escape sequences for special characters in reference
58 tokens. The character [~] must be encoded as [~0] and [/] as [~1].
59 When unescaping, [~1] is processed before [~0] to correctly handle
60 sequences like [~01] which should become [~1], not [/]. *)
61
62(** {1 Reference tokens}
63
64 Reference tokens are the individual segments between [/] characters
65 in a JSON Pointer string. They require escaping of [~] and [/]. *)
66module Token : sig
67
68 type t = string
69 (** The type for unescaped reference tokens. These are plain strings
70 representing object member names or array index strings. *)
71
72 val escape : t -> string
73 (** [escape s] escapes special characters in [s] for use in a JSON Pointer.
74 Specifically, [~] becomes [~0] and [/] becomes [~1]. *)
75
76 val unescape : string -> t
77 (** [unescape s] unescapes a JSON Pointer reference token.
78 Specifically, [~1] becomes [/] and [~0] becomes [~].
79
80 @raise Jsont.Error.Error if [s] contains invalid escape sequences
81 (a [~] not followed by [0] or [1]). *)
82end
83
84(** {1 Indices}
85
86 Indices are the individual navigation steps in a JSON Pointer.
87 This library reuses {!Jsont.Path.index} directly - the JSON Pointer
88 specific [-] token is handled separately via phantom types on the
89 pointer type itself. *)
90
91type index = Jsont.Path.index
92(** The type for navigation indices. This is exactly {!Jsont.Path.index}:
93 either [Jsont.Path.Mem (name, meta)] for object member access or
94 [Jsont.Path.Nth (n, meta)] for array index access. *)
95
96val mem : ?meta:Jsont.Meta.t -> string -> index
97(** [mem ?meta s] is [Jsont.Path.Mem (s, meta)].
98 Convenience constructor for object member access.
99 [meta] defaults to {!Jsont.Meta.none}. *)
100
101val nth : ?meta:Jsont.Meta.t -> int -> index
102(** [nth ?meta n] is [Jsont.Path.Nth (n, meta)].
103 Convenience constructor for array index access.
104 [meta] defaults to {!Jsont.Meta.none}. *)
105
106val pp_index : Format.formatter -> index -> unit
107(** [pp_index] formats an index in JSON Pointer string notation. *)
108
109val equal_index : index -> index -> bool
110(** [equal_index i1 i2] is [true] iff [i1] and [i2] are the same index. *)
111
112val compare_index : index -> index -> int
113(** [compare_index i1 i2] is a total order on indices. *)
114
115(** {1 Pointers}
116
117 JSON Pointers use phantom types to distinguish between:
118 {ul
119 {- {!nav} pointers that reference existing elements (safe for all operations)}
120 {- {!append} pointers that end with [-] (only valid for {!add} and {!set})}}
121
122 This ensures at compile time that you cannot accidentally try to
123 retrieve a nonexistent "end of array" position. *)
124
125type 'a t
126(** The type for JSON Pointers. The phantom type ['a] indicates whether
127 the pointer can be used for navigation ([nav]) or only for append
128 operations ([append]). *)
129
130type nav
131(** Phantom type for pointers that reference existing elements.
132 These can be used with all operations including {!get} and {!find}. *)
133
134type append
135(** Phantom type for pointers ending with [-] (the "after last element"
136 position). These can only be used with {!add} and {!set}. *)
137
138(** {2 Existential wrapper}
139
140 The {!type:any} type wraps a pointer of unknown phantom type, allowing
141 ergonomic use with mutation operations like {!set} and {!add} without
142 needing to pattern match on the pointer kind. *)
143
144type any = Any : _ t -> any
145(** Existential wrapper for pointers. Use this when you don't need to
146 distinguish between navigation and append pointers at the type level,
147 such as when using {!set} or {!add} which accept either kind. *)
148
149val root : nav t
150(** [root] is the empty pointer that references the whole document.
151 In string form this is [""]. *)
152
153val is_root : _ t -> bool
154(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
155
156val make : index list -> nav t
157(** [make indices] creates a navigation pointer from a list of indices.
158 The list is ordered from root to target (i.e., the first element
159 is the first step from the root). *)
160
161val ( / ) : nav t -> index -> nav t
162(** [p / idx] appends [idx] to pointer [p]. Operator form of {!append_index}. *)
163
164val append_index : nav t -> index -> nav t
165(** [append_index p idx] appends [idx] to the end of pointer [p]. *)
166
167val at_end : nav t -> append t
168(** [at_end p] creates an append pointer by adding [-] to [p].
169 The resulting pointer refers to the position after the last element
170 of the array at [p]. Only valid for use with {!add} and {!set}. *)
171
172val concat : nav t -> nav t -> nav t
173(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
174
175val parent : nav t -> nav t option
176(** [parent p] returns the parent pointer of [p], or [None] if [p]
177 is the {!root}. *)
178
179val last : nav t -> index option
180(** [last p] returns the last index of [p], or [None] if [p] is
181 the {!root}. *)
182
183val indices : _ t -> index list
184(** [indices p] returns the indices of [p] from root to target.
185 Note: for append pointers, this returns the indices of the path
186 portion; the [-] (append position) is not represented as an index. *)
187
188(** {2:coercion Coercion and inspection} *)
189
190val any : _ t -> any
191(** [any p] wraps a typed pointer in the existential {!type:any} type.
192 Use this when you have a [nav t] or [append t] but need an {!type:any}
193 for use with functions like {!set} or {!add}. *)
194
195val is_nav : any -> bool
196(** [is_nav p] is [true] if [p] is a navigation pointer (not an append
197 pointer ending with [-]). *)
198
199val to_nav : any -> nav t option
200(** [to_nav p] returns [Some nav_p] if [p] is a navigation pointer,
201 or [None] if it's an append pointer. *)
202
203val to_nav_exn : any -> nav t
204(** [to_nav_exn p] returns the navigation pointer if [p] is one.
205 @raise Jsont.Error.Error if [p] is an append pointer. *)
206
207(** {2:parsing Parsing} *)
208
209val of_string : string -> any
210(** [of_string s] parses a JSON Pointer from its string representation.
211
212 Returns an {!type:any} pointer that can be used directly with mutation
213 operations like {!set} and {!add}. For retrieval operations like
214 {!get}, use {!of_string_nav} instead.
215
216 The string must be either empty (representing the root) or start
217 with [/]. Each segment between [/] characters is unescaped as a
218 reference token.
219
220 @raise Jsont.Error.Error if [s] has invalid syntax:
221 - Non-empty string not starting with [/]
222 - Invalid escape sequence ([~] not followed by [0] or [1])
223 - [-] appears in non-final position *)
224
225val of_string_kind : string -> [ `Nav of nav t | `Append of append t ]
226(** [of_string_kind s] parses a JSON Pointer and returns a tagged variant
227 indicating whether it's a navigation or append pointer.
228
229 Use this when you need to handle navigation and append pointers
230 differently, or when you need a typed pointer for operations that
231 require a specific kind.
232
233 @raise Jsont.Error.Error if [s] has invalid syntax. *)
234
235val of_string_nav : string -> nav t
236(** [of_string_nav s] parses a JSON Pointer that must not contain [-].
237
238 Use this when you need a {!nav} pointer for retrieval operations
239 like {!get} or {!find}.
240
241 @raise Jsont.Error.Error if [s] has invalid syntax or contains [-]. *)
242
243val of_string_result : string -> (any, string) result
244(** [of_string_result s] is like {!of_string} but returns a result
245 instead of raising. *)
246
247val of_uri_fragment : string -> any
248(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
249
250 This is like {!of_string} but first percent-decodes the string
251 according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}.
252 The leading [#] should {b not} be included in [s].
253
254 @raise Jsont.Error.Error on invalid syntax or invalid percent-encoding. *)
255
256val of_uri_fragment_nav : string -> nav t
257(** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires
258 the pointer to not contain [-].
259
260 @raise Jsont.Error.Error if invalid or contains [-]. *)
261
262val of_uri_fragment_result : string -> (any, string) result
263(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
264 a result instead of raising. *)
265
266(** {2:serializing Serializing} *)
267
268val to_string : _ t -> string
269(** [to_string p] serializes [p] to its JSON Pointer string representation.
270
271 Returns [""] for the root pointer, otherwise [/] followed by
272 escaped reference tokens joined by [/]. Append pointers include
273 the trailing [/-]. *)
274
275val to_uri_fragment : _ t -> string
276(** [to_uri_fragment p] serializes [p] to URI fragment form.
277
278 This is like {!to_string} but additionally percent-encodes
279 characters that are not allowed in URI fragments per RFC 3986.
280 The leading [#] is {b not} included in the result. *)
281
282val pp : Format.formatter -> _ t -> unit
283(** [pp] formats a pointer using {!to_string}. *)
284
285val pp_verbose : Format.formatter -> _ t -> unit
286(** [pp_verbose] formats a pointer showing its index structure.
287 For example, [/foo/0] is formatted as [[Mem "foo"; Nth 0]].
288 Append pointers show [/-] at the end.
289 Useful for debugging and understanding pointer structure. *)
290
291(** {2:comparison Comparison} *)
292
293val equal : _ t -> _ t -> bool
294(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices
295 and the same append status. *)
296
297val compare : _ t -> _ t -> int
298(** [compare p1 p2] is a total order on pointers, comparing indices
299 lexicographically. Append pointers sort after nav pointers with
300 the same prefix. *)
301
302(** {2:jsont_path Conversion with Jsont.Path} *)
303
304val of_path : Jsont.Path.t -> nav t
305(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer.
306 Always returns a {!nav} pointer since {!Jsont.Path} has no [-] concept. *)
307
308val to_path : nav t -> Jsont.Path.t
309(** [to_path p] converts a navigation pointer to a {!Jsont.Path.t}. *)
310
311(** {1 Evaluation}
312
313 These functions evaluate a JSON Pointer against a {!type:Jsont.json} value
314 to retrieve the referenced value. They only accept {!nav} pointers
315 since {!append} pointers refer to nonexistent positions. *)
316
317val get : nav t -> Jsont.json -> Jsont.json
318(** [get p json] retrieves the value at pointer [p] in [json].
319
320 @raise Jsont.Error.Error if:
321 - The pointer references a nonexistent object member
322 - The pointer references an out-of-bounds array index
323 - An index type doesn't match the JSON value (e.g., [Nth]
324 on an object) *)
325
326val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
327(** [get_result p json] is like {!get} but returns a result. *)
328
329val find : nav t -> Jsont.json -> Jsont.json option
330(** [find p json] is like {!get} but returns [None] instead of
331 raising when the pointer doesn't resolve to a value. *)
332
333(** {1 Mutation}
334
335 These functions modify a {!type:Jsont.json} value at a location specified
336 by a JSON Pointer. They are designed to support
337 {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch}
338 operations.
339
340 All mutation functions return a new JSON value with the modification
341 applied; they do not mutate the input.
342
343 Functions that support the [-] token ({!set}, {!add}, {!move}, {!copy})
344 accept {!type:any} pointers, making them easy to use with {!of_string}.
345 Functions that require an existing element ({!remove}, {!replace})
346 only accept {!nav} pointers. *)
347
348val set : any -> Jsont.json -> value:Jsont.json -> Jsont.json
349(** [set p json ~value] replaces the value at pointer [p] with [value].
350
351 For {!append} pointers, appends [value] to the end of the array.
352
353 This accepts {!type:any} pointers directly from {!of_string}:
354 {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]}
355
356 @raise Jsont.Error.Error if the pointer doesn't resolve to an existing
357 location (except for {!append} pointers on arrays). *)
358
359val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json
360(** [add p json ~value] adds [value] at the location specified by [p].
361
362 The behavior depends on the target:
363 {ul
364 {- For objects: If the member exists, it is replaced. If it doesn't
365 exist, a new member is added.}
366 {- For arrays with [Nth]: Inserts [value] {e before} the
367 specified index, shifting subsequent elements. The index must be
368 valid (0 to length inclusive).}
369 {- For {!append} pointers: Appends [value] to the array.}}
370
371 @raise Jsont.Error.Error if:
372 - The parent of the target location doesn't exist
373 - An array index is out of bounds (except for {!append} pointers)
374 - The parent is not an object or array *)
375
376val remove : nav t -> Jsont.json -> Jsont.json
377(** [remove p json] removes the value at pointer [p].
378
379 For objects, removes the member. For arrays, removes the element
380 and shifts subsequent elements.
381
382 @raise Jsont.Error.Error if:
383 - [p] is the root (cannot remove the root)
384 - The pointer doesn't resolve to an existing value *)
385
386val replace : nav t -> Jsont.json -> value:Jsont.json -> Jsont.json
387(** [replace p json ~value] replaces the value at pointer [p] with [value].
388
389 Unlike {!add}, this requires the target to exist.
390
391 @raise Jsont.Error.Error if the pointer doesn't resolve to an existing value. *)
392
393val move : from:nav t -> path:any -> Jsont.json -> Jsont.json
394(** [move ~from ~path json] moves the value from [from] to [path].
395
396 This is equivalent to {!remove} at [from] followed by {!add}
397 at [path] with the removed value.
398
399 @raise Jsont.Error.Error if:
400 - [from] doesn't resolve to a value
401 - [path] is a proper prefix of [from] (would create a cycle) *)
402
403val copy : from:nav t -> path:any -> Jsont.json -> Jsont.json
404(** [copy ~from ~path json] copies the value from [from] to [path].
405
406 This is equivalent to {!get} at [from] followed by {!add}
407 at [path] with the retrieved value.
408
409 @raise Jsont.Error.Error if [from] doesn't resolve to a value. *)
410
411val test : nav t -> Jsont.json -> expected:Jsont.json -> bool
412(** [test p json ~expected] tests if the value at [p] equals [expected].
413
414 Returns [true] if the values are equal according to {!Jsont.Json.equal},
415 [false] otherwise. Also returns [false] (rather than raising) if the
416 pointer doesn't resolve.
417
418 Note: This implements the semantics of the JSON Patch "test" operation. *)
419
420(** {1 Jsont Integration}
421
422 These types and functions integrate JSON Pointers with the {!Jsont}
423 codec system. *)
424
425val jsont : any Jsont.t
426(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
427
428 On decode, parses a JSON string as a JSON Pointer using {!of_string}.
429 On encode, serializes a pointer to a JSON string using {!to_string}. *)
430
431val jsont_kind : [ `Nav of nav t | `Append of append t ] Jsont.t
432(** [jsont_kind] is a {!Jsont.t} codec for JSON Pointers that preserves
433 the pointer kind in the type.
434
435 On decode, parses using {!of_string_kind}.
436 On encode, serializes using {!to_string}. *)
437
438val jsont_nav : nav t Jsont.t
439(** [jsont_nav] is a {!Jsont.t} codec for navigation JSON Pointers.
440
441 On decode, parses using {!of_string_nav} (fails on [-]).
442 On encode, serializes using {!to_string}. *)
443
444val jsont_uri_fragment : any Jsont.t
445(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
446
447 On decode, parses using {!of_uri_fragment}.
448 On encode, serializes using {!to_uri_fragment}. *)
449
450(** {2:query Query combinators}
451
452 These combinators integrate with jsont's query system, allowing
453 JSON Pointers to be used with jsont codecs for typed access. *)
454
455val path : ?absent:'a -> nav t -> 'a Jsont.t -> 'a Jsont.t
456(** [path p t] decodes the value at pointer [p] using codec [t].
457
458 If [absent] is provided and the pointer doesn't resolve, returns
459 [absent] instead of raising.
460
461 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
462
463val set_path : ?allow_absent:bool -> 'a Jsont.t -> any -> 'a -> Jsont.json Jsont.t
464(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
465
466 If [allow_absent] is [true] (default [false]), creates missing
467 intermediate structure as needed.
468
469 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
470
471val update_path : ?absent:'a -> nav t -> 'a Jsont.t -> Jsont.json Jsont.t
472(** [update_path p t] recodes the value at pointer [p] with codec [t].
473
474 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
475
476val delete_path : ?allow_absent:bool -> nav t -> Jsont.json Jsont.t
477(** [delete_path p] removes the value at pointer [p].
478
479 If [allow_absent] is [true] (default [false]), does nothing if
480 the pointer doesn't resolve instead of raising. *)
481
482(** {1:jmap JMAP Extended Pointers}
483
484 {{:https://www.rfc-editor.org/rfc/rfc8620#section-3.7}RFC 8620 Section 3.7}
485 extends JSON Pointer with a wildcard token [*] for mapping through arrays.
486 This is used in JMAP result references.
487
488 The wildcard semantics are:
489 {ul
490 {- When the current value is an array and the token is [*], apply the rest
491 of the pointer to each element, collecting results into a new array.}
492 {- If a mapped result is itself an array, its contents are flattened into
493 the output (i.e., array of arrays becomes a single array).}}
494
495 Example: Given [{"list": \[{"id": "a"}, {"id": "b"}\]}], the extended
496 pointer [/list/*/id] evaluates to [["a", "b"]].
497
498 {b Note}: These extended pointers are {e not} valid RFC 6901 JSON Pointers.
499 They should only be used for JMAP result reference resolution. *)
500
501module Jmap : sig
502 (** JMAP extended JSON Pointer with wildcard support. *)
503
504 type t
505 (** The type for JMAP extended pointers. Unlike standard pointers, these
506 may contain [*] tokens for array mapping. *)
507
508 val of_string : string -> t
509 (** [of_string s] parses a JMAP extended pointer.
510
511 The syntax is the same as RFC 6901 JSON Pointer, except [*] is allowed
512 as a reference token for array mapping.
513
514 @raise Jsont.Error.Error if [s] has invalid syntax. *)
515
516 val of_string_result : string -> (t, string) result
517 (** [of_string_result s] is like {!of_string} but returns a result. *)
518
519 val to_string : t -> string
520 (** [to_string p] serializes [p] to string form. *)
521
522 val pp : Format.formatter -> t -> unit
523 (** [pp] formats a pointer using {!to_string}. *)
524
525 val eval : t -> Jsont.json -> Jsont.json
526 (** [eval p json] evaluates the extended pointer [p] against [json].
527
528 For [*] tokens on arrays, maps through all elements and collects results.
529 Results that are arrays are flattened into the output.
530
531 @raise Jsont.Error.Error if:
532 - A standard token doesn't resolve (member not found, index out of bounds)
533 - [*] is used on a non-array value
534 - [-] appears in the pointer (not supported in JMAP extended pointers) *)
535
536 val eval_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
537 (** [eval_result p json] is like {!eval} but returns a result. *)
538
539 val find : t -> Jsont.json -> Jsont.json option
540 (** [find p json] is like {!eval} but returns [None] on errors. *)
541
542 val jsont : t Jsont.t
543 (** [jsont] is a {!Jsont.t} codec for JMAP extended pointers. *)
544
545 (** {2:combinators Query combinators}
546
547 These combinators integrate JMAP extended pointers with jsont codecs,
548 enabling typed extraction from JSON using pointer paths. *)
549
550 val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t
551 (** [path p codec] extracts the value at pointer [p] and decodes it with [codec].
552
553 If [absent] is provided and the pointer doesn't resolve, returns [absent].
554 Otherwise raises on pointer resolution failure.
555
556 Example: Extract all thread IDs from an Email/get response:
557 {[
558 let thread_ids =
559 Jmap.path
560 (Jmap.of_string "/list/*/threadId")
561 (Jsont.list Jsont.string)
562 ]}
563
564 @raise Jsont.Error.Error if the pointer fails to resolve (and no [absent])
565 or if decoding with [codec] fails. *)
566
567 val path_list : t -> 'a Jsont.t -> 'a list Jsont.t
568 (** [path_list p codec] extracts the array at pointer [p] and decodes each
569 element with [codec].
570
571 This is a convenience for the common JMAP pattern where wildcards produce
572 arrays that need element-wise decoding:
573 {[
574 (* These are equivalent: *)
575 Jmap.path_list (Jmap.of_string "/list/*/id") Jsont.string
576 Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string)
577 ]}
578
579 @raise Jsont.Error.Error if pointer resolution fails, the result is not an array,
580 or any element fails to decode. *)
581end