···11+22+33+44+55+66+77+Internet Engineering Task Force (IETF) P. Bryan, Ed.
88+Request for Comments: 6901 Salesforce.com
99+Category: Standards Track K. Zyp
1010+ISSN: 2070-1721 SitePen (USA)
1111+ M. Nottingham, Ed.
1212+ Akamai
1313+ April 2013
1414+1515+1616+ JavaScript Object Notation (JSON) Pointer
1717+1818+Abstract
1919+2020+ JSON Pointer defines a string syntax for identifying a specific value
2121+ within a JavaScript Object Notation (JSON) document.
2222+2323+Status of This Memo
2424+2525+ This is an Internet Standards Track document.
2626+2727+ This document is a product of the Internet Engineering Task Force
2828+ (IETF). It represents the consensus of the IETF community. It has
2929+ received public review and has been approved for publication by the
3030+ Internet Engineering Steering Group (IESG). Further information on
3131+ Internet Standards is available in Section 2 of RFC 5741.
3232+3333+ Information about the current status of this document, any errata,
3434+ and how to provide feedback on it may be obtained at
3535+ http://www.rfc-editor.org/info/rfc6901.
3636+3737+Copyright Notice
3838+3939+ Copyright (c) 2013 IETF Trust and the persons identified as the
4040+ document authors. All rights reserved.
4141+4242+ This document is subject to BCP 78 and the IETF Trust's Legal
4343+ Provisions Relating to IETF Documents
4444+ (http://trustee.ietf.org/license-info) in effect on the date of
4545+ publication of this document. Please review these documents
4646+ carefully, as they describe your rights and restrictions with respect
4747+ to this document. Code Components extracted from this document must
4848+ include Simplified BSD License text as described in Section 4.e of
4949+ the Trust Legal Provisions and are provided without warranty as
5050+ described in the Simplified BSD License.
5151+5252+5353+5454+5555+5656+5757+5858+Bryan, et al. Standards Track [Page 1]
5959+6060+RFC 6901 JSON Pointer April 2013
6161+6262+6363+Table of Contents
6464+6565+ 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
6666+ 2. Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . 2
6767+ 3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
6868+ 4. Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . 3
6969+ 5. JSON String Representation . . . . . . . . . . . . . . . . . . 4
7070+ 6. URI Fragment Identifier Representation . . . . . . . . . . . . 5
7171+ 7. Error Handling . . . . . . . . . . . . . . . . . . . . . . . . 6
7272+ 8. Security Considerations . . . . . . . . . . . . . . . . . . . . 6
7373+ 9. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 7
7474+ 10. References . . . . . . . . . . . . . . . . . . . . . . . . . . 7
7575+ 10.1. Normative References . . . . . . . . . . . . . . . . . . . 7
7676+ 10.2. Informative References . . . . . . . . . . . . . . . . . . 7
7777+7878+1. Introduction
7979+8080+ This specification defines JSON Pointer, a string syntax for
8181+ identifying a specific value within a JavaScript Object Notation
8282+ (JSON) document [RFC4627]. JSON Pointer is intended to be easily
8383+ expressed in JSON string values as well as Uniform Resource
8484+ Identifier (URI) [RFC3986] fragment identifiers.
8585+8686+2. Conventions
8787+8888+ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
8989+ "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
9090+ document are to be interpreted as described in [RFC2119].
9191+9292+ This specification expresses normative syntax rules using Augmented
9393+ Backus-Naur Form (ABNF) [RFC5234] notation.
9494+9595+3. Syntax
9696+9797+ A JSON Pointer is a Unicode string (see [RFC4627], Section 3)
9898+ containing a sequence of zero or more reference tokens, each prefixed
9999+ by a '/' (%x2F) character.
100100+101101+ Because the characters '~' (%x7E) and '/' (%x2F) have special
102102+ meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/'
103103+ needs to be encoded as '~1' when these characters appear in a
104104+ reference token.
105105+106106+107107+108108+109109+110110+111111+112112+113113+114114+Bryan, et al. Standards Track [Page 2]
115115+116116+RFC 6901 JSON Pointer April 2013
117117+118118+119119+ The ABNF syntax of a JSON Pointer is:
120120+121121+ json-pointer = *( "/" reference-token )
122122+ reference-token = *( unescaped / escaped )
123123+ unescaped = %x00-2E / %x30-7D / %x7F-10FFFF
124124+ ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
125125+ escaped = "~" ( "0" / "1" )
126126+ ; representing '~' and '/', respectively
127127+128128+ It is an error condition if a JSON Pointer value does not conform to
129129+ this syntax (see Section 7).
130130+131131+ Note that JSON Pointers are specified in characters, not as bytes.
132132+133133+4. Evaluation
134134+135135+ Evaluation of a JSON Pointer begins with a reference to the root
136136+ value of a JSON document and completes with a reference to some value
137137+ within the document. Each reference token in the JSON Pointer is
138138+ evaluated sequentially.
139139+140140+ Evaluation of each reference token begins by decoding any escaped
141141+ character sequence. This is performed by first transforming any
142142+ occurrence of the sequence '~1' to '/', and then transforming any
143143+ occurrence of the sequence '~0' to '~'. By performing the
144144+ substitutions in this order, an implementation avoids the error of
145145+ turning '~01' first into '~1' and then into '/', which would be
146146+ incorrect (the string '~01' correctly becomes '~1' after
147147+ transformation).
148148+149149+ The reference token then modifies which value is referenced according
150150+ to the following scheme:
151151+152152+ o If the currently referenced value is a JSON object, the new
153153+ referenced value is the object member with the name identified by
154154+ the reference token. The member name is equal to the token if it
155155+ has the same number of Unicode characters as the token and their
156156+ code points are byte-by-byte equal. No Unicode character
157157+ normalization is performed. If a referenced member name is not
158158+ unique in an object, the member that is referenced is undefined,
159159+ and evaluation fails (see below).
160160+161161+162162+163163+164164+165165+166166+167167+168168+169169+170170+Bryan, et al. Standards Track [Page 3]
171171+172172+RFC 6901 JSON Pointer April 2013
173173+174174+175175+ o If the currently referenced value is a JSON array, the reference
176176+ token MUST contain either:
177177+178178+ * characters comprised of digits (see ABNF below; note that
179179+ leading zeros are not allowed) that represent an unsigned
180180+ base-10 integer value, making the new referenced value the
181181+ array element with the zero-based index identified by the
182182+ token, or
183183+184184+ * exactly the single character "-", making the new referenced
185185+ value the (nonexistent) member after the last array element.
186186+187187+ The ABNF syntax for array indices is:
188188+189189+ array-index = %x30 / ( %x31-39 *(%x30-39) )
190190+ ; "0", or digits without a leading "0"
191191+192192+ Implementations will evaluate each reference token against the
193193+ document's contents and will raise an error condition if it fails to
194194+ resolve a concrete value for any of the JSON pointer's reference
195195+ tokens. For example, if an array is referenced with a non-numeric
196196+ token, an error condition will be raised. See Section 7 for details.
197197+198198+ Note that the use of the "-" character to index an array will always
199199+ result in such an error condition because by definition it refers to
200200+ a nonexistent array element. Thus, applications of JSON Pointer need
201201+ to specify how that character is to be handled, if it is to be
202202+ useful.
203203+204204+ Any error condition for which a specific action is not defined by the
205205+ JSON Pointer application results in termination of evaluation.
206206+207207+5. JSON String Representation
208208+209209+ A JSON Pointer can be represented in a JSON string value. Per
210210+ [RFC4627], Section 2.5, all instances of quotation mark '"' (%x22),
211211+ reverse solidus '\' (%x5C), and control (%x00-1F) characters MUST be
212212+ escaped.
213213+214214+ Note that before processing a JSON string as a JSON Pointer,
215215+ backslash escape sequences must be unescaped.
216216+217217+218218+219219+220220+221221+222222+223223+224224+225225+226226+Bryan, et al. Standards Track [Page 4]
227227+228228+RFC 6901 JSON Pointer April 2013
229229+230230+231231+ For example, given the JSON document
232232+233233+ {
234234+ "foo": ["bar", "baz"],
235235+ "": 0,
236236+ "a/b": 1,
237237+ "c%d": 2,
238238+ "e^f": 3,
239239+ "g|h": 4,
240240+ "i\\j": 5,
241241+ "k\"l": 6,
242242+ " ": 7,
243243+ "m~n": 8
244244+ }
245245+246246+ The following JSON strings evaluate to the accompanying values:
247247+248248+ "" // the whole document
249249+ "/foo" ["bar", "baz"]
250250+ "/foo/0" "bar"
251251+ "/" 0
252252+ "/a~1b" 1
253253+ "/c%d" 2
254254+ "/e^f" 3
255255+ "/g|h" 4
256256+ "/i\\j" 5
257257+ "/k\"l" 6
258258+ "/ " 7
259259+ "/m~0n" 8
260260+261261+6. URI Fragment Identifier Representation
262262+263263+ A JSON Pointer can be represented in a URI fragment identifier by
264264+ encoding it into octets using UTF-8 [RFC3629], while percent-encoding
265265+ those characters not allowed by the fragment rule in [RFC3986].
266266+267267+ Note that a given media type needs to specify JSON Pointer as its
268268+ fragment identifier syntax explicitly (usually, in its registration
269269+ [RFC6838]). That is, just because a document is JSON does not imply
270270+ that JSON Pointer can be used as its fragment identifier syntax. In
271271+ particular, the fragment identifier syntax for application/json is
272272+ not JSON Pointer.
273273+274274+275275+276276+277277+278278+279279+280280+281281+282282+Bryan, et al. Standards Track [Page 5]
283283+284284+RFC 6901 JSON Pointer April 2013
285285+286286+287287+ Given the same example document as above, the following URI fragment
288288+ identifiers evaluate to the accompanying values:
289289+290290+ # // the whole document
291291+ #/foo ["bar", "baz"]
292292+ #/foo/0 "bar"
293293+ #/ 0
294294+ #/a~1b 1
295295+ #/c%25d 2
296296+ #/e%5Ef 3
297297+ #/g%7Ch 4
298298+ #/i%5Cj 5
299299+ #/k%22l 6
300300+ #/%20 7
301301+ #/m~0n 8
302302+303303+7. Error Handling
304304+305305+ In the event of an error condition, evaluation of the JSON Pointer
306306+ fails to complete.
307307+308308+ Error conditions include, but are not limited to:
309309+310310+ o Invalid pointer syntax
311311+312312+ o A pointer that references a nonexistent value
313313+314314+ This specification does not define how errors are handled. An
315315+ application of JSON Pointer SHOULD specify the impact and handling of
316316+ each type of error.
317317+318318+ For example, some applications might stop pointer processing upon an
319319+ error, while others may attempt to recover from missing values by
320320+ inserting default ones.
321321+322322+8. Security Considerations
323323+324324+ A given JSON Pointer is not guaranteed to reference an actual JSON
325325+ value. Therefore, applications using JSON Pointer should anticipate
326326+ this situation by defining how a pointer that does not resolve ought
327327+ to be handled.
328328+329329+ Note that JSON pointers can contain the NUL (Unicode U+0000)
330330+ character. Care is needed not to misinterpret this character in
331331+ programming languages that use NUL to mark the end of a string.
332332+333333+334334+335335+336336+337337+338338+Bryan, et al. Standards Track [Page 6]
339339+340340+RFC 6901 JSON Pointer April 2013
341341+342342+343343+9. Acknowledgements
344344+345345+ The following individuals contributed ideas, feedback, and wording to
346346+ this specification:
347347+348348+ Mike Acar, Carsten Bormann, Tim Bray, Jacob Davies, Martin J.
349349+ Duerst, Bjoern Hoehrmann, James H. Manger, Drew Perttula, and
350350+ Julian Reschke.
351351+352352+10. References
353353+354354+10.1. Normative References
355355+356356+ [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
357357+ Requirement Levels", BCP 14, RFC 2119, March 1997.
358358+359359+ [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO
360360+ 10646", STD 63, RFC 3629, November 2003.
361361+362362+ [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
363363+ Resource Identifier (URI): Generic Syntax", STD 66,
364364+ RFC 3986, January 2005.
365365+366366+ [RFC4627] Crockford, D., "The application/json Media Type for
367367+ JavaScript Object Notation (JSON)", RFC 4627, July 2006.
368368+369369+ [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax
370370+ Specifications: ABNF", STD 68, RFC 5234, January 2008.
371371+372372+10.2. Informative References
373373+374374+ [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type
375375+ Specifications and Registration Procedures", BCP 13,
376376+ RFC 6838, January 2013.
377377+378378+379379+380380+381381+382382+383383+384384+385385+386386+387387+388388+389389+390390+391391+392392+393393+394394+Bryan, et al. Standards Track [Page 7]
395395+396396+RFC 6901 JSON Pointer April 2013
397397+398398+399399+Authors' Addresses
400400+401401+ Paul C. Bryan (editor)
402402+ Salesforce.com
403403+404404+ Phone: +1 604 783 1481
405405+ EMail: pbryan@anode.ca
406406+407407+408408+ Kris Zyp
409409+ SitePen (USA)
410410+411411+ Phone: +1 650 968 8787
412412+ EMail: kris@sitepen.com
413413+414414+415415+ Mark Nottingham (editor)
416416+ Akamai
417417+418418+ EMail: mnot@mnot.net
419419+420420+421421+422422+423423+424424+425425+426426+427427+428428+429429+430430+431431+432432+433433+434434+435435+436436+437437+438438+439439+440440+441441+442442+443443+444444+445445+446446+447447+448448+449449+450450+Bryan, et al. Standards Track [Page 8]
451451+
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2024 The jsont programmers. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(* Token escaping/unescaping per RFC 6901 Section 3-4 *)
77+module Token = struct
88+ type t = string
99+1010+ let escape s =
1111+ let b = Buffer.create (String.length s) in
1212+ String.iter (function
1313+ | '~' -> Buffer.add_string b "~0"
1414+ | '/' -> Buffer.add_string b "~1"
1515+ | c -> Buffer.add_char b c
1616+ ) s;
1717+ Buffer.contents b
1818+1919+ let unescape s =
2020+ let len = String.length s in
2121+ let b = Buffer.create len in
2222+ let rec loop i =
2323+ if i >= len then Buffer.contents b
2424+ else match s.[i] with
2525+ | '~' when i + 1 >= len ->
2626+ Jsont.Error.msgf Jsont.Meta.none
2727+ "Invalid JSON Pointer: incomplete escape sequence at end"
2828+ | '~' ->
2929+ (match s.[i + 1] with
3030+ | '0' -> Buffer.add_char b '~'; loop (i + 2)
3131+ | '1' -> Buffer.add_char b '/'; loop (i + 2)
3232+ | c ->
3333+ Jsont.Error.msgf Jsont.Meta.none
3434+ "Invalid JSON Pointer: invalid escape sequence ~%c" c)
3535+ | c -> Buffer.add_char b c; loop (i + 1)
3636+ in
3737+ loop 0
3838+3939+ (* Check if a token is a valid array index per RFC 6901 ABNF:
4040+ array-index = %x30 / ( %x31-39 *(%x30-39) )
4141+ i.e., "0" or a non-zero digit followed by any digits *)
4242+ let is_valid_array_index s =
4343+ let len = String.length s in
4444+ let is_digit c = c >= '0' && c <= '9' in
4545+ if len = 0 then None
4646+ else if len = 1 && s.[0] = '0' then Some 0
4747+ else if s.[0] >= '1' && s.[0] <= '9' then
4848+ let rec all_digits i =
4949+ if i >= len then true
5050+ else if is_digit s.[i] then all_digits (i + 1)
5151+ else false
5252+ in
5353+ if all_digits 1 then int_of_string_opt s else None
5454+ else None
5555+end
5656+5757+(* Index type - represents how a token is interpreted in context *)
5858+module Index = struct
5959+ type t =
6060+ | Mem of string
6161+ | Nth of int
6262+ | End
6363+6464+ let pp ppf = function
6565+ | Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
6666+ | Nth n -> Format.fprintf ppf "/%d" n
6767+ | End -> Format.fprintf ppf "/-"
6868+6969+ let equal i1 i2 = match i1, i2 with
7070+ | Mem s1, Mem s2 -> String.equal s1 s2
7171+ | Nth n1, Nth n2 -> Int.equal n1 n2
7272+ | End, End -> true
7373+ | _ -> false
7474+7575+ let compare i1 i2 = match i1, i2 with
7676+ | Mem s1, Mem s2 -> String.compare s1 s2
7777+ | Mem _, _ -> -1
7878+ | _, Mem _ -> 1
7979+ | Nth n1, Nth n2 -> Int.compare n1 n2
8080+ | Nth _, End -> -1
8181+ | End, Nth _ -> 1
8282+ | End, End -> 0
8383+8484+ let of_path_index (idx : Jsont.Path.index) : t =
8585+ match idx with
8686+ | Jsont.Path.Mem (s, _meta) -> Mem s
8787+ | Jsont.Path.Nth (n, _meta) -> Nth n
8888+8989+ let to_path_index (idx : t) : Jsont.Path.index option =
9090+ match idx with
9191+ | Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none))
9292+ | Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none))
9393+ | End -> None
9494+end
9595+9696+(* Internal representation: raw unescaped tokens.
9797+ Per RFC 6901, interpretation as member name vs array index
9898+ depends on the JSON value type at evaluation time. *)
9999+module Segment = struct
100100+ type t =
101101+ | Token of string (* Unescaped reference token *)
102102+ | End (* The "-" token for end-of-array *)
103103+104104+ let of_escaped_string s =
105105+ if s = "-" then End
106106+ else Token (Token.unescape s)
107107+108108+ let to_escaped_string = function
109109+ | Token s -> Token.escape s
110110+ | End -> "-"
111111+112112+ (* Convert to Index for a given JSON value type *)
113113+ let to_index seg ~for_array =
114114+ match seg with
115115+ | End -> Index.End
116116+ | Token s ->
117117+ if for_array then
118118+ match Token.is_valid_array_index s with
119119+ | Some n -> Index.Nth n
120120+ | None -> Index.Mem s (* Invalid index becomes member for error msg *)
121121+ else
122122+ Index.Mem s
123123+124124+ (* Convert from Index *)
125125+ let of_index = function
126126+ | Index.End -> End
127127+ | Index.Mem s -> Token s
128128+ | Index.Nth n -> Token (string_of_int n)
129129+end
130130+131131+(* Pointer type - list of segments *)
132132+type t = Segment.t list
133133+134134+let root = []
135135+136136+let is_root p = p = []
137137+138138+(* Convert indices to segments *)
139139+let make indices = List.map Segment.of_index indices
140140+141141+(* Convert segments to indices, assuming array context for numeric tokens *)
142142+let indices p = List.map (fun seg -> Segment.to_index seg ~for_array:true) p
143143+144144+let append p idx = p @ [Segment.of_index idx]
145145+146146+let concat p1 p2 = p1 @ p2
147147+148148+let parent p = match List.rev p with
149149+ | [] -> None
150150+ | _ :: rest -> Some (List.rev rest)
151151+152152+let last p = match List.rev p with
153153+ | [] -> None
154154+ | seg :: _ -> Some (Segment.to_index seg ~for_array:true)
155155+156156+(* Parsing *)
157157+158158+let of_string s =
159159+ if s = "" then root
160160+ else if s.[0] <> '/' then
161161+ Jsont.Error.msgf Jsont.Meta.none
162162+ "Invalid JSON Pointer: must be empty or start with '/': %s" s
163163+ else
164164+ let rest = String.sub s 1 (String.length s - 1) in
165165+ let tokens = String.split_on_char '/' rest in
166166+ List.map Segment.of_escaped_string tokens
167167+168168+let of_string_result s =
169169+ try Ok (of_string s)
170170+ with Jsont.Error (_, _, _) as e ->
171171+ Error (Jsont.Error.to_string (match e with Jsont.Error e -> e | _ -> assert false))
172172+173173+(* URI fragment percent-decoding *)
174174+let hex_value c =
175175+ if c >= '0' && c <= '9' then Char.code c - Char.code '0'
176176+ else if c >= 'A' && c <= 'F' then Char.code c - Char.code 'A' + 10
177177+ else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10
178178+ else -1
179179+180180+let percent_decode s =
181181+ let len = String.length s in
182182+ let b = Buffer.create len in
183183+ let rec loop i =
184184+ if i >= len then Buffer.contents b
185185+ else match s.[i] with
186186+ | '%' when i + 2 < len ->
187187+ let h1 = hex_value s.[i + 1] in
188188+ let h2 = hex_value s.[i + 2] in
189189+ if h1 >= 0 && h2 >= 0 then begin
190190+ Buffer.add_char b (Char.chr ((h1 lsl 4) lor h2));
191191+ loop (i + 3)
192192+ end else
193193+ Jsont.Error.msgf Jsont.Meta.none
194194+ "Invalid percent-encoding at position %d" i
195195+ | '%' ->
196196+ Jsont.Error.msgf Jsont.Meta.none
197197+ "Incomplete percent-encoding at position %d" i
198198+ | c -> Buffer.add_char b c; loop (i + 1)
199199+ in
200200+ loop 0
201201+202202+let of_uri_fragment s =
203203+ of_string (percent_decode s)
204204+205205+let of_uri_fragment_result s =
206206+ try Ok (of_uri_fragment s)
207207+ with Jsont.Error (_, _, _) as e ->
208208+ Error (Jsont.Error.to_string (match e with Jsont.Error e -> e | _ -> assert false))
209209+210210+(* Serialization *)
211211+212212+let to_string p =
213213+ if p = [] then ""
214214+ else
215215+ let b = Buffer.create 64 in
216216+ List.iter (fun seg ->
217217+ Buffer.add_char b '/';
218218+ Buffer.add_string b (Segment.to_escaped_string seg)
219219+ ) p;
220220+ Buffer.contents b
221221+222222+(* URI fragment percent-encoding *)
223223+let needs_percent_encoding c =
224224+ (* RFC 3986 fragment: unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" *)
225225+ (* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" *)
226226+ (* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *)
227227+ not (
228228+ (c >= 'A' && c <= 'Z') ||
229229+ (c >= 'a' && c <= 'z') ||
230230+ (c >= '0' && c <= '9') ||
231231+ c = '-' || c = '.' || c = '_' || c = '~' ||
232232+ c = '!' || c = '$' || c = '&' || c = '\'' ||
233233+ c = '(' || c = ')' || c = '*' || c = '+' ||
234234+ c = ',' || c = ';' || c = '=' ||
235235+ c = ':' || c = '@' || c = '/' || c = '?'
236236+ )
237237+238238+let hex_char n =
239239+ if n < 10 then Char.chr (Char.code '0' + n)
240240+ else Char.chr (Char.code 'A' + n - 10)
241241+242242+let percent_encode s =
243243+ let b = Buffer.create (String.length s * 3) in
244244+ String.iter (fun c ->
245245+ if needs_percent_encoding c then begin
246246+ let code = Char.code c in
247247+ Buffer.add_char b '%';
248248+ Buffer.add_char b (hex_char (code lsr 4));
249249+ Buffer.add_char b (hex_char (code land 0xF))
250250+ end else
251251+ Buffer.add_char b c
252252+ ) s;
253253+ Buffer.contents b
254254+255255+let to_uri_fragment p =
256256+ percent_encode (to_string p)
257257+258258+let pp ppf p =
259259+ Format.pp_print_string ppf (to_string p)
260260+261261+(* Comparison *)
262262+263263+let segment_equal s1 s2 = match s1, s2 with
264264+ | Segment.Token t1, Segment.Token t2 -> String.equal t1 t2
265265+ | Segment.End, Segment.End -> true
266266+ | _ -> false
267267+268268+let segment_compare s1 s2 = match s1, s2 with
269269+ | Segment.Token t1, Segment.Token t2 -> String.compare t1 t2
270270+ | Segment.Token _, Segment.End -> -1
271271+ | Segment.End, Segment.Token _ -> 1
272272+ | Segment.End, Segment.End -> 0
273273+274274+let equal p1 p2 =
275275+ List.length p1 = List.length p2 &&
276276+ List.for_all2 segment_equal p1 p2
277277+278278+let compare p1 p2 =
279279+ let rec loop l1 l2 = match l1, l2 with
280280+ | [], [] -> 0
281281+ | [], _ -> -1
282282+ | _, [] -> 1
283283+ | h1 :: t1, h2 :: t2 ->
284284+ let c = segment_compare h1 h2 in
285285+ if c <> 0 then c else loop t1 t2
286286+ in
287287+ loop p1 p2
288288+289289+(* Path conversion *)
290290+291291+let segment_of_path_index (idx : Jsont.Path.index) : Segment.t =
292292+ match idx with
293293+ | Jsont.Path.Mem (s, _meta) -> Segment.Token s
294294+ | Jsont.Path.Nth (n, _meta) -> Segment.Token (string_of_int n)
295295+296296+let of_path (p : Jsont.Path.t) : t =
297297+ List.rev_map segment_of_path_index (Jsont.Path.rev_indices p)
298298+299299+let to_path p =
300300+ let rec convert acc = function
301301+ | [] -> Some acc
302302+ | Segment.End :: _ -> None
303303+ | Segment.Token s :: rest ->
304304+ (* For path conversion, we need to decide if it's a member or index.
305305+ We use array context for numeric tokens since Jsont.Path distinguishes. *)
306306+ let acc' = match Token.is_valid_array_index s with
307307+ | Some n -> Jsont.Path.nth ~meta:Jsont.Meta.none n acc
308308+ | None -> Jsont.Path.mem ~meta:Jsont.Meta.none s acc
309309+ in
310310+ convert acc' rest
311311+ in
312312+ convert Jsont.Path.root p
313313+314314+let to_path_exn p =
315315+ match to_path p with
316316+ | Some path -> path
317317+ | None ->
318318+ Jsont.Error.msgf Jsont.Meta.none
319319+ "Cannot convert JSON Pointer with '-' index to Jsont.Path"
320320+321321+(* Evaluation helpers *)
322322+323323+let json_sort_string (j : Jsont.json) =
324324+ match j with
325325+ | Null _ -> "null"
326326+ | Bool _ -> "boolean"
327327+ | Number _ -> "number"
328328+ | String _ -> "string"
329329+ | Array _ -> "array"
330330+ | Object _ -> "object"
331331+332332+let get_member name (obj : Jsont.object') =
333333+ List.find_opt (fun ((n, _), _) -> String.equal n name) obj
334334+335335+let get_nth n (arr : Jsont.json list) =
336336+ if n < 0 || n >= List.length arr then None
337337+ else Some (List.nth arr n)
338338+339339+(* Evaluation *)
340340+341341+let rec eval_get p json =
342342+ match p with
343343+ | [] -> json
344344+ | Segment.End :: _ ->
345345+ Jsont.Error.msgf (Jsont.Json.meta json)
346346+ "JSON Pointer: '-' (end marker) refers to nonexistent array element"
347347+ | Segment.Token token :: rest ->
348348+ (match json with
349349+ | Jsont.Object (members, _) ->
350350+ (* For objects, token is always a member name *)
351351+ (match get_member token members with
352352+ | Some (_, value) -> eval_get rest value
353353+ | None ->
354354+ Jsont.Error.msgf (Jsont.Json.meta json)
355355+ "JSON Pointer: member '%s' not found" token)
356356+ | Jsont.Array (elements, _) ->
357357+ (* For arrays, token must be a valid array index *)
358358+ (match Token.is_valid_array_index token with
359359+ | Some n ->
360360+ (match get_nth n elements with
361361+ | Some value -> eval_get rest value
362362+ | None ->
363363+ Jsont.Error.msgf (Jsont.Json.meta json)
364364+ "JSON Pointer: index %d out of bounds (array has %d elements)"
365365+ n (List.length elements))
366366+ | None ->
367367+ Jsont.Error.msgf (Jsont.Json.meta json)
368368+ "JSON Pointer: invalid array index '%s'" token)
369369+ | _ ->
370370+ Jsont.Error.msgf (Jsont.Json.meta json)
371371+ "JSON Pointer: cannot index into %s with '%s'"
372372+ (json_sort_string json) token)
373373+374374+let get p json = eval_get p json
375375+376376+let get_result p json =
377377+ try Ok (get p json)
378378+ with Jsont.Error e -> Error e
379379+380380+let find p json =
381381+ try Some (get p json)
382382+ with Jsont.Error _ -> None
383383+384384+(* Mutation helpers *)
385385+386386+let set_member name value (obj : Jsont.object') : Jsont.object' =
387387+ let found = ref false in
388388+ let result = List.map (fun ((n, m), v) ->
389389+ if String.equal n name then begin
390390+ found := true;
391391+ ((n, m), value)
392392+ end else
393393+ ((n, m), v)
394394+ ) obj in
395395+ if !found then result
396396+ else obj @ [((name, Jsont.Meta.none), value)]
397397+398398+let remove_member name (obj : Jsont.object') : Jsont.object' =
399399+ List.filter (fun ((n, _), _) -> not (String.equal n name)) obj
400400+401401+let insert_at n value lst =
402402+ let rec loop i acc = function
403403+ | [] when i = n -> List.rev (value :: acc)
404404+ | [] -> List.rev acc (* shouldn't happen if n is valid *)
405405+ | h :: t when i = n -> List.rev_append (value :: acc) (h :: t)
406406+ | h :: t -> loop (i + 1) (h :: acc) t
407407+ in
408408+ loop 0 [] lst
409409+410410+let remove_at n lst =
411411+ let rec loop i acc = function
412412+ | [] -> List.rev acc
413413+ | _ :: t when i = n -> List.rev_append acc t
414414+ | h :: t -> loop (i + 1) (h :: acc) t
415415+ in
416416+ loop 0 [] lst
417417+418418+let replace_at n value lst =
419419+ List.mapi (fun i v -> if i = n then value else v) lst
420420+421421+(* Mutation: set *)
422422+423423+let rec eval_set p value json =
424424+ match p with
425425+ | [] -> value
426426+ | [Segment.End] ->
427427+ (match json with
428428+ | Jsont.Array (elements, meta) ->
429429+ Jsont.Array (elements @ [value], meta)
430430+ | _ ->
431431+ Jsont.Error.msgf (Jsont.Json.meta json)
432432+ "JSON Pointer: '-' can only be used on arrays, got %s"
433433+ (json_sort_string json))
434434+ | Segment.End :: _ ->
435435+ Jsont.Error.msgf (Jsont.Json.meta json)
436436+ "JSON Pointer: '-' (end marker) refers to nonexistent array element"
437437+ | [Segment.Token token] ->
438438+ (match json with
439439+ | Jsont.Object (members, meta) ->
440440+ if Option.is_some (get_member token members) then
441441+ Jsont.Object (set_member token value members, meta)
442442+ else
443443+ Jsont.Error.msgf (Jsont.Json.meta json)
444444+ "JSON Pointer: member '%s' not found for set" token
445445+ | Jsont.Array (elements, meta) ->
446446+ (match Token.is_valid_array_index token with
447447+ | Some n when n >= 0 && n < List.length elements ->
448448+ Jsont.Array (replace_at n value elements, meta)
449449+ | Some n ->
450450+ Jsont.Error.msgf (Jsont.Json.meta json)
451451+ "JSON Pointer: index %d out of bounds for set" n
452452+ | None ->
453453+ Jsont.Error.msgf (Jsont.Json.meta json)
454454+ "JSON Pointer: invalid array index '%s'" token)
455455+ | _ ->
456456+ Jsont.Error.msgf (Jsont.Json.meta json)
457457+ "JSON Pointer: cannot set in %s" (json_sort_string json))
458458+ | Segment.Token token :: rest ->
459459+ (match json with
460460+ | Jsont.Object (members, meta) ->
461461+ (match get_member token members with
462462+ | Some (_, child) ->
463463+ Jsont.Object (set_member token (eval_set rest value child) members, meta)
464464+ | None ->
465465+ Jsont.Error.msgf (Jsont.Json.meta json)
466466+ "JSON Pointer: member '%s' not found" token)
467467+ | Jsont.Array (elements, meta) ->
468468+ (match Token.is_valid_array_index token with
469469+ | Some n ->
470470+ (match get_nth n elements with
471471+ | Some child ->
472472+ Jsont.Array (replace_at n (eval_set rest value child) elements, meta)
473473+ | None ->
474474+ Jsont.Error.msgf (Jsont.Json.meta json)
475475+ "JSON Pointer: index %d out of bounds" n)
476476+ | None ->
477477+ Jsont.Error.msgf (Jsont.Json.meta json)
478478+ "JSON Pointer: invalid array index '%s'" token)
479479+ | _ ->
480480+ Jsont.Error.msgf (Jsont.Json.meta json)
481481+ "JSON Pointer: cannot navigate through %s" (json_sort_string json))
482482+483483+let set p json ~value = eval_set p value json
484484+485485+(* Mutation: add (RFC 6902 semantics) *)
486486+487487+let rec eval_add p value json =
488488+ match p with
489489+ | [] -> value
490490+ | [Segment.End] ->
491491+ (match json with
492492+ | Jsont.Array (elements, meta) ->
493493+ Jsont.Array (elements @ [value], meta)
494494+ | _ ->
495495+ Jsont.Error.msgf (Jsont.Json.meta json)
496496+ "JSON Pointer: '-' can only be used on arrays, got %s"
497497+ (json_sort_string json))
498498+ | Segment.End :: _ ->
499499+ Jsont.Error.msgf (Jsont.Json.meta json)
500500+ "JSON Pointer: '-' in non-final position"
501501+ | [Segment.Token token] ->
502502+ (match json with
503503+ | Jsont.Object (members, meta) ->
504504+ (* For objects, add/replace member *)
505505+ Jsont.Object (set_member token value members, meta)
506506+ | Jsont.Array (elements, meta) ->
507507+ (* For arrays, insert at index *)
508508+ (match Token.is_valid_array_index token with
509509+ | Some n ->
510510+ let len = List.length elements in
511511+ if n >= 0 && n <= len then
512512+ Jsont.Array (insert_at n value elements, meta)
513513+ else
514514+ Jsont.Error.msgf (Jsont.Json.meta json)
515515+ "JSON Pointer: index %d out of bounds for add (array has %d elements)"
516516+ n len
517517+ | None ->
518518+ Jsont.Error.msgf (Jsont.Json.meta json)
519519+ "JSON Pointer: invalid array index '%s'" token)
520520+ | _ ->
521521+ Jsont.Error.msgf (Jsont.Json.meta json)
522522+ "JSON Pointer: cannot add to %s" (json_sort_string json))
523523+ | Segment.Token token :: rest ->
524524+ (match json with
525525+ | Jsont.Object (members, meta) ->
526526+ (match get_member token members with
527527+ | Some (_, child) ->
528528+ Jsont.Object (set_member token (eval_add rest value child) members, meta)
529529+ | None ->
530530+ Jsont.Error.msgf (Jsont.Json.meta json)
531531+ "JSON Pointer: member '%s' not found" token)
532532+ | Jsont.Array (elements, meta) ->
533533+ (match Token.is_valid_array_index token with
534534+ | Some n ->
535535+ (match get_nth n elements with
536536+ | Some child ->
537537+ Jsont.Array (replace_at n (eval_add rest value child) elements, meta)
538538+ | None ->
539539+ Jsont.Error.msgf (Jsont.Json.meta json)
540540+ "JSON Pointer: index %d out of bounds" n)
541541+ | None ->
542542+ Jsont.Error.msgf (Jsont.Json.meta json)
543543+ "JSON Pointer: invalid array index '%s'" token)
544544+ | _ ->
545545+ Jsont.Error.msgf (Jsont.Json.meta json)
546546+ "JSON Pointer: cannot navigate through %s" (json_sort_string json))
547547+548548+let add p json ~value = eval_add p value json
549549+550550+(* Mutation: remove *)
551551+552552+let rec eval_remove p json =
553553+ match p with
554554+ | [] ->
555555+ Jsont.Error.msgf Jsont.Meta.none
556556+ "JSON Pointer: cannot remove root document"
557557+ | [Segment.End] ->
558558+ Jsont.Error.msgf (Jsont.Json.meta json)
559559+ "JSON Pointer: '-' refers to nonexistent element"
560560+ | Segment.End :: _ ->
561561+ Jsont.Error.msgf (Jsont.Json.meta json)
562562+ "JSON Pointer: '-' in non-final position"
563563+ | [Segment.Token token] ->
564564+ (match json with
565565+ | Jsont.Object (members, meta) ->
566566+ if Option.is_some (get_member token members) then
567567+ Jsont.Object (remove_member token members, meta)
568568+ else
569569+ Jsont.Error.msgf (Jsont.Json.meta json)
570570+ "JSON Pointer: member '%s' not found for remove" token
571571+ | Jsont.Array (elements, meta) ->
572572+ (match Token.is_valid_array_index token with
573573+ | Some n when n >= 0 && n < List.length elements ->
574574+ Jsont.Array (remove_at n elements, meta)
575575+ | Some n ->
576576+ Jsont.Error.msgf (Jsont.Json.meta json)
577577+ "JSON Pointer: index %d out of bounds for remove" n
578578+ | None ->
579579+ Jsont.Error.msgf (Jsont.Json.meta json)
580580+ "JSON Pointer: invalid array index '%s'" token)
581581+ | _ ->
582582+ Jsont.Error.msgf (Jsont.Json.meta json)
583583+ "JSON Pointer: cannot remove from %s" (json_sort_string json))
584584+ | Segment.Token token :: rest ->
585585+ (match json with
586586+ | Jsont.Object (members, meta) ->
587587+ (match get_member token members with
588588+ | Some (_, child) ->
589589+ Jsont.Object (set_member token (eval_remove rest child) members, meta)
590590+ | None ->
591591+ Jsont.Error.msgf (Jsont.Json.meta json)
592592+ "JSON Pointer: member '%s' not found" token)
593593+ | Jsont.Array (elements, meta) ->
594594+ (match Token.is_valid_array_index token with
595595+ | Some n ->
596596+ (match get_nth n elements with
597597+ | Some child ->
598598+ Jsont.Array (replace_at n (eval_remove rest child) elements, meta)
599599+ | None ->
600600+ Jsont.Error.msgf (Jsont.Json.meta json)
601601+ "JSON Pointer: index %d out of bounds" n)
602602+ | None ->
603603+ Jsont.Error.msgf (Jsont.Json.meta json)
604604+ "JSON Pointer: invalid array index '%s'" token)
605605+ | _ ->
606606+ Jsont.Error.msgf (Jsont.Json.meta json)
607607+ "JSON Pointer: cannot navigate through %s" (json_sort_string json))
608608+609609+let remove p json = eval_remove p json
610610+611611+(* Mutation: replace *)
612612+613613+let replace p json ~value =
614614+ (* Replace requires the target to exist, unlike add *)
615615+ let _ = get p json in (* Will raise if not found *)
616616+ eval_set p value json
617617+618618+(* Mutation: move *)
619619+620620+let is_prefix_of p1 p2 =
621621+ let rec loop l1 l2 = match l1, l2 with
622622+ | [], _ -> true
623623+ | _, [] -> false
624624+ | h1 :: t1, h2 :: t2 ->
625625+ segment_equal h1 h2 && loop t1 t2
626626+ in
627627+ loop p1 p2
628628+629629+let move ~from ~path json =
630630+ (* Check for cycle: path cannot be a proper prefix of from *)
631631+ if is_prefix_of path from && not (equal path from) then
632632+ Jsont.Error.msgf Jsont.Meta.none
633633+ "JSON Pointer: move would create cycle (path is prefix of from)";
634634+ let value = get from json in
635635+ let json' = remove from json in
636636+ add path json' ~value
637637+638638+(* Mutation: copy *)
639639+640640+let copy ~from ~path json =
641641+ let value = get from json in
642642+ add path json ~value
643643+644644+(* Mutation: test *)
645645+646646+let test p json ~expected =
647647+ match find p json with
648648+ | None -> false
649649+ | Some value -> Jsont.Json.equal value expected
650650+651651+(* Jsont codec *)
652652+653653+let jsont : t Jsont.t =
654654+ let dec _meta s = of_string s in
655655+ let enc p = to_string p in
656656+ Jsont.Base.string (Jsont.Base.map
657657+ ~kind:"JSON Pointer"
658658+ ~doc:"RFC 6901 JSON Pointer"
659659+ ~dec ~enc ())
660660+661661+let jsont_uri_fragment : t Jsont.t =
662662+ let dec _meta s = of_uri_fragment s in
663663+ let enc p = to_uri_fragment p in
664664+ Jsont.Base.string (Jsont.Base.map
665665+ ~kind:"JSON Pointer (URI fragment)"
666666+ ~doc:"RFC 6901 JSON Pointer in URI fragment encoding"
667667+ ~dec ~enc ())
668668+669669+(* Query combinators *)
670670+671671+let path ?absent p t =
672672+ let dec json =
673673+ match find p json with
674674+ | Some value ->
675675+ (match Jsont.Json.decode' t value with
676676+ | Ok v -> v
677677+ | Error e -> raise (Jsont.Error e))
678678+ | None ->
679679+ match absent with
680680+ | Some v -> v
681681+ | None ->
682682+ Jsont.Error.msgf Jsont.Meta.none
683683+ "JSON Pointer %s: path not found" (to_string p)
684684+ in
685685+ Jsont.map Jsont.json ~dec ~enc:(fun _ ->
686686+ Jsont.Error.msgf Jsont.Meta.none "path: encode not supported")
687687+688688+let set_path ?(allow_absent = false) t p v =
689689+ let encoded = match Jsont.Json.encode' t v with
690690+ | Ok json -> json
691691+ | Error e -> raise (Jsont.Error e)
692692+ in
693693+ let dec json =
694694+ if allow_absent then
695695+ add p json ~value:encoded
696696+ else
697697+ set p json ~value:encoded
698698+ in
699699+ Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
700700+701701+let update_path ?absent p t =
702702+ let dec json =
703703+ let value = match find p json with
704704+ | Some v -> v
705705+ | None ->
706706+ match absent with
707707+ | Some v ->
708708+ (match Jsont.Json.encode' t v with
709709+ | Ok j -> j
710710+ | Error e -> raise (Jsont.Error e))
711711+ | None ->
712712+ Jsont.Error.msgf Jsont.Meta.none
713713+ "JSON Pointer %s: path not found" (to_string p)
714714+ in
715715+ let decoded = match Jsont.Json.decode' t value with
716716+ | Ok v -> v
717717+ | Error e -> raise (Jsont.Error e)
718718+ in
719719+ let re_encoded = match Jsont.Json.encode' t decoded with
720720+ | Ok j -> j
721721+ | Error e -> raise (Jsont.Error e)
722722+ in
723723+ set p json ~value:re_encoded
724724+ in
725725+ Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
726726+727727+let delete_path ?(allow_absent = false) p =
728728+ let dec json =
729729+ if allow_absent then
730730+ match find p json with
731731+ | Some _ -> remove p json
732732+ | None -> json
733733+ else
734734+ remove p json
735735+ in
736736+ Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
+368
src/jsont_pointer.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2024 The jsont programmers. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** RFC 6901 JSON Pointer implementation for jsont.
77+88+ This module provides {{:https://www.rfc-editor.org/rfc/rfc6901}RFC 6901}
99+ JSON Pointer parsing, serialization, and evaluation compatible with
1010+ {!Jsont} codecs.
1111+1212+ A JSON Pointer is a string syntax for identifying a specific value within
1313+ a JSON document. For example, given the JSON document:
1414+ {v
1515+ {
1616+ "foo": ["bar", "baz"],
1717+ "": 0,
1818+ "a/b": 1,
1919+ "m~n": 2
2020+ }
2121+ v}
2222+2323+ The following JSON Pointers evaluate to:
2424+ {ul
2525+ {- [""] - the whole document}
2626+ {- ["/foo"] - the array [\["bar", "baz"\]]}
2727+ {- ["/foo/0"] - the string ["bar"]}
2828+ {- ["/"] - the integer [0] (empty string key)}
2929+ {- ["/a~1b"] - the integer [1] ([~1] escapes [/])}
3030+ {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}}
3131+3232+ {1:tokens Reference Tokens}
3333+3434+ JSON Pointer uses escape sequences for special characters in reference
3535+ tokens. The character [~] must be encoded as [~0] and [/] as [~1].
3636+ When unescaping, [~1] is processed before [~0] to correctly handle
3737+ sequences like [~01] which should become [~1], not [/]. *)
3838+3939+(** {1 Reference tokens}
4040+4141+ Reference tokens are the individual segments between [/] characters
4242+ in a JSON Pointer string. They require escaping of [~] and [/]. *)
4343+module Token : sig
4444+4545+ type t = string
4646+ (** The type for unescaped reference tokens. These are plain strings
4747+ representing object member names or array index strings. *)
4848+4949+ val escape : t -> string
5050+ (** [escape s] escapes special characters in [s] for use in a JSON Pointer.
5151+ Specifically, [~] becomes [~0] and [/] becomes [~1]. *)
5252+5353+ val unescape : string -> t
5454+ (** [unescape s] unescapes a JSON Pointer reference token.
5555+ Specifically, [~1] becomes [/] and [~0] becomes [~].
5656+5757+ @raise Jsont.Error if [s] contains invalid escape sequences
5858+ (a [~] not followed by [0] or [1]). *)
5959+end
6060+6161+(** {1 Indices}
6262+6363+ Indices represent individual navigation steps in a JSON Pointer.
6464+ For objects, this is a member name. For arrays, this is either
6565+ a numeric index or the special end-of-array marker [-]. *)
6666+module Index : sig
6767+6868+ type t =
6969+ | Mem of string
7070+ (** [Mem name] indexes into an object member with the given [name].
7171+ The name is unescaped (i.e., [/] and [~] appear literally). *)
7272+ | Nth of int
7373+ (** [Nth n] indexes into an array at position [n] (zero-based).
7474+ Must be non-negative and without leading zeros in string form
7575+ (except for [0] itself). *)
7676+ | End
7777+ (** [End] represents the [-] token, indicating the position after
7878+ the last element of an array. This is used for append operations
7979+ in {!Jsont_pointer.add} and similar mutation functions.
8080+ Evaluating a pointer containing [End] with {!Jsont_pointer.get}
8181+ will raise an error since it refers to a nonexistent element. *)
8282+8383+ val pp : Format.formatter -> t -> unit
8484+ (** [pp] formats an index in JSON Pointer string notation. *)
8585+8686+ val equal : t -> t -> bool
8787+ (** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *)
8888+8989+ val compare : t -> t -> int
9090+ (** [compare i1 i2] is a total order on indices. *)
9191+9292+ (** {2:jsont_conv Conversion with Jsont.Path} *)
9393+9494+ val of_path_index : Jsont.Path.index -> t
9595+ (** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *)
9696+9797+ val to_path_index : t -> Jsont.Path.index option
9898+ (** [to_path_index idx] converts to a {!Jsont.Path.index}.
9999+ Returns [None] for {!End} since it has no equivalent in
100100+ {!Jsont.Path}. *)
101101+end
102102+103103+(** {1 Pointers} *)
104104+105105+type t
106106+(** The type for JSON Pointers. A pointer is a sequence of {!Index.t}
107107+ values representing a path from the root of a JSON document to
108108+ a specific value. *)
109109+110110+val root : t
111111+(** [root] is the empty pointer that references the whole document.
112112+ In string form this is [""]. *)
113113+114114+val is_root : t -> bool
115115+(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
116116+117117+val make : Index.t list -> t
118118+(** [make indices] creates a pointer from a list of indices.
119119+ The list is ordered from root to target (i.e., the first element
120120+ is the first step from the root). *)
121121+122122+val indices : t -> Index.t list
123123+(** [indices p] returns the indices of [p] from root to target. *)
124124+125125+val append : t -> Index.t -> t
126126+(** [append p idx] appends [idx] to the end of pointer [p]. *)
127127+128128+val concat : t -> t -> t
129129+(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
130130+131131+val parent : t -> t option
132132+(** [parent p] returns the parent pointer of [p], or [None] if [p]
133133+ is the {!root}. *)
134134+135135+val last : t -> Index.t option
136136+(** [last p] returns the last index of [p], or [None] if [p] is
137137+ the {!root}. *)
138138+139139+(** {2:parsing Parsing} *)
140140+141141+val of_string : string -> t
142142+(** [of_string s] parses a JSON Pointer from its string representation.
143143+144144+ The string must be either empty (representing the root) or start
145145+ with [/]. Each segment between [/] characters is unescaped as a
146146+ reference token. Segments that are valid non-negative integers
147147+ without leading zeros become {!Index.Nth} indices; the string [-]
148148+ becomes {!Index.End}; all others become {!Index.Mem}.
149149+150150+ @raise Jsont.Error if [s] has invalid syntax:
151151+ - Non-empty string not starting with [/]
152152+ - Invalid escape sequence ([~] not followed by [0] or [1])
153153+ - Array index with leading zeros
154154+ - Array index that overflows [int] *)
155155+156156+val of_string_result : string -> (t, string) result
157157+(** [of_string_result s] is like {!of_string} but returns a result
158158+ instead of raising. *)
159159+160160+val of_uri_fragment : string -> t
161161+(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
162162+163163+ This is like {!of_string} but first percent-decodes the string
164164+ according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}.
165165+ The leading [#] should {b not} be included in [s].
166166+167167+ @raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
168168+169169+val of_uri_fragment_result : string -> (t, string) result
170170+(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
171171+ a result instead of raising. *)
172172+173173+(** {2:serializing Serializing} *)
174174+175175+val to_string : t -> string
176176+(** [to_string p] serializes [p] to its JSON Pointer string representation.
177177+178178+ Returns [""] for the root pointer, otherwise [/] followed by
179179+ escaped reference tokens joined by [/]. *)
180180+181181+val to_uri_fragment : t -> string
182182+(** [to_uri_fragment p] serializes [p] to URI fragment form.
183183+184184+ This is like {!to_string} but additionally percent-encodes
185185+ characters that are not allowed in URI fragments per RFC 3986.
186186+ The leading [#] is {b not} included in the result. *)
187187+188188+val pp : Format.formatter -> t -> unit
189189+(** [pp] formats a pointer using {!to_string}. *)
190190+191191+(** {2:comparison Comparison} *)
192192+193193+val equal : t -> t -> bool
194194+(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *)
195195+196196+val compare : t -> t -> int
197197+(** [compare p1 p2] is a total order on pointers, comparing indices
198198+ lexicographically. *)
199199+200200+(** {2:jsont_path Conversion with Jsont.Path} *)
201201+202202+val of_path : Jsont.Path.t -> t
203203+(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *)
204204+205205+val to_path : t -> Jsont.Path.t option
206206+(** [to_path p] converts to a {!Jsont.Path.t}.
207207+ Returns [None] if [p] contains an {!Index.End} index. *)
208208+209209+val to_path_exn : t -> Jsont.Path.t
210210+(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error}
211211+ if conversion fails. *)
212212+213213+(** {1 Evaluation}
214214+215215+ These functions evaluate a JSON Pointer against a {!Jsont.json} value
216216+ to retrieve the referenced value. *)
217217+218218+val get : t -> Jsont.json -> Jsont.json
219219+(** [get p json] retrieves the value at pointer [p] in [json].
220220+221221+ @raise Jsont.Error if:
222222+ - The pointer references a nonexistent object member
223223+ - The pointer references an out-of-bounds array index
224224+ - The pointer contains {!Index.End} (since [-] always refers
225225+ to a nonexistent element)
226226+ - An index type doesn't match the JSON value (e.g., {!Index.Nth}
227227+ on an object) *)
228228+229229+val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
230230+(** [get_result p json] is like {!get} but returns a result. *)
231231+232232+val find : t -> Jsont.json -> Jsont.json option
233233+(** [find p json] is like {!get} but returns [None] instead of
234234+ raising when the pointer doesn't resolve to a value. *)
235235+236236+(** {1 Mutation}
237237+238238+ These functions modify a {!Jsont.json} value at a location specified
239239+ by a JSON Pointer. They are designed to support
240240+ {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch}
241241+ operations.
242242+243243+ All mutation functions return a new JSON value with the modification
244244+ applied; they do not mutate the input. *)
245245+246246+val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json
247247+(** [set p json ~value] replaces the value at pointer [p] with [value].
248248+249249+ For {!Index.End} on arrays, appends [value] to the end of the array.
250250+251251+ @raise Jsont.Error if the pointer doesn't resolve to an existing
252252+ location (except for {!Index.End} on arrays). *)
253253+254254+val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
255255+(** [add p json ~value] adds [value] at the location specified by [p].
256256+257257+ The behavior depends on the target:
258258+ {ul
259259+ {- For objects: If the member exists, it is replaced. If it doesn't
260260+ exist, a new member is added.}
261261+ {- For arrays with {!Index.Nth}: Inserts [value] {e before} the
262262+ specified index, shifting subsequent elements. The index must be
263263+ valid (0 to length inclusive).}
264264+ {- For arrays with {!Index.End}: Appends [value] to the array.}}
265265+266266+ @raise Jsont.Error if:
267267+ - The parent of the target location doesn't exist
268268+ - An array index is out of bounds (except for {!Index.End})
269269+ - The parent is not an object or array *)
270270+271271+val remove : t -> Jsont.json -> Jsont.json
272272+(** [remove p json] removes the value at pointer [p].
273273+274274+ For objects, removes the member. For arrays, removes the element
275275+ and shifts subsequent elements.
276276+277277+ @raise Jsont.Error if:
278278+ - [p] is the root (cannot remove the root)
279279+ - The pointer doesn't resolve to an existing value
280280+ - The pointer contains {!Index.End} *)
281281+282282+val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json
283283+(** [replace p json ~value] replaces the value at pointer [p] with [value].
284284+285285+ Unlike {!add}, this requires the target to exist.
286286+287287+ @raise Jsont.Error if:
288288+ - The pointer doesn't resolve to an existing value
289289+ - The pointer contains {!Index.End} *)
290290+291291+val move : from:t -> path:t -> Jsont.json -> Jsont.json
292292+(** [move ~from ~path json] moves the value from [from] to [path].
293293+294294+ This is equivalent to {!remove} at [from] followed by {!add}
295295+ at [path] with the removed value.
296296+297297+ @raise Jsont.Error if:
298298+ - [from] doesn't resolve to a value
299299+ - [path] is a proper prefix of [from] (would create a cycle)
300300+ - Either pointer contains {!Index.End} *)
301301+302302+val copy : from:t -> path:t -> Jsont.json -> Jsont.json
303303+(** [copy ~from ~path json] copies the value from [from] to [path].
304304+305305+ This is equivalent to {!get} at [from] followed by {!add}
306306+ at [path] with the retrieved value.
307307+308308+ @raise Jsont.Error if:
309309+ - [from] doesn't resolve to a value
310310+ - Either pointer contains {!Index.End} *)
311311+312312+val test : t -> Jsont.json -> expected:Jsont.json -> bool
313313+(** [test p json ~expected] tests if the value at [p] equals [expected].
314314+315315+ Returns [true] if the values are equal according to {!Jsont.Json.equal},
316316+ [false] otherwise. Also returns [false] (rather than raising) if the
317317+ pointer doesn't resolve.
318318+319319+ Note: This implements the semantics of the JSON Patch "test" operation. *)
320320+321321+(** {1 Jsont Integration}
322322+323323+ These types and functions integrate JSON Pointers with the {!Jsont}
324324+ codec system. *)
325325+326326+val jsont : t Jsont.t
327327+(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
328328+329329+ On decode, parses a JSON string as a JSON Pointer using {!of_string}.
330330+ On encode, serializes a pointer to a JSON string using {!to_string}. *)
331331+332332+val jsont_uri_fragment : t Jsont.t
333333+(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
334334+335335+ On decode, parses using {!of_uri_fragment}.
336336+ On encode, serializes using {!to_uri_fragment}. *)
337337+338338+(** {2:query Query combinators}
339339+340340+ These combinators integrate with jsont's query system, allowing
341341+ JSON Pointers to be used with jsont codecs for typed access. *)
342342+343343+val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t
344344+(** [path p t] decodes the value at pointer [p] using codec [t].
345345+346346+ If [absent] is provided and the pointer doesn't resolve, returns
347347+ [absent] instead of raising.
348348+349349+ This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
350350+351351+val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t
352352+(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
353353+354354+ If [allow_absent] is [true] (default [false]), creates missing
355355+ intermediate structure as needed.
356356+357357+ This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
358358+359359+val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t
360360+(** [update_path p t] recodes the value at pointer [p] with codec [t].
361361+362362+ This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
363363+364364+val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t
365365+(** [delete_path p] removes the value at pointer [p].
366366+367367+ If [allow_absent] is [true] (default [false]), does nothing if
368368+ the pointer doesn't resolve instead of raising. *)
···11+# Evaluation error tests
22+# Format: pointer -> error type
33+44+# Nonexistent members
55+/nonexistent -> member not found
66+/foo/nonexistent -> type mismatch (array, not object)
77+88+# Out of bounds array indices
99+/foo/2 -> index out of bounds
1010+/foo/99 -> index out of bounds
1111+1212+# Type mismatches
1313+/foo/0/bar -> type mismatch (string, not object)
1414+/ /0 -> type mismatch (number, not array)
1515+1616+# End marker always errors on get
1717+/foo/- -> end marker not allowed
1818+/- -> end marker not allowed
···11+# Invalid JSON Pointer parsing tests
22+# Format: pointer -> error description
33+44+# Must start with / if non-empty
55+foo -> must start with /
66+a/b -> must start with /
77+0 -> must start with /
88+- -> must start with /
99+1010+# Invalid escape sequences
1111+/~ -> incomplete escape
1212+/~2 -> invalid escape ~2
1313+/~a -> invalid escape ~a
1414+/foo~bar -> invalid escape ~b
1515+/~~ -> invalid escape ~~
1616+1717+# Leading zeros in array indices (RFC 6901 Section 4)
1818+/00 -> leading zero
1919+/01 -> leading zero
2020+/007 -> leading zero
2121+2222+# Negative indices not allowed
2323+/-1 -> not a valid index
2424+/-42 -> not a valid index