RFC6901 JSON Pointer implementation in OCaml using jsont

initial import

+1
.gitignore
···
··· 1 + _build
+18
dune-project
···
··· 1 + (lang dune 3.17) 2 + (name jsont-pointer) 3 + (version 0.1.0) 4 + 5 + (generate_opam_files true) 6 + 7 + (source (github avsm/jsont-pointer)) 8 + (license ISC) 9 + (authors "Anil Madhavapeddy") 10 + (maintainers "anil@recoil.org") 11 + 12 + (package 13 + (name jsont-pointer) 14 + (synopsis "RFC 6901 JSON Pointer implementation for jsont") 15 + (description "This library provides RFC 6901 JSON Pointer parsing, serialization, and evaluation compatible with jsont codecs. It also provides mutation operations suitable for implementing RFC 6902 JSON Patch.") 16 + (depends 17 + (ocaml (>= 4.14.0)) 18 + (jsont (>= 0.2.0))))
+32
jsont-pointer.opam
···
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + version: "0.1.0" 4 + synopsis: "RFC 6901 JSON Pointer implementation for jsont" 5 + description: 6 + "This library provides RFC 6901 JSON Pointer parsing, serialization, and evaluation compatible with jsont codecs. It also provides mutation operations suitable for implementing RFC 6902 JSON Patch." 7 + maintainer: ["anil@recoil.org"] 8 + authors: ["Anil Madhavapeddy"] 9 + license: "ISC" 10 + homepage: "https://github.com/avsm/jsont-pointer" 11 + bug-reports: "https://github.com/avsm/jsont-pointer/issues" 12 + depends: [ 13 + "dune" {>= "3.17"} 14 + "ocaml" {>= "4.14.0"} 15 + "jsont" {>= "0.2.0"} 16 + "odoc" {with-doc} 17 + ] 18 + build: [ 19 + ["dune" "subst"] {dev} 20 + [ 21 + "dune" 22 + "build" 23 + "-p" 24 + name 25 + "-j" 26 + jobs 27 + "@install" 28 + "@runtest" {with-test} 29 + "@doc" {with-doc} 30 + ] 31 + ] 32 + dev-repo: "git+https://github.com/avsm/jsont-pointer.git"
+451
spec/rfc6901.txt
···
··· 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 +
+4
src/dune
···
··· 1 + (library 2 + (name jsont_pointer) 3 + (public_name jsont-pointer) 4 + (libraries jsont))
+736
src/jsont_pointer.ml
···
··· 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)
+368
src/jsont_pointer.mli
···
··· 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. *)
+151
test/comprehensive.t
···
··· 1 + Comprehensive JSON Pointer Tests (from json-pointer-js test suite) 2 + 3 + Additional parsing tests: 4 + $ ./test_pointer.exe parse "/foo/bar" 5 + OK: [Mem:foo, Mem:bar] 6 + $ ./test_pointer.exe parse "/foo/-" 7 + OK: [Mem:foo, End] 8 + $ ./test_pointer.exe parse "/foo/1" 9 + OK: [Mem:foo, Nth:1] 10 + $ ./test_pointer.exe parse "/foo/~0" 11 + OK: [Mem:foo, Mem:~] 12 + $ ./test_pointer.exe parse "/foo/~1" 13 + OK: [Mem:foo, Mem:/] 14 + $ ./test_pointer.exe parse "/foo/~1~0" 15 + OK: [Mem:foo, Mem:/~] 16 + $ ./test_pointer.exe parse "/foo/~0~1" 17 + OK: [Mem:foo, Mem:~/] 18 + $ ./test_pointer.exe parse "/foo/~01" 19 + OK: [Mem:foo, Mem:~1] 20 + $ ./test_pointer.exe parse "/foo/~10" 21 + OK: [Mem:foo, Mem:/0] 22 + 23 + Parse error tests: 24 + $ ./test_pointer.exe parse "foo/bar" 25 + ERROR: Invalid JSON Pointer: must be empty or start with '/': foo/bar 26 + $ ./test_pointer.exe parse "~foo" 27 + ERROR: Invalid JSON Pointer: must be empty or start with '/': ~foo 28 + $ ./test_pointer.exe parse "/~a" 29 + ERROR: Invalid JSON Pointer: invalid escape sequence ~a 30 + $ ./test_pointer.exe parse "~1/foo" 31 + ERROR: Invalid JSON Pointer: must be empty or start with '/': ~1/foo 32 + 33 + Get value tests: 34 + $ ./test_pointer.exe eval data/rfc6901_example.json "/a~1b" 35 + OK: 1 36 + $ ./test_pointer.exe eval data/rfc6901_example.json "/c%d" 37 + OK: 2 38 + $ ./test_pointer.exe eval data/rfc6901_example.json "/e^f" 39 + OK: 3 40 + $ ./test_pointer.exe eval data/rfc6901_example.json "/g|h" 41 + OK: 4 42 + $ ./test_pointer.exe eval data/rfc6901_example.json '/i\j' 43 + OK: 5 44 + $ ./test_pointer.exe eval data/rfc6901_example.json '/k"l' 45 + OK: 6 46 + $ ./test_pointer.exe eval data/rfc6901_example.json "/ " 47 + OK: 7 48 + $ ./test_pointer.exe eval data/rfc6901_example.json "/m~0n" 49 + OK: 8 50 + $ ./test_pointer.exe eval data/rfc6901_example.json "/" 51 + OK: 0 52 + 53 + Set tests (using add for replace/add semantics): 54 + $ ./test_pointer.exe add '{"bar":"foo"}' '/bar' '"baz"' 55 + {"bar":"baz"} 56 + $ ./test_pointer.exe add '{"bar":"foo"}' '/foo' '"baz"' 57 + {"bar":"foo","foo":"baz"} 58 + $ ./test_pointer.exe add '["foo"]' '/0' '"bar"' 59 + ["bar","foo"] 60 + $ ./test_pointer.exe add '["foo"]' '/-' '"bar"' 61 + ["foo","bar"] 62 + $ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/0' '"baz"' 63 + {"foo":["baz","bar"]} 64 + 65 + Replace tests using special characters: 66 + $ ./test_pointer.exe replace '{"a/b":"bar"}' '/a~1b' '"baz"' 67 + {"a/b":"baz"} 68 + $ ./test_pointer.exe replace '{"c%d":"bar"}' '/c%d' '"baz"' 69 + {"c%d":"baz"} 70 + $ ./test_pointer.exe replace '{"e^f":"bar"}' '/e^f' '"baz"' 71 + {"e^f":"baz"} 72 + $ ./test_pointer.exe replace '{"g|h":"bar"}' '/g|h' '"baz"' 73 + {"g|h":"baz"} 74 + $ ./test_pointer.exe replace '{" ":"bar"}' '/ ' '"baz"' 75 + {" ":"baz"} 76 + $ ./test_pointer.exe replace '{"m~n":"bar"}' '/m~0n' '"baz"' 77 + {"m~n":"baz"} 78 + $ ./test_pointer.exe replace '{"":"bar"}' '/' '"baz"' 79 + {"":"baz"} 80 + 81 + Remove tests: 82 + $ ./test_pointer.exe remove '{"foo":"bar","baz":"qux"}' '/foo' 83 + {"baz":"qux"} 84 + $ ./test_pointer.exe remove '["foo","baz"]' '/1' 85 + ["foo"] 86 + $ ./test_pointer.exe remove '["foo","baz"]' '/0' 87 + ["baz"] 88 + $ ./test_pointer.exe remove '{"foo":["bar"]}' '/foo/0' 89 + {"foo":[]} 90 + 91 + Copy tests: 92 + $ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/baz' 93 + {"foo":"bar","baz":"bar"} 94 + 95 + Test operation: 96 + $ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"bar"' 97 + true 98 + $ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"baz"' 99 + false 100 + $ ./test_pointer.exe test '{"foo":["bar","baz"]}' '/foo' '["bar","baz"]' 101 + true 102 + $ ./test_pointer.exe test '{"foo":"bar"}' '/baz' '"qux"' 103 + false 104 + 105 + Equality tests (pointers that should be equal after roundtrip): 106 + $ ./test_pointer.exe roundtrip "/foo/~0" 107 + OK: /foo/~0 108 + $ ./test_pointer.exe roundtrip "/foo/~1" 109 + OK: /foo/~1 110 + 111 + Has/exists tests (from json-pointer-js): 112 + $ ./test_pointer.exe has '{"foo":"bar"}' '/foo' 113 + true 114 + $ ./test_pointer.exe has '{"foo":"bar"}' '/bar' 115 + false 116 + $ ./test_pointer.exe has '{"foo":{"bar":"baz"}}' '/foo/bar' 117 + true 118 + $ ./test_pointer.exe has '{"foo":{"bar":"baz"}}' '/foo/qux' 119 + false 120 + $ ./test_pointer.exe has '["foo","bar"]' '/0' 121 + true 122 + $ ./test_pointer.exe has '["foo","bar"]' '/1' 123 + true 124 + $ ./test_pointer.exe has '["foo","bar"]' '/2' 125 + false 126 + $ ./test_pointer.exe has '{"foo":["bar"]}' '/foo/0' 127 + true 128 + $ ./test_pointer.exe has '{"foo":["bar"]}' '/foo/1' 129 + false 130 + $ ./test_pointer.exe has '{}' '' 131 + true 132 + $ ./test_pointer.exe has '[]' '' 133 + true 134 + $ ./test_pointer.exe has '{"":0}' '/' 135 + true 136 + $ ./test_pointer.exe has '{"a/b":1}' '/a~1b' 137 + true 138 + $ ./test_pointer.exe has '{"m~n":8}' '/m~0n' 139 + true 140 + 141 + Has with null values: 142 + $ ./test_pointer.exe has '{"foo":null}' '/foo' 143 + true 144 + $ ./test_pointer.exe has 'null' '' 145 + true 146 + 147 + Has with '-' (end marker - should be false for get, points to nonexistent): 148 + $ ./test_pointer.exe has '["foo"]' '/-' 149 + false 150 + $ ./test_pointer.exe has '[]' '/-' 151 + false
+4
test/data/booleans.json
···
··· 1 + { 2 + "true": true, 3 + "false": false 4 + }
+25
test/data/escape.txt
···
··· 1 + # Token escape/unescape tests 2 + # Format: unescaped -> escaped 3 + 4 + # Basic cases 5 + foo -> foo 6 + bar -> bar 7 + 8 + # Tilde escaping 9 + ~ -> ~0 10 + ~0 -> ~00 11 + ~1 -> ~01 12 + 13 + # Slash escaping 14 + / -> ~1 15 + a/b -> a~1b 16 + /foo/bar -> ~1foo~1bar 17 + 18 + # Combined 19 + ~/ -> ~0~1 20 + /~ -> ~1~0 21 + a~b/c -> a~0b~1c 22 + 23 + # No escaping needed 24 + hello world -> hello world 25 + special!@#$%^&*() -> special!@#$%^&*()
+18
test/data/eval_errors.txt
···
··· 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
+19
test/data/eval_rfc6901.txt
···
··· 1 + # Evaluation tests against RFC 6901 example document 2 + # Format: pointer -> expected JSON value 3 + 4 + # Root - whole document 5 + -> {"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8} 6 + 7 + # RFC 6901 Section 5 examples 8 + /foo -> ["bar","baz"] 9 + /foo/0 -> "bar" 10 + /foo/1 -> "baz" 11 + / -> 0 12 + /a~1b -> 1 13 + /c%d -> 2 14 + /e^f -> 3 15 + /g|h -> 4 16 + /i\j -> 5 17 + /k"l -> 6 18 + / -> 7 19 + /m~0n -> 8
+96
test/data/mutations.json
···
··· 1 + { 2 + "add_tests": [ 3 + { 4 + "doc": {"foo": "bar"}, 5 + "pointer": "/baz", 6 + "value": "qux", 7 + "expected": {"foo": "bar", "baz": "qux"} 8 + }, 9 + { 10 + "doc": {"foo": ["bar", "baz"]}, 11 + "pointer": "/foo/1", 12 + "value": "qux", 13 + "expected": {"foo": ["bar", "qux", "baz"]} 14 + }, 15 + { 16 + "doc": {"foo": ["bar"]}, 17 + "pointer": "/foo/-", 18 + "value": "qux", 19 + "expected": {"foo": ["bar", "qux"]} 20 + }, 21 + { 22 + "doc": {"foo": ["bar", "baz"]}, 23 + "pointer": "/foo/0", 24 + "value": "qux", 25 + "expected": {"foo": ["qux", "bar", "baz"]} 26 + } 27 + ], 28 + "remove_tests": [ 29 + { 30 + "doc": {"foo": "bar", "baz": "qux"}, 31 + "pointer": "/baz", 32 + "expected": {"foo": "bar"} 33 + }, 34 + { 35 + "doc": {"foo": ["bar", "qux", "baz"]}, 36 + "pointer": "/foo/1", 37 + "expected": {"foo": ["bar", "baz"]} 38 + } 39 + ], 40 + "replace_tests": [ 41 + { 42 + "doc": {"foo": "bar"}, 43 + "pointer": "/foo", 44 + "value": "baz", 45 + "expected": {"foo": "baz"} 46 + }, 47 + { 48 + "doc": {"foo": ["bar", "baz"]}, 49 + "pointer": "/foo/0", 50 + "value": "qux", 51 + "expected": {"foo": ["qux", "baz"]} 52 + } 53 + ], 54 + "move_tests": [ 55 + { 56 + "doc": {"foo": {"bar": "baz"}, "qux": {"corge": "grault"}}, 57 + "from": "/foo/bar", 58 + "path": "/qux/thud", 59 + "expected": {"foo": {}, "qux": {"corge": "grault", "thud": "baz"}} 60 + }, 61 + { 62 + "doc": {"foo": ["all", "grass", "cows", "eat"]}, 63 + "from": "/foo/1", 64 + "path": "/foo/3", 65 + "expected": {"foo": ["all", "cows", "eat", "grass"]} 66 + } 67 + ], 68 + "copy_tests": [ 69 + { 70 + "doc": {"foo": {"bar": "baz"}}, 71 + "from": "/foo/bar", 72 + "path": "/foo/qux", 73 + "expected": {"foo": {"bar": "baz", "qux": "baz"}} 74 + } 75 + ], 76 + "test_tests": [ 77 + { 78 + "doc": {"foo": "bar"}, 79 + "pointer": "/foo", 80 + "value": "bar", 81 + "expected": true 82 + }, 83 + { 84 + "doc": {"foo": "bar"}, 85 + "pointer": "/foo", 86 + "value": "baz", 87 + "expected": false 88 + }, 89 + { 90 + "doc": {"foo": ["bar", "baz"]}, 91 + "pointer": "/foo", 92 + "value": ["bar", "baz"], 93 + "expected": true 94 + } 95 + ] 96 + }
+11
test/data/nested.json
···
··· 1 + { 2 + "a": { 3 + "b": { 4 + "c": { 5 + "d": "deep value" 6 + } 7 + } 8 + }, 9 + "arr": [[1, 2], [3, 4]], 10 + "mixed": { "list": [{"x": 1}, {"y": 2}] } 11 + }
+4
test/data/nulls.json
···
··· 1 + { 2 + "null": null, 3 + "nested": { "null": null } 4 + }
+5
test/data/numeric_keys.json
···
··· 1 + { 2 + "0": "zero", 3 + "1": "one", 4 + "2": "two" 5 + }
+24
test/data/parse_invalid.txt
···
··· 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
+53
test/data/parse_valid.txt
···
··· 1 + # Valid JSON Pointer parsing tests 2 + # Format: pointer -> indices (Mem:name, Nth:n, End) 3 + 4 + # Root pointer (empty string) 5 + -> [] 6 + 7 + # RFC 6901 Section 5 examples 8 + /foo -> [Mem:foo] 9 + /foo/0 -> [Mem:foo, Nth:0] 10 + / -> [Mem:] 11 + /a~1b -> [Mem:a/b] 12 + /c%d -> [Mem:c%d] 13 + /e^f -> [Mem:e^f] 14 + /g|h -> [Mem:g|h] 15 + /i\j -> [Mem:i\j] 16 + /k"l -> [Mem:k"l] 17 + / -> [Mem: ] 18 + /m~0n -> [Mem:m~n] 19 + 20 + # Array indices 21 + /0 -> [Nth:0] 22 + /1 -> [Nth:1] 23 + /10 -> [Nth:10] 24 + /123 -> [Nth:123] 25 + /999999 -> [Nth:999999] 26 + 27 + # End-of-array marker 28 + /- -> [End] 29 + /foo/- -> [Mem:foo, End] 30 + /arr/0/- -> [Mem:arr, Nth:0, End] 31 + 32 + # Multiple levels 33 + /a/b/c -> [Mem:a, Mem:b, Mem:c] 34 + /0/1/2 -> [Nth:0, Nth:1, Nth:2] 35 + /foo/0/bar/1 -> [Mem:foo, Nth:0, Mem:bar, Nth:1] 36 + 37 + # Escape sequences 38 + /~0 -> [Mem:~] 39 + /~1 -> [Mem:/] 40 + /~0~1 -> [Mem:~/] 41 + /~1~0 -> [Mem:/~] 42 + /~01 -> [Mem:~1] 43 + /a~0b~1c -> [Mem:a~b/c] 44 + 45 + # Empty member names 46 + // -> [Mem:, Mem:] 47 + /// -> [Mem:, Mem:, Mem:] 48 + /foo//bar -> [Mem:foo, Mem:, Mem:bar] 49 + 50 + # Special characters (not needing escaping) 51 + /hello world -> [Mem:hello world] 52 + /with spaces -> [Mem:with spaces] 53 + /unicode-\u00e9 -> [Mem:unicode-\u00e9]
+12
test/data/rfc6901_example.json
···
··· 1 + { 2 + "foo": ["bar", "baz"], 3 + "": 0, 4 + "a/b": 1, 5 + "c%d": 2, 6 + "e^f": 3, 7 + "g|h": 4, 8 + "i\\j": 5, 9 + "k\"l": 6, 10 + " ": 7, 11 + "m~n": 8 12 + }
+5
test/data/unicode.json
···
··· 1 + { 2 + "café": "coffee", 3 + "日本語": "japanese", 4 + "🎉": "party" 5 + }
+21
test/data/uri_fragment.txt
···
··· 1 + # URI Fragment encoding/decoding tests 2 + # Format: uri_fragment <-> pointer_string 3 + 4 + # RFC 6901 Section 6 examples (without leading #) 5 + <-> 6 + /foo <-> /foo 7 + /foo/0 <-> /foo/0 8 + / <-> / 9 + /a~1b <-> /a~1b 10 + /c%25d <-> /c%d 11 + /e%5Ef <-> /e^f 12 + /g%7Ch <-> /g|h 13 + /i%5Cj <-> /i\j 14 + /k%22l <-> /k"l 15 + /%20 <-> / 16 + /m~0n <-> /m~0n 17 + 18 + # Additional percent-encoding cases 19 + /hello%20world <-> /hello world 20 + /%2F <-> // 21 + /%7E <-> /~
+13
test/dune
···
··· 1 + (executable 2 + (name test_pointer) 3 + (libraries jsont jsont_pointer yojson)) 4 + 5 + (cram 6 + (deps test_pointer.exe 7 + data/rfc6901_example.json 8 + data/mutations.json 9 + data/nested.json 10 + data/nulls.json 11 + data/booleans.json 12 + data/unicode.json 13 + data/numeric_keys.json))
+53
test/escape.t
···
··· 1 + Token Escaping Tests 2 + 3 + Escape tilde: 4 + $ ./test_pointer.exe escape "~" 5 + ~0 6 + $ ./test_pointer.exe escape "~0" 7 + ~00 8 + $ ./test_pointer.exe escape "~1" 9 + ~01 10 + 11 + Escape slash: 12 + $ ./test_pointer.exe escape "/" 13 + ~1 14 + $ ./test_pointer.exe escape "a/b" 15 + a~1b 16 + $ ./test_pointer.exe escape "/foo/bar" 17 + ~1foo~1bar 18 + 19 + Combined: 20 + $ ./test_pointer.exe escape "~/" 21 + ~0~1 22 + $ ./test_pointer.exe escape "/~" 23 + ~1~0 24 + $ ./test_pointer.exe escape "a~b/c" 25 + a~0b~1c 26 + 27 + No escaping needed: 28 + $ ./test_pointer.exe escape "foo" 29 + foo 30 + $ ./test_pointer.exe escape "hello world" 31 + hello world 32 + 33 + Unescape: 34 + $ ./test_pointer.exe unescape "~0" 35 + OK: ~ 36 + $ ./test_pointer.exe unescape "~1" 37 + OK: / 38 + $ ./test_pointer.exe unescape "~0~1" 39 + OK: ~/ 40 + $ ./test_pointer.exe unescape "~1~0" 41 + OK: /~ 42 + $ ./test_pointer.exe unescape "a~0b~1c" 43 + OK: a~b/c 44 + $ ./test_pointer.exe unescape "foo" 45 + OK: foo 46 + 47 + Unescape errors: 48 + $ ./test_pointer.exe unescape "~" 49 + ERROR: Invalid JSON Pointer: incomplete escape sequence at end 50 + $ ./test_pointer.exe unescape "~2" 51 + ERROR: Invalid JSON Pointer: invalid escape sequence ~2 52 + $ ./test_pointer.exe unescape "foo~" 53 + ERROR: Invalid JSON Pointer: incomplete escape sequence at end
+134
test/eval.t
···
··· 1 + JSON Pointer Evaluation Tests (RFC 6901 Section 5) 2 + 3 + Using the RFC 6901 example document: 4 + $ cat data/rfc6901_example.json 5 + { 6 + "foo": ["bar", "baz"], 7 + "": 0, 8 + "a/b": 1, 9 + "c%d": 2, 10 + "e^f": 3, 11 + "g|h": 4, 12 + "i\\j": 5, 13 + "k\"l": 6, 14 + " ": 7, 15 + "m~n": 8 16 + } 17 + 18 + Root pointer returns whole document: 19 + $ ./test_pointer.exe eval data/rfc6901_example.json "" 20 + OK: {"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8} 21 + 22 + RFC 6901 examples: 23 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo" 24 + OK: ["bar","baz"] 25 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0" 26 + OK: "bar" 27 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/1" 28 + OK: "baz" 29 + $ ./test_pointer.exe eval data/rfc6901_example.json "/" 30 + OK: 0 31 + $ ./test_pointer.exe eval data/rfc6901_example.json "/a~1b" 32 + OK: 1 33 + $ ./test_pointer.exe eval data/rfc6901_example.json "/c%d" 34 + OK: 2 35 + $ ./test_pointer.exe eval data/rfc6901_example.json "/e^f" 36 + OK: 3 37 + $ ./test_pointer.exe eval data/rfc6901_example.json "/g|h" 38 + OK: 4 39 + $ ./test_pointer.exe eval data/rfc6901_example.json '/i\j' 40 + OK: 5 41 + $ ./test_pointer.exe eval data/rfc6901_example.json '/k"l' 42 + OK: 6 43 + $ ./test_pointer.exe eval data/rfc6901_example.json "/ " 44 + OK: 7 45 + $ ./test_pointer.exe eval data/rfc6901_example.json "/m~0n" 46 + OK: 8 47 + 48 + Error: nonexistent member: 49 + $ ./test_pointer.exe eval data/rfc6901_example.json "/nonexistent" 50 + ERROR: JSON Pointer: member 'nonexistent' not found 51 + 52 + Error: index out of bounds: 53 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/2" 54 + ERROR: JSON Pointer: index 2 out of bounds (array has 2 elements) 55 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/99" 56 + ERROR: JSON Pointer: index 99 out of bounds (array has 2 elements) 57 + 58 + Error: invalid array index (not a valid integer): 59 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/bar" 60 + ERROR: JSON Pointer: invalid array index 'bar' 61 + 62 + Error: end marker not allowed in get: 63 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-" 64 + ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element 65 + 66 + Error: navigating through primitive (string): 67 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0" 68 + ERROR: JSON Pointer: cannot index into string with '0' 69 + $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/bar" 70 + ERROR: JSON Pointer: cannot index into string with 'bar' 71 + 72 + Nested evaluation with deep nesting: 73 + $ cat data/nested.json 74 + { 75 + "a": { 76 + "b": { 77 + "c": { 78 + "d": "deep value" 79 + } 80 + } 81 + }, 82 + "arr": [[1, 2], [3, 4]], 83 + "mixed": { "list": [{"x": 1}, {"y": 2}] } 84 + } 85 + $ ./test_pointer.exe eval data/nested.json "/a/b/c/d" 86 + OK: "deep value" 87 + $ ./test_pointer.exe eval data/nested.json "/arr/0/1" 88 + OK: 2 89 + $ ./test_pointer.exe eval data/nested.json "/arr/1/0" 90 + OK: 3 91 + $ ./test_pointer.exe eval data/nested.json "/mixed/list/0/x" 92 + OK: 1 93 + $ ./test_pointer.exe eval data/nested.json "/mixed/list/1/y" 94 + OK: 2 95 + 96 + Null value handling: 97 + $ cat data/nulls.json 98 + { 99 + "null": null, 100 + "nested": { "null": null } 101 + } 102 + $ ./test_pointer.exe eval data/nulls.json "/null" 103 + OK: null 104 + $ ./test_pointer.exe eval data/nulls.json "/nested/null" 105 + OK: null 106 + 107 + Boolean values: 108 + $ cat data/booleans.json 109 + { 110 + "true": true, 111 + "false": false 112 + } 113 + $ ./test_pointer.exe eval data/booleans.json "/true" 114 + OK: true 115 + $ ./test_pointer.exe eval data/booleans.json "/false" 116 + OK: false 117 + 118 + Empty key access: 119 + $ ./test_pointer.exe eval data/rfc6901_example.json "/" 120 + OK: 0 121 + 122 + Unicode member access: 123 + $ cat data/unicode.json 124 + { 125 + "café": "coffee", 126 + "日本語": "japanese", 127 + "🎉": "party" 128 + } 129 + $ ./test_pointer.exe eval data/unicode.json "/café" 130 + OK: "coffee" 131 + $ ./test_pointer.exe eval data/unicode.json "/日本語" 132 + OK: "japanese" 133 + $ ./test_pointer.exe eval data/unicode.json "/🎉" 134 + OK: "party"
+167
test/mutations.t
···
··· 1 + JSON Pointer Mutation Tests (RFC 6902 JSON Patch operations) 2 + 3 + Add to object: 4 + $ ./test_pointer.exe add '{"foo":"bar"}' '/baz' '"qux"' 5 + {"foo":"bar","baz":"qux"} 6 + 7 + Add to array (insert before): 8 + $ ./test_pointer.exe add '{"foo":["bar","baz"]}' '/foo/1' '"qux"' 9 + {"foo":["bar","qux","baz"]} 10 + 11 + Add to array at beginning: 12 + $ ./test_pointer.exe add '{"foo":["bar","baz"]}' '/foo/0' '"qux"' 13 + {"foo":["qux","bar","baz"]} 14 + 15 + Add to array at end using -: 16 + $ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/-' '"qux"' 17 + {"foo":["bar","qux"]} 18 + 19 + Add replaces existing member: 20 + $ ./test_pointer.exe add '{"foo":"bar"}' '/foo' '"baz"' 21 + {"foo":"baz"} 22 + 23 + Remove from object: 24 + $ ./test_pointer.exe remove '{"foo":"bar","baz":"qux"}' '/baz' 25 + {"foo":"bar"} 26 + 27 + Remove from array: 28 + $ ./test_pointer.exe remove '{"foo":["bar","qux","baz"]}' '/foo/1' 29 + {"foo":["bar","baz"]} 30 + 31 + Remove first element: 32 + $ ./test_pointer.exe remove '{"foo":["bar","baz"]}' '/foo/0' 33 + {"foo":["baz"]} 34 + 35 + Replace in object: 36 + $ ./test_pointer.exe replace '{"foo":"bar"}' '/foo' '"baz"' 37 + {"foo":"baz"} 38 + 39 + Replace in array: 40 + $ ./test_pointer.exe replace '{"foo":["bar","baz"]}' '/foo/0' '"qux"' 41 + {"foo":["qux","baz"]} 42 + 43 + Move within object: 44 + $ ./test_pointer.exe move '{"foo":{"bar":"baz"},"qux":{}}' '/foo/bar' '/qux/thud' 45 + {"foo":{},"qux":{"thud":"baz"}} 46 + 47 + Move within array: 48 + $ ./test_pointer.exe move '{"foo":["a","b","c","d"]}' '/foo/1' '/foo/3' 49 + {"foo":["a","c","d","b"]} 50 + 51 + Copy within object: 52 + $ ./test_pointer.exe copy '{"foo":{"bar":"baz"}}' '/foo/bar' '/foo/qux' 53 + {"foo":{"bar":"baz","qux":"baz"}} 54 + 55 + Copy to new location: 56 + $ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/baz' 57 + {"foo":"bar","baz":"bar"} 58 + 59 + Test equality (true): 60 + $ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"bar"' 61 + true 62 + 63 + Test equality (false): 64 + $ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"baz"' 65 + false 66 + 67 + Test array equality: 68 + $ ./test_pointer.exe test '{"foo":["bar","baz"]}' '/foo' '["bar","baz"]' 69 + true 70 + 71 + Test nonexistent path returns false: 72 + $ ./test_pointer.exe test '{"foo":"bar"}' '/baz' '"qux"' 73 + false 74 + 75 + Error: remove root: 76 + $ ./test_pointer.exe remove '{"foo":"bar"}' '' 77 + ERROR: JSON Pointer: cannot remove root document 78 + 79 + Error: remove nonexistent: 80 + $ ./test_pointer.exe remove '{"foo":"bar"}' '/baz' 81 + ERROR: JSON Pointer: member 'baz' not found for remove 82 + 83 + Error: replace nonexistent: 84 + $ ./test_pointer.exe replace '{"foo":"bar"}' '/baz' '"qux"' 85 + ERROR: JSON Pointer: member 'baz' not found 86 + 87 + Error: add to out of bounds index: 88 + $ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/5' '"qux"' 89 + ERROR: JSON Pointer: index 5 out of bounds for add (array has 1 elements) 90 + 91 + Add nested path (parent must exist): 92 + $ ./test_pointer.exe add '{"foo":{}}' '/foo/bar' '"baz"' 93 + {"foo":{"bar":"baz"}} 94 + 95 + Add to root-level array: 96 + $ ./test_pointer.exe add '["a","b"]' '/1' '"x"' 97 + ["a","x","b"] 98 + $ ./test_pointer.exe add '["a","b"]' '/-' '"c"' 99 + ["a","b","c"] 100 + $ ./test_pointer.exe add '["a","b"]' '/0' '"x"' 101 + ["x","a","b"] 102 + 103 + Replace root document: 104 + $ ./test_pointer.exe replace '{"foo":"bar"}' '' '"new root"' 105 + "new root" 106 + $ ./test_pointer.exe replace '["a","b"]' '' '{"replaced":true}' 107 + {"replaced":true} 108 + 109 + Move from array to object: 110 + $ ./test_pointer.exe move '{"arr":["x","y"],"obj":{}}' '/arr/0' '/obj/item' 111 + {"arr":["y"],"obj":{"item":"x"}} 112 + 113 + Copy array element: 114 + $ ./test_pointer.exe copy '{"arr":["x","y"]}' '/arr/0' '/arr/-' 115 + {"arr":["x","y","x"]} 116 + 117 + Test with numeric string member names (RFC 6901: context determines interpretation): 118 + $ ./test_pointer.exe add '{"0":"zero","1":"one"}' '/2' '"two"' 119 + {"0":"zero","1":"one","2":"two"} 120 + $ ./test_pointer.exe eval data/numeric_keys.json "/0" 121 + OK: "zero" 122 + 123 + Special characters in member names: 124 + $ ./test_pointer.exe add '{}' '/a~1b' '"slash"' 125 + {"a/b":"slash"} 126 + $ ./test_pointer.exe add '{}' '/m~0n' '"tilde"' 127 + {"m~n":"tilde"} 128 + $ ./test_pointer.exe replace '{"a/b":"old"}' '/a~1b' '"new"' 129 + {"a/b":"new"} 130 + 131 + Deep nested mutations: 132 + $ ./test_pointer.exe add '{"a":{"b":{"c":{}}}}' '/a/b/c/d' '"deep"' 133 + {"a":{"b":{"c":{"d":"deep"}}}} 134 + $ ./test_pointer.exe remove '{"a":{"b":{"c":{"d":"deep"}}}}' '/a/b/c/d' 135 + {"a":{"b":{"c":{}}}} 136 + 137 + Error: remove from nonexistent array index: 138 + $ ./test_pointer.exe remove '{"foo":["bar"]}' '/foo/5' 139 + ERROR: JSON Pointer: index 5 out of bounds for remove 140 + 141 + Error: move to descendant of source (path doesn't exist after removal): 142 + $ ./test_pointer.exe move '{"a":{"b":"c"}}' '/a' '/a/b' 143 + ERROR: JSON Pointer: member 'a' not found 144 + 145 + Copy to same location (no-op essentially): 146 + $ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/foo' 147 + {"foo":"bar"} 148 + 149 + Test value equality with different types: 150 + $ ./test_pointer.exe test '{"n":1}' '/n' '1' 151 + true 152 + $ ./test_pointer.exe test '{"n":1.0}' '/n' '1' 153 + true 154 + $ ./test_pointer.exe test '{"n":"1"}' '/n' '1' 155 + false 156 + $ ./test_pointer.exe test '{"b":true}' '/b' 'true' 157 + true 158 + $ ./test_pointer.exe test '{"b":false}' '/b' 'false' 159 + true 160 + $ ./test_pointer.exe test '{"n":null}' '/n' 'null' 161 + true 162 + 163 + Test deep equality: 164 + $ ./test_pointer.exe test '{"obj":{"a":1,"b":2}}' '/obj' '{"a":1,"b":2}' 165 + true 166 + $ ./test_pointer.exe test '{"arr":[1,2,3]}' '/arr' '[1,2,3]' 167 + true
+135
test/parse.t
···
··· 1 + JSON Pointer Parsing Tests (RFC 6901) 2 + 3 + Root pointer (empty string): 4 + $ ./test_pointer.exe parse "" 5 + OK: [] 6 + 7 + RFC 6901 Section 5 examples: 8 + $ ./test_pointer.exe parse "/foo" 9 + OK: [Mem:foo] 10 + $ ./test_pointer.exe parse "/foo/0" 11 + OK: [Mem:foo, Nth:0] 12 + $ ./test_pointer.exe parse "/" 13 + OK: [Mem:] 14 + $ ./test_pointer.exe parse "/a~1b" 15 + OK: [Mem:a/b] 16 + $ ./test_pointer.exe parse "/c%d" 17 + OK: [Mem:c%d] 18 + $ ./test_pointer.exe parse "/e^f" 19 + OK: [Mem:e^f] 20 + $ ./test_pointer.exe parse "/g|h" 21 + OK: [Mem:g|h] 22 + $ ./test_pointer.exe parse '/i\j' 23 + OK: [Mem:i\j] 24 + $ ./test_pointer.exe parse '/k"l' 25 + OK: [Mem:k"l] 26 + $ ./test_pointer.exe parse "/ " 27 + OK: [Mem: ] 28 + $ ./test_pointer.exe parse "/m~0n" 29 + OK: [Mem:m~n] 30 + 31 + Array indices: 32 + $ ./test_pointer.exe parse "/0" 33 + OK: [Nth:0] 34 + $ ./test_pointer.exe parse "/1" 35 + OK: [Nth:1] 36 + $ ./test_pointer.exe parse "/10" 37 + OK: [Nth:10] 38 + $ ./test_pointer.exe parse "/123" 39 + OK: [Nth:123] 40 + 41 + End-of-array marker: 42 + $ ./test_pointer.exe parse "/-" 43 + OK: [End] 44 + $ ./test_pointer.exe parse "/foo/-" 45 + OK: [Mem:foo, End] 46 + 47 + Multiple levels: 48 + $ ./test_pointer.exe parse "/a/b/c" 49 + OK: [Mem:a, Mem:b, Mem:c] 50 + $ ./test_pointer.exe parse "/0/1/2" 51 + OK: [Nth:0, Nth:1, Nth:2] 52 + $ ./test_pointer.exe parse "/foo/0/bar/1" 53 + OK: [Mem:foo, Nth:0, Mem:bar, Nth:1] 54 + 55 + Escape sequences: 56 + $ ./test_pointer.exe parse "/~0" 57 + OK: [Mem:~] 58 + $ ./test_pointer.exe parse "/~1" 59 + OK: [Mem:/] 60 + $ ./test_pointer.exe parse "/~0~1" 61 + OK: [Mem:~/] 62 + $ ./test_pointer.exe parse "/~1~0" 63 + OK: [Mem:/~] 64 + $ ./test_pointer.exe parse "/~01" 65 + OK: [Mem:~1] 66 + 67 + Empty member names: 68 + $ ./test_pointer.exe parse "//" 69 + OK: [Mem:, Mem:] 70 + $ ./test_pointer.exe parse "///" 71 + OK: [Mem:, Mem:, Mem:] 72 + 73 + Invalid: must start with / 74 + $ ./test_pointer.exe parse "foo" 75 + ERROR: Invalid JSON Pointer: must be empty or start with '/': foo 76 + $ ./test_pointer.exe parse "a/b" 77 + ERROR: Invalid JSON Pointer: must be empty or start with '/': a/b 78 + 79 + Invalid: incomplete escape 80 + $ ./test_pointer.exe parse "/~" 81 + ERROR: Invalid JSON Pointer: incomplete escape sequence at end 82 + $ ./test_pointer.exe parse "/foo~" 83 + ERROR: Invalid JSON Pointer: incomplete escape sequence at end 84 + 85 + Invalid: bad escape sequence 86 + $ ./test_pointer.exe parse "/~2" 87 + ERROR: Invalid JSON Pointer: invalid escape sequence ~2 88 + $ ./test_pointer.exe parse "/~a" 89 + ERROR: Invalid JSON Pointer: invalid escape sequence ~a 90 + 91 + Leading zeros - valid as tokens, become member names (invalid as array indices at eval time): 92 + $ ./test_pointer.exe parse "/00" 93 + OK: [Mem:00] 94 + $ ./test_pointer.exe parse "/01" 95 + OK: [Mem:01] 96 + $ ./test_pointer.exe parse "/007" 97 + OK: [Mem:007] 98 + 99 + RFC 6901 Section 4: ~01 decodes to ~1, not / (order matters): 100 + $ ./test_pointer.exe parse "/~01" 101 + OK: [Mem:~1] 102 + $ ./test_pointer.exe parse "/~10" 103 + OK: [Mem:/0] 104 + 105 + Unicode characters (RFC 6901 specifies JSON Pointer is Unicode): 106 + $ ./test_pointer.exe parse "/café" 107 + OK: [Mem:café] 108 + $ ./test_pointer.exe parse "/日本語" 109 + OK: [Mem:日本語] 110 + $ ./test_pointer.exe parse "/emoji🎉" 111 + OK: [Mem:emoji🎉] 112 + 113 + Numeric member names (should be parsed as indices when valid): 114 + $ ./test_pointer.exe parse "/0" 115 + OK: [Nth:0] 116 + $ ./test_pointer.exe parse "/-1" 117 + OK: [Mem:-1] 118 + $ ./test_pointer.exe parse "/+1" 119 + OK: [Mem:+1] 120 + 121 + Very deep paths: 122 + $ ./test_pointer.exe parse "/a/b/c/d/e/f/g/h/i/j" 123 + OK: [Mem:a, Mem:b, Mem:c, Mem:d, Mem:e, Mem:f, Mem:g, Mem:h, Mem:i, Mem:j] 124 + 125 + Complex escape sequences: 126 + $ ./test_pointer.exe parse "/~0~0" 127 + OK: [Mem:~~] 128 + $ ./test_pointer.exe parse "/~1~1" 129 + OK: [Mem://] 130 + $ ./test_pointer.exe parse "/a~0b~1c~0d~1e" 131 + OK: [Mem:a~b/c~d/e] 132 + 133 + Invalid: tilde at end of path: 134 + $ ./test_pointer.exe parse "/foo/bar~" 135 + ERROR: Invalid JSON Pointer: incomplete escape sequence at end
+234
test/test_pointer.ml
···
··· 1 + (* Test runner for jsont_pointer *) 2 + 3 + let read_file path = 4 + let ic = open_in path in 5 + let n = in_channel_length ic in 6 + let s = really_input_string ic n in 7 + close_in ic; 8 + s 9 + 10 + (* Convert Yojson.Safe.t to Jsont.json *) 11 + let rec yojson_to_jsont (y : Yojson.Safe.t) : Jsont.json = 12 + match y with 13 + | `Null -> Jsont.Json.null () 14 + | `Bool b -> Jsont.Json.bool b 15 + | `Int i -> Jsont.Json.number (float_of_int i) 16 + | `Float f -> Jsont.Json.number f 17 + | `String s -> Jsont.Json.string s 18 + | `List l -> Jsont.Json.list (List.map yojson_to_jsont l) 19 + | `Assoc pairs -> 20 + let members = List.map (fun (k, v) -> 21 + Jsont.Json.mem (Jsont.Json.name k) (yojson_to_jsont v) 22 + ) pairs in 23 + Jsont.Json.object' members 24 + | `Intlit s -> Jsont.Json.number (float_of_string s) 25 + 26 + (* Convert Jsont.json to Yojson.Safe.t for output *) 27 + let rec jsont_to_yojson (j : Jsont.json) : Yojson.Safe.t = 28 + match j with 29 + | Jsont.Null _ -> `Null 30 + | Jsont.Bool (b, _) -> `Bool b 31 + | Jsont.Number (f, _) -> 32 + if Float.is_integer f && Float.abs f < 2e15 then 33 + `Int (int_of_float f) 34 + else 35 + `Float f 36 + | Jsont.String (s, _) -> `String s 37 + | Jsont.Array (l, _) -> `List (List.map jsont_to_yojson l) 38 + | Jsont.Object (members, _) -> 39 + `Assoc (List.map (fun ((name, _), v) -> 40 + (name, jsont_to_yojson v) 41 + ) members) 42 + 43 + let parse_json s = 44 + yojson_to_jsont (Yojson.Safe.from_string s) 45 + 46 + let json_to_string json = 47 + Yojson.Safe.to_string (jsont_to_yojson json) 48 + 49 + (* Test: parse pointer and print indices *) 50 + let test_parse pointer_str = 51 + try 52 + let p = Jsont_pointer.of_string pointer_str in 53 + let indices = Jsont_pointer.indices p in 54 + let index_strs = List.map (fun idx -> 55 + match idx with 56 + | Jsont_pointer.Index.Mem s -> Printf.sprintf "Mem:%s" s 57 + | Jsont_pointer.Index.Nth n -> Printf.sprintf "Nth:%d" n 58 + | Jsont_pointer.Index.End -> "End" 59 + ) indices in 60 + Printf.printf "OK: [%s]\n" (String.concat ", " index_strs) 61 + with Jsont.Error e -> 62 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 63 + 64 + (* Test: roundtrip pointer string *) 65 + let test_roundtrip pointer_str = 66 + try 67 + let p = Jsont_pointer.of_string pointer_str in 68 + let s = Jsont_pointer.to_string p in 69 + if s = pointer_str then 70 + Printf.printf "OK: %s\n" s 71 + else 72 + Printf.printf "MISMATCH: input=%s output=%s\n" pointer_str s 73 + with Jsont.Error e -> 74 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 75 + 76 + (* Test: evaluate pointer against JSON *) 77 + let test_eval json_path pointer_str = 78 + try 79 + let json = parse_json (read_file json_path) in 80 + let p = Jsont_pointer.of_string pointer_str in 81 + let result = Jsont_pointer.get p json in 82 + Printf.printf "OK: %s\n" (json_to_string result) 83 + with 84 + | Jsont.Error e -> 85 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 86 + | Failure e -> 87 + Printf.printf "FAIL: %s\n" e 88 + 89 + (* Test: escape token *) 90 + let test_escape token = 91 + let escaped = Jsont_pointer.Token.escape token in 92 + Printf.printf "%s\n" escaped 93 + 94 + (* Test: unescape token *) 95 + let test_unescape token = 96 + try 97 + let unescaped = Jsont_pointer.Token.unescape token in 98 + Printf.printf "OK: %s\n" unescaped 99 + with Jsont.Error e -> 100 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 101 + 102 + (* Test: URI fragment roundtrip *) 103 + let test_uri_fragment pointer_str = 104 + try 105 + let p = Jsont_pointer.of_string pointer_str in 106 + let frag = Jsont_pointer.to_uri_fragment p in 107 + let p2 = Jsont_pointer.of_uri_fragment frag in 108 + let s2 = Jsont_pointer.to_string p2 in 109 + if s2 = pointer_str then 110 + Printf.printf "OK: %s -> %s\n" pointer_str frag 111 + else 112 + Printf.printf "MISMATCH: %s -> %s -> %s\n" pointer_str frag s2 113 + with Jsont.Error e -> 114 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 115 + 116 + (* Test: add operation *) 117 + let test_add json_str pointer_str value_str = 118 + try 119 + let json = parse_json json_str in 120 + let p = Jsont_pointer.of_string pointer_str in 121 + let value = parse_json value_str in 122 + let result = Jsont_pointer.add p json ~value in 123 + Printf.printf "%s\n" (json_to_string result) 124 + with Jsont.Error e -> 125 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 126 + 127 + (* Test: remove operation *) 128 + let test_remove json_str pointer_str = 129 + try 130 + let json = parse_json json_str in 131 + let p = Jsont_pointer.of_string pointer_str in 132 + let result = Jsont_pointer.remove p json in 133 + Printf.printf "%s\n" (json_to_string result) 134 + with Jsont.Error e -> 135 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 136 + 137 + (* Test: replace operation *) 138 + let test_replace json_str pointer_str value_str = 139 + try 140 + let json = parse_json json_str in 141 + let p = Jsont_pointer.of_string pointer_str in 142 + let value = parse_json value_str in 143 + let result = Jsont_pointer.replace p json ~value in 144 + Printf.printf "%s\n" (json_to_string result) 145 + with Jsont.Error e -> 146 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 147 + 148 + (* Test: move operation *) 149 + let test_move json_str from_str path_str = 150 + try 151 + let json = parse_json json_str in 152 + let from = Jsont_pointer.of_string from_str in 153 + let path = Jsont_pointer.of_string path_str in 154 + let result = Jsont_pointer.move ~from ~path json in 155 + Printf.printf "%s\n" (json_to_string result) 156 + with Jsont.Error e -> 157 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 158 + 159 + (* Test: copy operation *) 160 + let test_copy json_str from_str path_str = 161 + try 162 + let json = parse_json json_str in 163 + let from = Jsont_pointer.of_string from_str in 164 + let path = Jsont_pointer.of_string path_str in 165 + let result = Jsont_pointer.copy ~from ~path json in 166 + Printf.printf "%s\n" (json_to_string result) 167 + with Jsont.Error e -> 168 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 169 + 170 + (* Test: test operation *) 171 + let test_test json_str pointer_str expected_str = 172 + try 173 + let json = parse_json json_str in 174 + let p = Jsont_pointer.of_string pointer_str in 175 + let expected = parse_json expected_str in 176 + let result = Jsont_pointer.test p json ~expected in 177 + Printf.printf "%b\n" result 178 + with Jsont.Error e -> 179 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 180 + 181 + (* Test: has operation (checks if pointer exists) *) 182 + let test_has json_str pointer_str = 183 + try 184 + let json = parse_json json_str in 185 + let p = Jsont_pointer.of_string pointer_str in 186 + let result = Jsont_pointer.find p json in 187 + Printf.printf "%b\n" (Option.is_some result) 188 + with Jsont.Error e -> 189 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 190 + 191 + let () = 192 + match Array.to_list Sys.argv with 193 + | _ :: "parse" :: pointer :: _ -> 194 + test_parse pointer 195 + | _ :: "roundtrip" :: pointer :: _ -> 196 + test_roundtrip pointer 197 + | _ :: "eval" :: json_path :: pointer :: _ -> 198 + test_eval json_path pointer 199 + | _ :: "escape" :: token :: _ -> 200 + test_escape token 201 + | _ :: "unescape" :: token :: _ -> 202 + test_unescape token 203 + | _ :: "uri-fragment" :: pointer :: _ -> 204 + test_uri_fragment pointer 205 + | _ :: "add" :: json :: pointer :: value :: _ -> 206 + test_add json pointer value 207 + | _ :: "remove" :: json :: pointer :: _ -> 208 + test_remove json pointer 209 + | _ :: "replace" :: json :: pointer :: value :: _ -> 210 + test_replace json pointer value 211 + | _ :: "move" :: json :: from :: path :: _ -> 212 + test_move json from path 213 + | _ :: "copy" :: json :: from :: path :: _ -> 214 + test_copy json from path 215 + | _ :: "test" :: json :: pointer :: expected :: _ -> 216 + test_test json pointer expected 217 + | _ :: "has" :: json :: pointer :: _ -> 218 + test_has json pointer 219 + | _ -> 220 + Printf.printf "Usage:\n"; 221 + Printf.printf " test_pointer parse <pointer>\n"; 222 + Printf.printf " test_pointer roundtrip <pointer>\n"; 223 + Printf.printf " test_pointer eval <json-file> <pointer>\n"; 224 + Printf.printf " test_pointer escape <token>\n"; 225 + Printf.printf " test_pointer unescape <token>\n"; 226 + Printf.printf " test_pointer uri-fragment <pointer>\n"; 227 + Printf.printf " test_pointer add <json> <pointer> <value>\n"; 228 + Printf.printf " test_pointer remove <json> <pointer>\n"; 229 + Printf.printf " test_pointer replace <json> <pointer> <value>\n"; 230 + Printf.printf " test_pointer move <json> <from> <path>\n"; 231 + Printf.printf " test_pointer copy <json> <from> <path>\n"; 232 + Printf.printf " test_pointer test <json> <pointer> <expected>\n"; 233 + Printf.printf " test_pointer has <json> <pointer>\n"; 234 + exit 1
+77
test/uri_fragment.t
···
··· 1 + URI Fragment Encoding Tests (RFC 6901 Section 6) 2 + 3 + Roundtrip through URI fragment encoding: 4 + $ ./test_pointer.exe uri-fragment "" 5 + OK: -> 6 + $ ./test_pointer.exe uri-fragment "/foo" 7 + OK: /foo -> /foo 8 + $ ./test_pointer.exe uri-fragment "/foo/0" 9 + OK: /foo/0 -> /foo/0 10 + $ ./test_pointer.exe uri-fragment "/" 11 + OK: / -> / 12 + $ ./test_pointer.exe uri-fragment "/a~1b" 13 + OK: /a~1b -> /a~1b 14 + $ ./test_pointer.exe uri-fragment "/m~0n" 15 + OK: /m~0n -> /m~0n 16 + 17 + Characters requiring percent-encoding: 18 + $ ./test_pointer.exe uri-fragment "/c%d" 19 + OK: /c%d -> /c%25d 20 + $ ./test_pointer.exe uri-fragment "/e^f" 21 + OK: /e^f -> /e%5Ef 22 + $ ./test_pointer.exe uri-fragment "/g|h" 23 + OK: /g|h -> /g%7Ch 24 + $ ./test_pointer.exe uri-fragment '/i\j' 25 + OK: /i\j -> /i%5Cj 26 + $ ./test_pointer.exe uri-fragment '/k"l' 27 + OK: /k"l -> /k%22l 28 + $ ./test_pointer.exe uri-fragment "/ " 29 + OK: / -> /%20 30 + 31 + Roundtrip tests: 32 + $ ./test_pointer.exe roundtrip "" 33 + OK: 34 + $ ./test_pointer.exe roundtrip "/foo" 35 + OK: /foo 36 + $ ./test_pointer.exe roundtrip "/foo/0" 37 + OK: /foo/0 38 + $ ./test_pointer.exe roundtrip "/" 39 + OK: / 40 + $ ./test_pointer.exe roundtrip "/a~1b" 41 + OK: /a~1b 42 + $ ./test_pointer.exe roundtrip "/m~0n" 43 + OK: /m~0n 44 + $ ./test_pointer.exe roundtrip "/-" 45 + OK: /- 46 + $ ./test_pointer.exe roundtrip "/a/b/c" 47 + OK: /a/b/c 48 + 49 + RFC 6901 Section 6 examples (URI fragment encoding): 50 + Note: These test the full RFC 6901 Section 6 examples 51 + $ ./test_pointer.exe uri-fragment "/c%d" 52 + OK: /c%d -> /c%25d 53 + $ ./test_pointer.exe uri-fragment "/e^f" 54 + OK: /e^f -> /e%5Ef 55 + $ ./test_pointer.exe uri-fragment "/g|h" 56 + OK: /g|h -> /g%7Ch 57 + $ ./test_pointer.exe uri-fragment '/i\j' 58 + OK: /i\j -> /i%5Cj 59 + $ ./test_pointer.exe uri-fragment '/k"l' 60 + OK: /k"l -> /k%22l 61 + $ ./test_pointer.exe uri-fragment "/ " 62 + OK: / -> /%20 63 + 64 + Unicode in URI fragments: 65 + $ ./test_pointer.exe uri-fragment "/café" 66 + OK: /café -> /caf%C3%A9 67 + $ ./test_pointer.exe uri-fragment "/日本語" 68 + OK: /日本語 -> /%E6%97%A5%E6%9C%AC%E8%AA%9E 69 + 70 + Combined escapes (tilde escape + URI encode): 71 + Note: %25 in result is the URI encoding of %, and 100% is treated as member name 72 + $ ./test_pointer.exe uri-fragment "/100%" 73 + OK: /100% -> /100%25 74 + 75 + Multiple special chars: 76 + $ ./test_pointer.exe uri-fragment "/a b^c|d" 77 + OK: /a b^c|d -> /a%20b%5Ec%7Cd