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