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 A JSON Pointer is a string syntax for identifying a specific value within
13 a JSON document. For example, given the JSON document:
14 {v
15 {
16 "foo": ["bar", "baz"],
17 "": 0,
18 "a/b": 1,
19 "m~n": 2
20 }
21 v}
22
23 The following JSON Pointers evaluate to:
24 {ul
25 {- [""] - the whole document}
26 {- ["/foo"] - the array [\["bar", "baz"\]]}
27 {- ["/foo/0"] - the string ["bar"]}
28 {- ["/"] - the integer [0] (empty string key)}
29 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])}
30 {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}}
31
32 {1:tokens Reference Tokens}
33
34 JSON Pointer uses escape sequences for special characters in reference
35 tokens. The character [~] must be encoded as [~0] and [/] as [~1].
36 When unescaping, [~1] is processed before [~0] to correctly handle
37 sequences like [~01] which should become [~1], not [/]. *)
38
39(** {1 Reference tokens}
40
41 Reference tokens are the individual segments between [/] characters
42 in a JSON Pointer string. They require escaping of [~] and [/]. *)
43module Token : sig
44
45 type t = string
46 (** The type for unescaped reference tokens. These are plain strings
47 representing object member names or array index strings. *)
48
49 val escape : t -> string
50 (** [escape s] escapes special characters in [s] for use in a JSON Pointer.
51 Specifically, [~] becomes [~0] and [/] becomes [~1]. *)
52
53 val unescape : string -> t
54 (** [unescape s] unescapes a JSON Pointer reference token.
55 Specifically, [~1] becomes [/] and [~0] becomes [~].
56
57 @raise Jsont.Error if [s] contains invalid escape sequences
58 (a [~] not followed by [0] or [1]). *)
59end
60
61(** {1 Indices}
62
63 Indices represent individual navigation steps in a JSON Pointer.
64 For objects, this is a member name. For arrays, this is either
65 a numeric index or the special end-of-array marker [-]. *)
66module Index : sig
67
68 type t = [
69 | `Mem of string
70 (** [`Mem name] indexes into an object member with the given [name].
71 The name is unescaped (i.e., [/] and [~] appear literally). *)
72 | `Nth of int
73 (** [`Nth n] indexes into an array at position [n] (zero-based).
74 Must be non-negative and without leading zeros in string form
75 (except for [0] itself). *)
76 | `End
77 (** [`End] represents the [-] token, indicating the position after
78 the last element of an array. This is used for append operations
79 in {!Jsont_pointer.add} and similar mutation functions.
80 Evaluating a pointer containing [`End] with {!Jsont_pointer.get}
81 will raise an error since it refers to a nonexistent element. *)
82 ]
83
84 val pp : Format.formatter -> t -> unit
85 (** [pp] formats an index in JSON Pointer string notation. *)
86
87 val equal : t -> t -> bool
88 (** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *)
89
90 val compare : t -> t -> int
91 (** [compare i1 i2] is a total order on indices. *)
92
93 (** {2:jsont_conv Conversion with Jsont.Path} *)
94
95 val of_path_index : Jsont.Path.index -> t
96 (** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *)
97
98 val to_path_index : t -> Jsont.Path.index option
99 (** [to_path_index idx] converts to a {!Jsont.Path.index}.
100 Returns [None] for [`End] since it has no equivalent in
101 {!Jsont.Path}. *)
102end
103
104(** {1 Pointers} *)
105
106type t
107(** The type for JSON Pointers. A pointer is a sequence of {!Index.t}
108 values representing a path from the root of a JSON document to
109 a specific value. *)
110
111val root : t
112(** [root] is the empty pointer that references the whole document.
113 In string form this is [""]. *)
114
115val is_root : t -> bool
116(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
117
118val make : Index.t list -> t
119(** [make indices] creates a pointer from a list of indices.
120 The list is ordered from root to target (i.e., the first element
121 is the first step from the root). *)
122
123val indices : t -> Index.t list
124(** [indices p] returns the indices of [p] from root to target. *)
125
126val append : t -> Index.t -> t
127(** [append p idx] appends [idx] to the end of pointer [p]. *)
128
129val concat : t -> t -> t
130(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
131
132val parent : t -> t option
133(** [parent p] returns the parent pointer of [p], or [None] if [p]
134 is the {!root}. *)
135
136val last : t -> Index.t option
137(** [last p] returns the last index of [p], or [None] if [p] is
138 the {!root}. *)
139
140(** {2:parsing Parsing} *)
141
142val of_string : string -> t
143(** [of_string s] parses a JSON Pointer from its string representation.
144
145 The string must be either empty (representing the root) or start
146 with [/]. Each segment between [/] characters is unescaped as a
147 reference token. Segments that are valid non-negative integers
148 without leading zeros become [`Nth] indices; the string [-]
149 becomes [`End]; all others become [`Mem].
150
151 @raise Jsont.Error if [s] has invalid syntax:
152 - Non-empty string not starting with [/]
153 - Invalid escape sequence ([~] not followed by [0] or [1])
154 - Array index with leading zeros
155 - Array index that overflows [int] *)
156
157val of_string_result : string -> (t, string) result
158(** [of_string_result s] is like {!of_string} but returns a result
159 instead of raising. *)
160
161val of_uri_fragment : string -> t
162(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
163
164 This is like {!of_string} but first percent-decodes the string
165 according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}.
166 The leading [#] should {b not} be included in [s].
167
168 @raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
169
170val of_uri_fragment_result : string -> (t, string) result
171(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
172 a result instead of raising. *)
173
174(** {2:serializing Serializing} *)
175
176val to_string : t -> string
177(** [to_string p] serializes [p] to its JSON Pointer string representation.
178
179 Returns [""] for the root pointer, otherwise [/] followed by
180 escaped reference tokens joined by [/]. *)
181
182val to_uri_fragment : t -> string
183(** [to_uri_fragment p] serializes [p] to URI fragment form.
184
185 This is like {!to_string} but additionally percent-encodes
186 characters that are not allowed in URI fragments per RFC 3986.
187 The leading [#] is {b not} included in the result. *)
188
189val pp : Format.formatter -> t -> unit
190(** [pp] formats a pointer using {!to_string}. *)
191
192val pp_verbose : Format.formatter -> t -> unit
193(** [pp_verbose] formats a pointer showing its index structure.
194 For example, [/foo/0/-] is formatted as [[`Mem "foo"; `Nth 0; `End]].
195 Useful for debugging and understanding pointer structure. *)
196
197(** {2:comparison Comparison} *)
198
199val equal : t -> t -> bool
200(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *)
201
202val compare : t -> t -> int
203(** [compare p1 p2] is a total order on pointers, comparing indices
204 lexicographically. *)
205
206(** {2:jsont_path Conversion with Jsont.Path} *)
207
208val of_path : Jsont.Path.t -> t
209(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *)
210
211val to_path : t -> Jsont.Path.t option
212(** [to_path p] converts to a {!Jsont.Path.t}.
213 Returns [None] if [p] contains an [`End] index. *)
214
215val to_path_exn : t -> Jsont.Path.t
216(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error}
217 if conversion fails. *)
218
219(** {1 Evaluation}
220
221 These functions evaluate a JSON Pointer against a {!Jsont.json} value
222 to retrieve the referenced value. *)
223
224val get : t -> Jsont.json -> Jsont.json
225(** [get p json] retrieves the value at pointer [p] in [json].
226
227 @raise Jsont.Error if:
228 - The pointer references a nonexistent object member
229 - The pointer references an out-of-bounds array index
230 - The pointer contains [`End] (since [-] always refers
231 to a nonexistent element)
232 - An index type doesn't match the JSON value (e.g., [`Nth]
233 on an object) *)
234
235val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
236(** [get_result p json] is like {!get} but returns a result. *)
237
238val find : t -> Jsont.json -> Jsont.json option
239(** [find p json] is like {!get} but returns [None] instead of
240 raising when the pointer doesn't resolve to a value. *)
241
242(** {1 Mutation}
243
244 These functions modify a {!Jsont.json} value at a location specified
245 by a JSON Pointer. They are designed to support
246 {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch}
247 operations.
248
249 All mutation functions return a new JSON value with the modification
250 applied; they do not mutate the input. *)
251
252val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json
253(** [set p json ~value] replaces the value at pointer [p] with [value].
254
255 For [`End] on arrays, appends [value] to the end of the array.
256
257 @raise Jsont.Error if the pointer doesn't resolve to an existing
258 location (except for [`End] on arrays). *)
259
260val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
261(** [add p json ~value] adds [value] at the location specified by [p].
262
263 The behavior depends on the target:
264 {ul
265 {- For objects: If the member exists, it is replaced. If it doesn't
266 exist, a new member is added.}
267 {- For arrays with [`Nth]: Inserts [value] {e before} the
268 specified index, shifting subsequent elements. The index must be
269 valid (0 to length inclusive).}
270 {- For arrays with [`End]: Appends [value] to the array.}}
271
272 @raise Jsont.Error if:
273 - The parent of the target location doesn't exist
274 - An array index is out of bounds (except for [`End])
275 - The parent is not an object or array *)
276
277val remove : t -> Jsont.json -> Jsont.json
278(** [remove p json] removes the value at pointer [p].
279
280 For objects, removes the member. For arrays, removes the element
281 and shifts subsequent elements.
282
283 @raise Jsont.Error if:
284 - [p] is the root (cannot remove the root)
285 - The pointer doesn't resolve to an existing value
286 - The pointer contains [`End] *)
287
288val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json
289(** [replace p json ~value] replaces the value at pointer [p] with [value].
290
291 Unlike {!add}, this requires the target to exist.
292
293 @raise Jsont.Error if:
294 - The pointer doesn't resolve to an existing value
295 - The pointer contains [`End] *)
296
297val move : from:t -> path:t -> Jsont.json -> Jsont.json
298(** [move ~from ~path json] moves the value from [from] to [path].
299
300 This is equivalent to {!remove} at [from] followed by {!add}
301 at [path] with the removed value.
302
303 @raise Jsont.Error if:
304 - [from] doesn't resolve to a value
305 - [path] is a proper prefix of [from] (would create a cycle)
306 - Either pointer contains [`End] *)
307
308val copy : from:t -> path:t -> Jsont.json -> Jsont.json
309(** [copy ~from ~path json] copies the value from [from] to [path].
310
311 This is equivalent to {!get} at [from] followed by {!add}
312 at [path] with the retrieved value.
313
314 @raise Jsont.Error if:
315 - [from] doesn't resolve to a value
316 - Either pointer contains [`End] *)
317
318val test : t -> Jsont.json -> expected:Jsont.json -> bool
319(** [test p json ~expected] tests if the value at [p] equals [expected].
320
321 Returns [true] if the values are equal according to {!Jsont.Json.equal},
322 [false] otherwise. Also returns [false] (rather than raising) if the
323 pointer doesn't resolve.
324
325 Note: This implements the semantics of the JSON Patch "test" operation. *)
326
327(** {1 Jsont Integration}
328
329 These types and functions integrate JSON Pointers with the {!Jsont}
330 codec system. *)
331
332val jsont : t Jsont.t
333(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
334
335 On decode, parses a JSON string as a JSON Pointer using {!of_string}.
336 On encode, serializes a pointer to a JSON string using {!to_string}. *)
337
338val jsont_uri_fragment : t Jsont.t
339(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
340
341 On decode, parses using {!of_uri_fragment}.
342 On encode, serializes using {!to_uri_fragment}. *)
343
344(** {2:query Query combinators}
345
346 These combinators integrate with jsont's query system, allowing
347 JSON Pointers to be used with jsont codecs for typed access. *)
348
349val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t
350(** [path p t] decodes the value at pointer [p] using codec [t].
351
352 If [absent] is provided and the pointer doesn't resolve, returns
353 [absent] instead of raising.
354
355 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
356
357val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t
358(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
359
360 If [allow_absent] is [true] (default [false]), creates missing
361 intermediate structure as needed.
362
363 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
364
365val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t
366(** [update_path p t] recodes the value at pointer [p] with codec [t].
367
368 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
369
370val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t
371(** [delete_path p] removes the value at pointer [p].
372
373 If [allow_absent] is [true] (default [false]), does nothing if
374 the pointer doesn't resolve instead of raising. *)