···1+2+3+4+5+6+7+Internet Engineering Task Force (IETF) P. Bryan, Ed.
8+Request for Comments: 6901 Salesforce.com
9+Category: Standards Track K. Zyp
10+ISSN: 2070-1721 SitePen (USA)
11+ M. Nottingham, Ed.
12+ Akamai
13+ April 2013
14+15+16+ JavaScript Object Notation (JSON) Pointer
17+18+Abstract
19+20+ JSON Pointer defines a string syntax for identifying a specific value
21+ within a JavaScript Object Notation (JSON) document.
22+23+Status of This Memo
24+25+ This is an Internet Standards Track document.
26+27+ This document is a product of the Internet Engineering Task Force
28+ (IETF). It represents the consensus of the IETF community. It has
29+ received public review and has been approved for publication by the
30+ Internet Engineering Steering Group (IESG). Further information on
31+ Internet Standards is available in Section 2 of RFC 5741.
32+33+ Information about the current status of this document, any errata,
34+ and how to provide feedback on it may be obtained at
35+ http://www.rfc-editor.org/info/rfc6901.
36+37+Copyright Notice
38+39+ Copyright (c) 2013 IETF Trust and the persons identified as the
40+ document authors. All rights reserved.
41+42+ This document is subject to BCP 78 and the IETF Trust's Legal
43+ Provisions Relating to IETF Documents
44+ (http://trustee.ietf.org/license-info) in effect on the date of
45+ publication of this document. Please review these documents
46+ carefully, as they describe your rights and restrictions with respect
47+ to this document. Code Components extracted from this document must
48+ include Simplified BSD License text as described in Section 4.e of
49+ the Trust Legal Provisions and are provided without warranty as
50+ described in the Simplified BSD License.
51+52+53+54+55+56+57+58+Bryan, et al. Standards Track [Page 1]
59+60+RFC 6901 JSON Pointer April 2013
61+62+63+Table of Contents
64+65+ 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
66+ 2. Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . 2
67+ 3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
68+ 4. Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . 3
69+ 5. JSON String Representation . . . . . . . . . . . . . . . . . . 4
70+ 6. URI Fragment Identifier Representation . . . . . . . . . . . . 5
71+ 7. Error Handling . . . . . . . . . . . . . . . . . . . . . . . . 6
72+ 8. Security Considerations . . . . . . . . . . . . . . . . . . . . 6
73+ 9. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 7
74+ 10. References . . . . . . . . . . . . . . . . . . . . . . . . . . 7
75+ 10.1. Normative References . . . . . . . . . . . . . . . . . . . 7
76+ 10.2. Informative References . . . . . . . . . . . . . . . . . . 7
77+78+1. Introduction
79+80+ This specification defines JSON Pointer, a string syntax for
81+ identifying a specific value within a JavaScript Object Notation
82+ (JSON) document [RFC4627]. JSON Pointer is intended to be easily
83+ expressed in JSON string values as well as Uniform Resource
84+ Identifier (URI) [RFC3986] fragment identifiers.
85+86+2. Conventions
87+88+ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
89+ "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
90+ document are to be interpreted as described in [RFC2119].
91+92+ This specification expresses normative syntax rules using Augmented
93+ Backus-Naur Form (ABNF) [RFC5234] notation.
94+95+3. Syntax
96+97+ A JSON Pointer is a Unicode string (see [RFC4627], Section 3)
98+ containing a sequence of zero or more reference tokens, each prefixed
99+ by a '/' (%x2F) character.
100+101+ Because the characters '~' (%x7E) and '/' (%x2F) have special
102+ meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/'
103+ needs to be encoded as '~1' when these characters appear in a
104+ reference token.
105+106+107+108+109+110+111+112+113+114+Bryan, et al. Standards Track [Page 2]
115+116+RFC 6901 JSON Pointer April 2013
117+118+119+ The ABNF syntax of a JSON Pointer is:
120+121+ json-pointer = *( "/" reference-token )
122+ reference-token = *( unescaped / escaped )
123+ unescaped = %x00-2E / %x30-7D / %x7F-10FFFF
124+ ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
125+ escaped = "~" ( "0" / "1" )
126+ ; representing '~' and '/', respectively
127+128+ It is an error condition if a JSON Pointer value does not conform to
129+ this syntax (see Section 7).
130+131+ Note that JSON Pointers are specified in characters, not as bytes.
132+133+4. Evaluation
134+135+ Evaluation of a JSON Pointer begins with a reference to the root
136+ value of a JSON document and completes with a reference to some value
137+ within the document. Each reference token in the JSON Pointer is
138+ evaluated sequentially.
139+140+ Evaluation of each reference token begins by decoding any escaped
141+ character sequence. This is performed by first transforming any
142+ occurrence of the sequence '~1' to '/', and then transforming any
143+ occurrence of the sequence '~0' to '~'. By performing the
144+ substitutions in this order, an implementation avoids the error of
145+ turning '~01' first into '~1' and then into '/', which would be
146+ incorrect (the string '~01' correctly becomes '~1' after
147+ transformation).
148+149+ The reference token then modifies which value is referenced according
150+ to the following scheme:
151+152+ o If the currently referenced value is a JSON object, the new
153+ referenced value is the object member with the name identified by
154+ the reference token. The member name is equal to the token if it
155+ has the same number of Unicode characters as the token and their
156+ code points are byte-by-byte equal. No Unicode character
157+ normalization is performed. If a referenced member name is not
158+ unique in an object, the member that is referenced is undefined,
159+ and evaluation fails (see below).
160+161+162+163+164+165+166+167+168+169+170+Bryan, et al. Standards Track [Page 3]
171+172+RFC 6901 JSON Pointer April 2013
173+174+175+ o If the currently referenced value is a JSON array, the reference
176+ token MUST contain either:
177+178+ * characters comprised of digits (see ABNF below; note that
179+ leading zeros are not allowed) that represent an unsigned
180+ base-10 integer value, making the new referenced value the
181+ array element with the zero-based index identified by the
182+ token, or
183+184+ * exactly the single character "-", making the new referenced
185+ value the (nonexistent) member after the last array element.
186+187+ The ABNF syntax for array indices is:
188+189+ array-index = %x30 / ( %x31-39 *(%x30-39) )
190+ ; "0", or digits without a leading "0"
191+192+ Implementations will evaluate each reference token against the
193+ document's contents and will raise an error condition if it fails to
194+ resolve a concrete value for any of the JSON pointer's reference
195+ tokens. For example, if an array is referenced with a non-numeric
196+ token, an error condition will be raised. See Section 7 for details.
197+198+ Note that the use of the "-" character to index an array will always
199+ result in such an error condition because by definition it refers to
200+ a nonexistent array element. Thus, applications of JSON Pointer need
201+ to specify how that character is to be handled, if it is to be
202+ useful.
203+204+ Any error condition for which a specific action is not defined by the
205+ JSON Pointer application results in termination of evaluation.
206+207+5. JSON String Representation
208+209+ A JSON Pointer can be represented in a JSON string value. Per
210+ [RFC4627], Section 2.5, all instances of quotation mark '"' (%x22),
211+ reverse solidus '\' (%x5C), and control (%x00-1F) characters MUST be
212+ escaped.
213+214+ Note that before processing a JSON string as a JSON Pointer,
215+ backslash escape sequences must be unescaped.
216+217+218+219+220+221+222+223+224+225+226+Bryan, et al. Standards Track [Page 4]
227+228+RFC 6901 JSON Pointer April 2013
229+230+231+ For example, given the JSON document
232+233+ {
234+ "foo": ["bar", "baz"],
235+ "": 0,
236+ "a/b": 1,
237+ "c%d": 2,
238+ "e^f": 3,
239+ "g|h": 4,
240+ "i\\j": 5,
241+ "k\"l": 6,
242+ " ": 7,
243+ "m~n": 8
244+ }
245+246+ The following JSON strings evaluate to the accompanying values:
247+248+ "" // the whole document
249+ "/foo" ["bar", "baz"]
250+ "/foo/0" "bar"
251+ "/" 0
252+ "/a~1b" 1
253+ "/c%d" 2
254+ "/e^f" 3
255+ "/g|h" 4
256+ "/i\\j" 5
257+ "/k\"l" 6
258+ "/ " 7
259+ "/m~0n" 8
260+261+6. URI Fragment Identifier Representation
262+263+ A JSON Pointer can be represented in a URI fragment identifier by
264+ encoding it into octets using UTF-8 [RFC3629], while percent-encoding
265+ those characters not allowed by the fragment rule in [RFC3986].
266+267+ Note that a given media type needs to specify JSON Pointer as its
268+ fragment identifier syntax explicitly (usually, in its registration
269+ [RFC6838]). That is, just because a document is JSON does not imply
270+ that JSON Pointer can be used as its fragment identifier syntax. In
271+ particular, the fragment identifier syntax for application/json is
272+ not JSON Pointer.
273+274+275+276+277+278+279+280+281+282+Bryan, et al. Standards Track [Page 5]
283+284+RFC 6901 JSON Pointer April 2013
285+286+287+ Given the same example document as above, the following URI fragment
288+ identifiers evaluate to the accompanying values:
289+290+ # // the whole document
291+ #/foo ["bar", "baz"]
292+ #/foo/0 "bar"
293+ #/ 0
294+ #/a~1b 1
295+ #/c%25d 2
296+ #/e%5Ef 3
297+ #/g%7Ch 4
298+ #/i%5Cj 5
299+ #/k%22l 6
300+ #/%20 7
301+ #/m~0n 8
302+303+7. Error Handling
304+305+ In the event of an error condition, evaluation of the JSON Pointer
306+ fails to complete.
307+308+ Error conditions include, but are not limited to:
309+310+ o Invalid pointer syntax
311+312+ o A pointer that references a nonexistent value
313+314+ This specification does not define how errors are handled. An
315+ application of JSON Pointer SHOULD specify the impact and handling of
316+ each type of error.
317+318+ For example, some applications might stop pointer processing upon an
319+ error, while others may attempt to recover from missing values by
320+ inserting default ones.
321+322+8. Security Considerations
323+324+ A given JSON Pointer is not guaranteed to reference an actual JSON
325+ value. Therefore, applications using JSON Pointer should anticipate
326+ this situation by defining how a pointer that does not resolve ought
327+ to be handled.
328+329+ Note that JSON pointers can contain the NUL (Unicode U+0000)
330+ character. Care is needed not to misinterpret this character in
331+ programming languages that use NUL to mark the end of a string.
332+333+334+335+336+337+338+Bryan, et al. Standards Track [Page 6]
339+340+RFC 6901 JSON Pointer April 2013
341+342+343+9. Acknowledgements
344+345+ The following individuals contributed ideas, feedback, and wording to
346+ this specification:
347+348+ Mike Acar, Carsten Bormann, Tim Bray, Jacob Davies, Martin J.
349+ Duerst, Bjoern Hoehrmann, James H. Manger, Drew Perttula, and
350+ Julian Reschke.
351+352+10. References
353+354+10.1. Normative References
355+356+ [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
357+ Requirement Levels", BCP 14, RFC 2119, March 1997.
358+359+ [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO
360+ 10646", STD 63, RFC 3629, November 2003.
361+362+ [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
363+ Resource Identifier (URI): Generic Syntax", STD 66,
364+ RFC 3986, January 2005.
365+366+ [RFC4627] Crockford, D., "The application/json Media Type for
367+ JavaScript Object Notation (JSON)", RFC 4627, July 2006.
368+369+ [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax
370+ Specifications: ABNF", STD 68, RFC 5234, January 2008.
371+372+10.2. Informative References
373+374+ [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type
375+ Specifications and Registration Procedures", BCP 13,
376+ RFC 6838, January 2013.
377+378+379+380+381+382+383+384+385+386+387+388+389+390+391+392+393+394+Bryan, et al. Standards Track [Page 7]
395+396+RFC 6901 JSON Pointer April 2013
397+398+399+Authors' Addresses
400+401+ Paul C. Bryan (editor)
402+ Salesforce.com
403+404+ Phone: +1 604 783 1481
405+ EMail: pbryan@anode.ca
406+407+408+ Kris Zyp
409+ SitePen (USA)
410+411+ Phone: +1 650 968 8787
412+ EMail: kris@sitepen.com
413+414+415+ Mark Nottingham (editor)
416+ Akamai
417+418+ EMail: mnot@mnot.net
419+420+421+422+423+424+425+426+427+428+429+430+431+432+433+434+435+436+437+438+439+440+441+442+443+444+445+446+447+448+449+450+Bryan, et al. Standards Track [Page 8]
451+
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2024 The jsont programmers. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(* Token escaping/unescaping per RFC 6901 Section 3-4 *)
7+module Token = struct
8+ type t = string
9+10+ let escape s =
11+ let b = Buffer.create (String.length s) in
12+ String.iter (function
13+ | '~' -> Buffer.add_string b "~0"
14+ | '/' -> Buffer.add_string b "~1"
15+ | c -> Buffer.add_char b c
16+ ) s;
17+ Buffer.contents b
18+19+ let unescape s =
20+ let len = String.length s in
21+ let b = Buffer.create len in
22+ let rec loop i =
23+ if i >= len then Buffer.contents b
24+ else match s.[i] with
25+ | '~' when i + 1 >= len ->
26+ Jsont.Error.msgf Jsont.Meta.none
27+ "Invalid JSON Pointer: incomplete escape sequence at end"
28+ | '~' ->
29+ (match s.[i + 1] with
30+ | '0' -> Buffer.add_char b '~'; loop (i + 2)
31+ | '1' -> Buffer.add_char b '/'; loop (i + 2)
32+ | c ->
33+ Jsont.Error.msgf Jsont.Meta.none
34+ "Invalid JSON Pointer: invalid escape sequence ~%c" c)
35+ | c -> Buffer.add_char b c; loop (i + 1)
36+ in
37+ loop 0
38+39+ (* Check if a token is a valid array index per RFC 6901 ABNF:
40+ array-index = %x30 / ( %x31-39 *(%x30-39) )
41+ i.e., "0" or a non-zero digit followed by any digits *)
42+ let is_valid_array_index s =
43+ let len = String.length s in
44+ let is_digit c = c >= '0' && c <= '9' in
45+ if len = 0 then None
46+ else if len = 1 && s.[0] = '0' then Some 0
47+ else if s.[0] >= '1' && s.[0] <= '9' then
48+ let rec all_digits i =
49+ if i >= len then true
50+ else if is_digit s.[i] then all_digits (i + 1)
51+ else false
52+ in
53+ if all_digits 1 then int_of_string_opt s else None
54+ else None
55+end
56+57+(* Index type - represents how a token is interpreted in context *)
58+module Index = struct
59+ type t =
60+ | Mem of string
61+ | Nth of int
62+ | End
63+64+ let pp ppf = function
65+ | Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
66+ | Nth n -> Format.fprintf ppf "/%d" n
67+ | End -> Format.fprintf ppf "/-"
68+69+ let equal i1 i2 = match i1, i2 with
70+ | Mem s1, Mem s2 -> String.equal s1 s2
71+ | Nth n1, Nth n2 -> Int.equal n1 n2
72+ | End, End -> true
73+ | _ -> false
74+75+ let compare i1 i2 = match i1, i2 with
76+ | Mem s1, Mem s2 -> String.compare s1 s2
77+ | Mem _, _ -> -1
78+ | _, Mem _ -> 1
79+ | Nth n1, Nth n2 -> Int.compare n1 n2
80+ | Nth _, End -> -1
81+ | End, Nth _ -> 1
82+ | End, End -> 0
83+84+ let of_path_index (idx : Jsont.Path.index) : t =
85+ match idx with
86+ | Jsont.Path.Mem (s, _meta) -> Mem s
87+ | Jsont.Path.Nth (n, _meta) -> Nth n
88+89+ let to_path_index (idx : t) : Jsont.Path.index option =
90+ match idx with
91+ | Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none))
92+ | Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none))
93+ | End -> None
94+end
95+96+(* Internal representation: raw unescaped tokens.
97+ Per RFC 6901, interpretation as member name vs array index
98+ depends on the JSON value type at evaluation time. *)
99+module Segment = struct
100+ type t =
101+ | Token of string (* Unescaped reference token *)
102+ | End (* The "-" token for end-of-array *)
103+104+ let of_escaped_string s =
105+ if s = "-" then End
106+ else Token (Token.unescape s)
107+108+ let to_escaped_string = function
109+ | Token s -> Token.escape s
110+ | End -> "-"
111+112+ (* Convert to Index for a given JSON value type *)
113+ let to_index seg ~for_array =
114+ match seg with
115+ | End -> Index.End
116+ | Token s ->
117+ if for_array then
118+ match Token.is_valid_array_index s with
119+ | Some n -> Index.Nth n
120+ | None -> Index.Mem s (* Invalid index becomes member for error msg *)
121+ else
122+ Index.Mem s
123+124+ (* Convert from Index *)
125+ let of_index = function
126+ | Index.End -> End
127+ | Index.Mem s -> Token s
128+ | Index.Nth n -> Token (string_of_int n)
129+end
130+131+(* Pointer type - list of segments *)
132+type t = Segment.t list
133+134+let root = []
135+136+let is_root p = p = []
137+138+(* Convert indices to segments *)
139+let make indices = List.map Segment.of_index indices
140+141+(* Convert segments to indices, assuming array context for numeric tokens *)
142+let indices p = List.map (fun seg -> Segment.to_index seg ~for_array:true) p
143+144+let append p idx = p @ [Segment.of_index idx]
145+146+let concat p1 p2 = p1 @ p2
147+148+let parent p = match List.rev p with
149+ | [] -> None
150+ | _ :: rest -> Some (List.rev rest)
151+152+let last p = match List.rev p with
153+ | [] -> None
154+ | seg :: _ -> Some (Segment.to_index seg ~for_array:true)
155+156+(* Parsing *)
157+158+let of_string s =
159+ if s = "" then root
160+ else if s.[0] <> '/' then
161+ Jsont.Error.msgf Jsont.Meta.none
162+ "Invalid JSON Pointer: must be empty or start with '/': %s" s
163+ else
164+ let rest = String.sub s 1 (String.length s - 1) in
165+ let tokens = String.split_on_char '/' rest in
166+ List.map Segment.of_escaped_string tokens
167+168+let of_string_result s =
169+ try Ok (of_string s)
170+ with Jsont.Error (_, _, _) as e ->
171+ Error (Jsont.Error.to_string (match e with Jsont.Error e -> e | _ -> assert false))
172+173+(* URI fragment percent-decoding *)
174+let hex_value c =
175+ if c >= '0' && c <= '9' then Char.code c - Char.code '0'
176+ else if c >= 'A' && c <= 'F' then Char.code c - Char.code 'A' + 10
177+ else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10
178+ else -1
179+180+let percent_decode s =
181+ let len = String.length s in
182+ let b = Buffer.create len in
183+ let rec loop i =
184+ if i >= len then Buffer.contents b
185+ else match s.[i] with
186+ | '%' when i + 2 < len ->
187+ let h1 = hex_value s.[i + 1] in
188+ let h2 = hex_value s.[i + 2] in
189+ if h1 >= 0 && h2 >= 0 then begin
190+ Buffer.add_char b (Char.chr ((h1 lsl 4) lor h2));
191+ loop (i + 3)
192+ end else
193+ Jsont.Error.msgf Jsont.Meta.none
194+ "Invalid percent-encoding at position %d" i
195+ | '%' ->
196+ Jsont.Error.msgf Jsont.Meta.none
197+ "Incomplete percent-encoding at position %d" i
198+ | c -> Buffer.add_char b c; loop (i + 1)
199+ in
200+ loop 0
201+202+let of_uri_fragment s =
203+ of_string (percent_decode s)
204+205+let of_uri_fragment_result s =
206+ try Ok (of_uri_fragment s)
207+ with Jsont.Error (_, _, _) as e ->
208+ Error (Jsont.Error.to_string (match e with Jsont.Error e -> e | _ -> assert false))
209+210+(* Serialization *)
211+212+let to_string p =
213+ if p = [] then ""
214+ else
215+ let b = Buffer.create 64 in
216+ List.iter (fun seg ->
217+ Buffer.add_char b '/';
218+ Buffer.add_string b (Segment.to_escaped_string seg)
219+ ) p;
220+ Buffer.contents b
221+222+(* URI fragment percent-encoding *)
223+let needs_percent_encoding c =
224+ (* RFC 3986 fragment: unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" *)
225+ (* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" *)
226+ (* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *)
227+ not (
228+ (c >= 'A' && c <= 'Z') ||
229+ (c >= 'a' && c <= 'z') ||
230+ (c >= '0' && c <= '9') ||
231+ c = '-' || c = '.' || c = '_' || c = '~' ||
232+ c = '!' || c = '$' || c = '&' || c = '\'' ||
233+ c = '(' || c = ')' || c = '*' || c = '+' ||
234+ c = ',' || c = ';' || c = '=' ||
235+ c = ':' || c = '@' || c = '/' || c = '?'
236+ )
237+238+let hex_char n =
239+ if n < 10 then Char.chr (Char.code '0' + n)
240+ else Char.chr (Char.code 'A' + n - 10)
241+242+let percent_encode s =
243+ let b = Buffer.create (String.length s * 3) in
244+ String.iter (fun c ->
245+ if needs_percent_encoding c then begin
246+ let code = Char.code c in
247+ Buffer.add_char b '%';
248+ Buffer.add_char b (hex_char (code lsr 4));
249+ Buffer.add_char b (hex_char (code land 0xF))
250+ end else
251+ Buffer.add_char b c
252+ ) s;
253+ Buffer.contents b
254+255+let to_uri_fragment p =
256+ percent_encode (to_string p)
257+258+let pp ppf p =
259+ Format.pp_print_string ppf (to_string p)
260+261+(* Comparison *)
262+263+let segment_equal s1 s2 = match s1, s2 with
264+ | Segment.Token t1, Segment.Token t2 -> String.equal t1 t2
265+ | Segment.End, Segment.End -> true
266+ | _ -> false
267+268+let segment_compare s1 s2 = match s1, s2 with
269+ | Segment.Token t1, Segment.Token t2 -> String.compare t1 t2
270+ | Segment.Token _, Segment.End -> -1
271+ | Segment.End, Segment.Token _ -> 1
272+ | Segment.End, Segment.End -> 0
273+274+let equal p1 p2 =
275+ List.length p1 = List.length p2 &&
276+ List.for_all2 segment_equal p1 p2
277+278+let compare p1 p2 =
279+ let rec loop l1 l2 = match l1, l2 with
280+ | [], [] -> 0
281+ | [], _ -> -1
282+ | _, [] -> 1
283+ | h1 :: t1, h2 :: t2 ->
284+ let c = segment_compare h1 h2 in
285+ if c <> 0 then c else loop t1 t2
286+ in
287+ loop p1 p2
288+289+(* Path conversion *)
290+291+let segment_of_path_index (idx : Jsont.Path.index) : Segment.t =
292+ match idx with
293+ | Jsont.Path.Mem (s, _meta) -> Segment.Token s
294+ | Jsont.Path.Nth (n, _meta) -> Segment.Token (string_of_int n)
295+296+let of_path (p : Jsont.Path.t) : t =
297+ List.rev_map segment_of_path_index (Jsont.Path.rev_indices p)
298+299+let to_path p =
300+ let rec convert acc = function
301+ | [] -> Some acc
302+ | Segment.End :: _ -> None
303+ | Segment.Token s :: rest ->
304+ (* For path conversion, we need to decide if it's a member or index.
305+ We use array context for numeric tokens since Jsont.Path distinguishes. *)
306+ let acc' = match Token.is_valid_array_index s with
307+ | Some n -> Jsont.Path.nth ~meta:Jsont.Meta.none n acc
308+ | None -> Jsont.Path.mem ~meta:Jsont.Meta.none s acc
309+ in
310+ convert acc' rest
311+ in
312+ convert Jsont.Path.root p
313+314+let to_path_exn p =
315+ match to_path p with
316+ | Some path -> path
317+ | None ->
318+ Jsont.Error.msgf Jsont.Meta.none
319+ "Cannot convert JSON Pointer with '-' index to Jsont.Path"
320+321+(* Evaluation helpers *)
322+323+let json_sort_string (j : Jsont.json) =
324+ match j with
325+ | Null _ -> "null"
326+ | Bool _ -> "boolean"
327+ | Number _ -> "number"
328+ | String _ -> "string"
329+ | Array _ -> "array"
330+ | Object _ -> "object"
331+332+let get_member name (obj : Jsont.object') =
333+ List.find_opt (fun ((n, _), _) -> String.equal n name) obj
334+335+let get_nth n (arr : Jsont.json list) =
336+ if n < 0 || n >= List.length arr then None
337+ else Some (List.nth arr n)
338+339+(* Evaluation *)
340+341+let rec eval_get p json =
342+ match p with
343+ | [] -> json
344+ | Segment.End :: _ ->
345+ Jsont.Error.msgf (Jsont.Json.meta json)
346+ "JSON Pointer: '-' (end marker) refers to nonexistent array element"
347+ | Segment.Token token :: rest ->
348+ (match json with
349+ | Jsont.Object (members, _) ->
350+ (* For objects, token is always a member name *)
351+ (match get_member token members with
352+ | Some (_, value) -> eval_get rest value
353+ | None ->
354+ Jsont.Error.msgf (Jsont.Json.meta json)
355+ "JSON Pointer: member '%s' not found" token)
356+ | Jsont.Array (elements, _) ->
357+ (* For arrays, token must be a valid array index *)
358+ (match Token.is_valid_array_index token with
359+ | Some n ->
360+ (match get_nth n elements with
361+ | Some value -> eval_get rest value
362+ | None ->
363+ Jsont.Error.msgf (Jsont.Json.meta json)
364+ "JSON Pointer: index %d out of bounds (array has %d elements)"
365+ n (List.length elements))
366+ | None ->
367+ Jsont.Error.msgf (Jsont.Json.meta json)
368+ "JSON Pointer: invalid array index '%s'" token)
369+ | _ ->
370+ Jsont.Error.msgf (Jsont.Json.meta json)
371+ "JSON Pointer: cannot index into %s with '%s'"
372+ (json_sort_string json) token)
373+374+let get p json = eval_get p json
375+376+let get_result p json =
377+ try Ok (get p json)
378+ with Jsont.Error e -> Error e
379+380+let find p json =
381+ try Some (get p json)
382+ with Jsont.Error _ -> None
383+384+(* Mutation helpers *)
385+386+let set_member name value (obj : Jsont.object') : Jsont.object' =
387+ let found = ref false in
388+ let result = List.map (fun ((n, m), v) ->
389+ if String.equal n name then begin
390+ found := true;
391+ ((n, m), value)
392+ end else
393+ ((n, m), v)
394+ ) obj in
395+ if !found then result
396+ else obj @ [((name, Jsont.Meta.none), value)]
397+398+let remove_member name (obj : Jsont.object') : Jsont.object' =
399+ List.filter (fun ((n, _), _) -> not (String.equal n name)) obj
400+401+let insert_at n value lst =
402+ let rec loop i acc = function
403+ | [] when i = n -> List.rev (value :: acc)
404+ | [] -> List.rev acc (* shouldn't happen if n is valid *)
405+ | h :: t when i = n -> List.rev_append (value :: acc) (h :: t)
406+ | h :: t -> loop (i + 1) (h :: acc) t
407+ in
408+ loop 0 [] lst
409+410+let remove_at n lst =
411+ let rec loop i acc = function
412+ | [] -> List.rev acc
413+ | _ :: t when i = n -> List.rev_append acc t
414+ | h :: t -> loop (i + 1) (h :: acc) t
415+ in
416+ loop 0 [] lst
417+418+let replace_at n value lst =
419+ List.mapi (fun i v -> if i = n then value else v) lst
420+421+(* Mutation: set *)
422+423+let rec eval_set p value json =
424+ match p with
425+ | [] -> value
426+ | [Segment.End] ->
427+ (match json with
428+ | Jsont.Array (elements, meta) ->
429+ Jsont.Array (elements @ [value], meta)
430+ | _ ->
431+ Jsont.Error.msgf (Jsont.Json.meta json)
432+ "JSON Pointer: '-' can only be used on arrays, got %s"
433+ (json_sort_string json))
434+ | Segment.End :: _ ->
435+ Jsont.Error.msgf (Jsont.Json.meta json)
436+ "JSON Pointer: '-' (end marker) refers to nonexistent array element"
437+ | [Segment.Token token] ->
438+ (match json with
439+ | Jsont.Object (members, meta) ->
440+ if Option.is_some (get_member token members) then
441+ Jsont.Object (set_member token value members, meta)
442+ else
443+ Jsont.Error.msgf (Jsont.Json.meta json)
444+ "JSON Pointer: member '%s' not found for set" token
445+ | Jsont.Array (elements, meta) ->
446+ (match Token.is_valid_array_index token with
447+ | Some n when n >= 0 && n < List.length elements ->
448+ Jsont.Array (replace_at n value elements, meta)
449+ | Some n ->
450+ Jsont.Error.msgf (Jsont.Json.meta json)
451+ "JSON Pointer: index %d out of bounds for set" n
452+ | None ->
453+ Jsont.Error.msgf (Jsont.Json.meta json)
454+ "JSON Pointer: invalid array index '%s'" token)
455+ | _ ->
456+ Jsont.Error.msgf (Jsont.Json.meta json)
457+ "JSON Pointer: cannot set in %s" (json_sort_string json))
458+ | Segment.Token token :: rest ->
459+ (match json with
460+ | Jsont.Object (members, meta) ->
461+ (match get_member token members with
462+ | Some (_, child) ->
463+ Jsont.Object (set_member token (eval_set rest value child) members, meta)
464+ | None ->
465+ Jsont.Error.msgf (Jsont.Json.meta json)
466+ "JSON Pointer: member '%s' not found" token)
467+ | Jsont.Array (elements, meta) ->
468+ (match Token.is_valid_array_index token with
469+ | Some n ->
470+ (match get_nth n elements with
471+ | Some child ->
472+ Jsont.Array (replace_at n (eval_set rest value child) elements, meta)
473+ | None ->
474+ Jsont.Error.msgf (Jsont.Json.meta json)
475+ "JSON Pointer: index %d out of bounds" n)
476+ | None ->
477+ Jsont.Error.msgf (Jsont.Json.meta json)
478+ "JSON Pointer: invalid array index '%s'" token)
479+ | _ ->
480+ Jsont.Error.msgf (Jsont.Json.meta json)
481+ "JSON Pointer: cannot navigate through %s" (json_sort_string json))
482+483+let set p json ~value = eval_set p value json
484+485+(* Mutation: add (RFC 6902 semantics) *)
486+487+let rec eval_add p value json =
488+ match p with
489+ | [] -> value
490+ | [Segment.End] ->
491+ (match json with
492+ | Jsont.Array (elements, meta) ->
493+ Jsont.Array (elements @ [value], meta)
494+ | _ ->
495+ Jsont.Error.msgf (Jsont.Json.meta json)
496+ "JSON Pointer: '-' can only be used on arrays, got %s"
497+ (json_sort_string json))
498+ | Segment.End :: _ ->
499+ Jsont.Error.msgf (Jsont.Json.meta json)
500+ "JSON Pointer: '-' in non-final position"
501+ | [Segment.Token token] ->
502+ (match json with
503+ | Jsont.Object (members, meta) ->
504+ (* For objects, add/replace member *)
505+ Jsont.Object (set_member token value members, meta)
506+ | Jsont.Array (elements, meta) ->
507+ (* For arrays, insert at index *)
508+ (match Token.is_valid_array_index token with
509+ | Some n ->
510+ let len = List.length elements in
511+ if n >= 0 && n <= len then
512+ Jsont.Array (insert_at n value elements, meta)
513+ else
514+ Jsont.Error.msgf (Jsont.Json.meta json)
515+ "JSON Pointer: index %d out of bounds for add (array has %d elements)"
516+ n len
517+ | None ->
518+ Jsont.Error.msgf (Jsont.Json.meta json)
519+ "JSON Pointer: invalid array index '%s'" token)
520+ | _ ->
521+ Jsont.Error.msgf (Jsont.Json.meta json)
522+ "JSON Pointer: cannot add to %s" (json_sort_string json))
523+ | Segment.Token token :: rest ->
524+ (match json with
525+ | Jsont.Object (members, meta) ->
526+ (match get_member token members with
527+ | Some (_, child) ->
528+ Jsont.Object (set_member token (eval_add rest value child) members, meta)
529+ | None ->
530+ Jsont.Error.msgf (Jsont.Json.meta json)
531+ "JSON Pointer: member '%s' not found" token)
532+ | Jsont.Array (elements, meta) ->
533+ (match Token.is_valid_array_index token with
534+ | Some n ->
535+ (match get_nth n elements with
536+ | Some child ->
537+ Jsont.Array (replace_at n (eval_add rest value child) elements, meta)
538+ | None ->
539+ Jsont.Error.msgf (Jsont.Json.meta json)
540+ "JSON Pointer: index %d out of bounds" n)
541+ | None ->
542+ Jsont.Error.msgf (Jsont.Json.meta json)
543+ "JSON Pointer: invalid array index '%s'" token)
544+ | _ ->
545+ Jsont.Error.msgf (Jsont.Json.meta json)
546+ "JSON Pointer: cannot navigate through %s" (json_sort_string json))
547+548+let add p json ~value = eval_add p value json
549+550+(* Mutation: remove *)
551+552+let rec eval_remove p json =
553+ match p with
554+ | [] ->
555+ Jsont.Error.msgf Jsont.Meta.none
556+ "JSON Pointer: cannot remove root document"
557+ | [Segment.End] ->
558+ Jsont.Error.msgf (Jsont.Json.meta json)
559+ "JSON Pointer: '-' refers to nonexistent element"
560+ | Segment.End :: _ ->
561+ Jsont.Error.msgf (Jsont.Json.meta json)
562+ "JSON Pointer: '-' in non-final position"
563+ | [Segment.Token token] ->
564+ (match json with
565+ | Jsont.Object (members, meta) ->
566+ if Option.is_some (get_member token members) then
567+ Jsont.Object (remove_member token members, meta)
568+ else
569+ Jsont.Error.msgf (Jsont.Json.meta json)
570+ "JSON Pointer: member '%s' not found for remove" token
571+ | Jsont.Array (elements, meta) ->
572+ (match Token.is_valid_array_index token with
573+ | Some n when n >= 0 && n < List.length elements ->
574+ Jsont.Array (remove_at n elements, meta)
575+ | Some n ->
576+ Jsont.Error.msgf (Jsont.Json.meta json)
577+ "JSON Pointer: index %d out of bounds for remove" n
578+ | None ->
579+ Jsont.Error.msgf (Jsont.Json.meta json)
580+ "JSON Pointer: invalid array index '%s'" token)
581+ | _ ->
582+ Jsont.Error.msgf (Jsont.Json.meta json)
583+ "JSON Pointer: cannot remove from %s" (json_sort_string json))
584+ | Segment.Token token :: rest ->
585+ (match json with
586+ | Jsont.Object (members, meta) ->
587+ (match get_member token members with
588+ | Some (_, child) ->
589+ Jsont.Object (set_member token (eval_remove rest child) members, meta)
590+ | None ->
591+ Jsont.Error.msgf (Jsont.Json.meta json)
592+ "JSON Pointer: member '%s' not found" token)
593+ | Jsont.Array (elements, meta) ->
594+ (match Token.is_valid_array_index token with
595+ | Some n ->
596+ (match get_nth n elements with
597+ | Some child ->
598+ Jsont.Array (replace_at n (eval_remove rest child) elements, meta)
599+ | None ->
600+ Jsont.Error.msgf (Jsont.Json.meta json)
601+ "JSON Pointer: index %d out of bounds" n)
602+ | None ->
603+ Jsont.Error.msgf (Jsont.Json.meta json)
604+ "JSON Pointer: invalid array index '%s'" token)
605+ | _ ->
606+ Jsont.Error.msgf (Jsont.Json.meta json)
607+ "JSON Pointer: cannot navigate through %s" (json_sort_string json))
608+609+let remove p json = eval_remove p json
610+611+(* Mutation: replace *)
612+613+let replace p json ~value =
614+ (* Replace requires the target to exist, unlike add *)
615+ let _ = get p json in (* Will raise if not found *)
616+ eval_set p value json
617+618+(* Mutation: move *)
619+620+let is_prefix_of p1 p2 =
621+ let rec loop l1 l2 = match l1, l2 with
622+ | [], _ -> true
623+ | _, [] -> false
624+ | h1 :: t1, h2 :: t2 ->
625+ segment_equal h1 h2 && loop t1 t2
626+ in
627+ loop p1 p2
628+629+let move ~from ~path json =
630+ (* Check for cycle: path cannot be a proper prefix of from *)
631+ if is_prefix_of path from && not (equal path from) then
632+ Jsont.Error.msgf Jsont.Meta.none
633+ "JSON Pointer: move would create cycle (path is prefix of from)";
634+ let value = get from json in
635+ let json' = remove from json in
636+ add path json' ~value
637+638+(* Mutation: copy *)
639+640+let copy ~from ~path json =
641+ let value = get from json in
642+ add path json ~value
643+644+(* Mutation: test *)
645+646+let test p json ~expected =
647+ match find p json with
648+ | None -> false
649+ | Some value -> Jsont.Json.equal value expected
650+651+(* Jsont codec *)
652+653+let jsont : t Jsont.t =
654+ let dec _meta s = of_string s in
655+ let enc p = to_string p in
656+ Jsont.Base.string (Jsont.Base.map
657+ ~kind:"JSON Pointer"
658+ ~doc:"RFC 6901 JSON Pointer"
659+ ~dec ~enc ())
660+661+let jsont_uri_fragment : t Jsont.t =
662+ let dec _meta s = of_uri_fragment s in
663+ let enc p = to_uri_fragment p in
664+ Jsont.Base.string (Jsont.Base.map
665+ ~kind:"JSON Pointer (URI fragment)"
666+ ~doc:"RFC 6901 JSON Pointer in URI fragment encoding"
667+ ~dec ~enc ())
668+669+(* Query combinators *)
670+671+let path ?absent p t =
672+ let dec json =
673+ match find p json with
674+ | Some value ->
675+ (match Jsont.Json.decode' t value with
676+ | Ok v -> v
677+ | Error e -> raise (Jsont.Error e))
678+ | None ->
679+ match absent with
680+ | Some v -> v
681+ | None ->
682+ Jsont.Error.msgf Jsont.Meta.none
683+ "JSON Pointer %s: path not found" (to_string p)
684+ in
685+ Jsont.map Jsont.json ~dec ~enc:(fun _ ->
686+ Jsont.Error.msgf Jsont.Meta.none "path: encode not supported")
687+688+let set_path ?(allow_absent = false) t p v =
689+ let encoded = match Jsont.Json.encode' t v with
690+ | Ok json -> json
691+ | Error e -> raise (Jsont.Error e)
692+ in
693+ let dec json =
694+ if allow_absent then
695+ add p json ~value:encoded
696+ else
697+ set p json ~value:encoded
698+ in
699+ Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
700+701+let update_path ?absent p t =
702+ let dec json =
703+ let value = match find p json with
704+ | Some v -> v
705+ | None ->
706+ match absent with
707+ | Some v ->
708+ (match Jsont.Json.encode' t v with
709+ | Ok j -> j
710+ | Error e -> raise (Jsont.Error e))
711+ | None ->
712+ Jsont.Error.msgf Jsont.Meta.none
713+ "JSON Pointer %s: path not found" (to_string p)
714+ in
715+ let decoded = match Jsont.Json.decode' t value with
716+ | Ok v -> v
717+ | Error e -> raise (Jsont.Error e)
718+ in
719+ let re_encoded = match Jsont.Json.encode' t decoded with
720+ | Ok j -> j
721+ | Error e -> raise (Jsont.Error e)
722+ in
723+ set p json ~value:re_encoded
724+ in
725+ Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
726+727+let delete_path ?(allow_absent = false) p =
728+ let dec json =
729+ if allow_absent then
730+ match find p json with
731+ | Some _ -> remove p json
732+ | None -> json
733+ else
734+ remove p json
735+ in
736+ Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2024 The jsont programmers. 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 [/]. *)
43+module 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]). *)
59+end
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 [-]. *)
66+module 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}. *)
101+end
102+103+(** {1 Pointers} *)
104+105+type 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+110+val root : t
111+(** [root] is the empty pointer that references the whole document.
112+ In string form this is [""]. *)
113+114+val is_root : t -> bool
115+(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
116+117+val 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+122+val indices : t -> Index.t list
123+(** [indices p] returns the indices of [p] from root to target. *)
124+125+val append : t -> Index.t -> t
126+(** [append p idx] appends [idx] to the end of pointer [p]. *)
127+128+val concat : t -> t -> t
129+(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
130+131+val parent : t -> t option
132+(** [parent p] returns the parent pointer of [p], or [None] if [p]
133+ is the {!root}. *)
134+135+val 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+141+val 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+156+val 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+160+val 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+169+val 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+175+val 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+181+val 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+188+val pp : Format.formatter -> t -> unit
189+(** [pp] formats a pointer using {!to_string}. *)
190+191+(** {2:comparison Comparison} *)
192+193+val equal : t -> t -> bool
194+(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *)
195+196+val 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+202+val of_path : Jsont.Path.t -> t
203+(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *)
204+205+val 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+209+val 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+218+val 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+229+val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
230+(** [get_result p json] is like {!get} but returns a result. *)
231+232+val 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+246+val 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+254+val 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+271+val 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+282+val 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+291+val 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+302+val 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+312+val 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+326+val 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+332+val 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+343+val 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+351+val 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+359+val 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+364+val 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. *)
···1+# Evaluation error tests
2+# Format: pointer -> error type
3+4+# Nonexistent members
5+/nonexistent -> member not found
6+/foo/nonexistent -> type mismatch (array, not object)
7+8+# Out of bounds array indices
9+/foo/2 -> index out of bounds
10+/foo/99 -> index out of bounds
11+12+# Type mismatches
13+/foo/0/bar -> type mismatch (string, not object)
14+/ /0 -> type mismatch (number, not array)
15+16+# End marker always errors on get
17+/foo/- -> end marker not allowed
18+/- -> end marker not allowed
···1+# Invalid JSON Pointer parsing tests
2+# Format: pointer -> error description
3+4+# Must start with / if non-empty
5+foo -> must start with /
6+a/b -> must start with /
7+0 -> must start with /
8+- -> must start with /
9+10+# Invalid escape sequences
11+/~ -> incomplete escape
12+/~2 -> invalid escape ~2
13+/~a -> invalid escape ~a
14+/foo~bar -> invalid escape ~b
15+/~~ -> invalid escape ~~
16+17+# Leading zeros in array indices (RFC 6901 Section 4)
18+/00 -> leading zero
19+/01 -> leading zero
20+/007 -> leading zero
21+22+# Negative indices not allowed
23+/-1 -> not a valid index
24+/-42 -> not a valid index