RFC6901 JSON Pointer implementation in OCaml using jsont

phantom

+48 -25
bin/jsonpp.ml
··· 17 17 | Ok s -> s 18 18 | Error e -> failwith e 19 19 20 + (* Helper to get indices from either nav or append pointer *) 21 + let indices_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t 22 + | `Append of Jsont_pointer.append Jsont_pointer.t ]) = 23 + match result with 24 + | `Nav p -> Jsont_pointer.indices p 25 + | `Append p -> Jsont_pointer.indices p 26 + 27 + (* Helper to convert to string from either nav or append pointer *) 28 + let to_string_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t 29 + | `Append of Jsont_pointer.append Jsont_pointer.t ]) = 30 + match result with 31 + | `Nav p -> Jsont_pointer.to_string p 32 + | `Append p -> Jsont_pointer.to_string p 33 + 20 34 (* Test: parse pointer and print indices *) 21 35 let test_parse pointer_str = 22 36 try 23 - let p = Jsont_pointer.of_string pointer_str in 24 - let indices = Jsont_pointer.indices p in 37 + let result = Jsont_pointer.of_string pointer_str in 38 + let indices = indices_of_result result in 25 39 let index_strs = List.map (fun idx -> 26 40 match idx with 27 - | `Mem s -> Printf.sprintf "Mem:%s" s 28 - | `Nth n -> Printf.sprintf "Nth:%d" n 29 - | `End -> "End" 41 + | Jsont.Path.Mem (s, _) -> Printf.sprintf "Mem:%s" s 42 + | Jsont.Path.Nth (n, _) -> Printf.sprintf "Nth:%d" n 30 43 ) indices in 31 - Printf.printf "OK: [%s]\n" (String.concat ", " index_strs) 44 + let suffix = match result with `Nav _ -> "" | `Append _ -> ", /-" in 45 + Printf.printf "OK: [%s%s]\n" (String.concat ", " index_strs) suffix 32 46 with Jsont.Error e -> 33 47 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 34 48 35 49 (* Test: roundtrip pointer string *) 36 50 let test_roundtrip pointer_str = 37 51 try 38 - let p = Jsont_pointer.of_string pointer_str in 39 - let s = Jsont_pointer.to_string p in 52 + let result = Jsont_pointer.of_string pointer_str in 53 + let s = to_string_of_result result in 40 54 if s = pointer_str then 41 55 Printf.printf "OK: %s\n" s 42 56 else ··· 48 62 let test_eval json_path pointer_str = 49 63 try 50 64 let json = parse_json (read_file json_path) in 51 - let p = Jsont_pointer.of_string pointer_str in 65 + let p = Jsont_pointer.of_string_nav pointer_str in 52 66 let result = Jsont_pointer.get p json in 53 67 Printf.printf "OK: %s\n" (json_to_string result) 54 68 with ··· 73 87 (* Test: URI fragment roundtrip *) 74 88 let test_uri_fragment pointer_str = 75 89 try 76 - let p = Jsont_pointer.of_string pointer_str in 77 - let frag = Jsont_pointer.to_uri_fragment p in 78 - let p2 = Jsont_pointer.of_uri_fragment frag in 79 - let s2 = Jsont_pointer.to_string p2 in 90 + let result = Jsont_pointer.of_string pointer_str in 91 + let frag = match result with 92 + | `Nav p -> Jsont_pointer.to_uri_fragment p 93 + | `Append p -> Jsont_pointer.to_uri_fragment p 94 + in 95 + let result2 = Jsont_pointer.of_uri_fragment frag in 96 + let s2 = to_string_of_result result2 in 80 97 if s2 = pointer_str then 81 98 Printf.printf "OK: %s -> %s\n" pointer_str frag 82 99 else ··· 88 105 let test_add json_str pointer_str value_str = 89 106 try 90 107 let json = parse_json json_str in 91 - let p = Jsont_pointer.of_string pointer_str in 92 108 let value = parse_json value_str in 93 - let result = Jsont_pointer.add p json ~value in 109 + let result = match Jsont_pointer.of_string pointer_str with 110 + | `Nav p -> Jsont_pointer.add p json ~value 111 + | `Append p -> Jsont_pointer.add p json ~value 112 + in 94 113 Printf.printf "%s\n" (json_to_string result) 95 114 with Jsont.Error e -> 96 115 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) ··· 99 118 let test_remove json_str pointer_str = 100 119 try 101 120 let json = parse_json json_str in 102 - let p = Jsont_pointer.of_string pointer_str in 121 + let p = Jsont_pointer.of_string_nav pointer_str in 103 122 let result = Jsont_pointer.remove p json in 104 123 Printf.printf "%s\n" (json_to_string result) 105 124 with Jsont.Error e -> ··· 109 128 let test_replace json_str pointer_str value_str = 110 129 try 111 130 let json = parse_json json_str in 112 - let p = Jsont_pointer.of_string pointer_str in 131 + let p = Jsont_pointer.of_string_nav pointer_str in 113 132 let value = parse_json value_str in 114 133 let result = Jsont_pointer.replace p json ~value in 115 134 Printf.printf "%s\n" (json_to_string result) ··· 120 139 let test_move json_str from_str path_str = 121 140 try 122 141 let json = parse_json json_str in 123 - let from = Jsont_pointer.of_string from_str in 124 - let path = Jsont_pointer.of_string path_str in 125 - let result = Jsont_pointer.move ~from ~path json in 142 + let from = Jsont_pointer.of_string_nav from_str in 143 + let result = match Jsont_pointer.of_string path_str with 144 + | `Nav path -> Jsont_pointer.move ~from ~path json 145 + | `Append path -> Jsont_pointer.move ~from ~path json 146 + in 126 147 Printf.printf "%s\n" (json_to_string result) 127 148 with Jsont.Error e -> 128 149 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) ··· 131 152 let test_copy json_str from_str path_str = 132 153 try 133 154 let json = parse_json json_str in 134 - let from = Jsont_pointer.of_string from_str in 135 - let path = Jsont_pointer.of_string path_str in 136 - let result = Jsont_pointer.copy ~from ~path json in 155 + let from = Jsont_pointer.of_string_nav from_str in 156 + let result = match Jsont_pointer.of_string path_str with 157 + | `Nav path -> Jsont_pointer.copy ~from ~path json 158 + | `Append path -> Jsont_pointer.copy ~from ~path json 159 + in 137 160 Printf.printf "%s\n" (json_to_string result) 138 161 with Jsont.Error e -> 139 162 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) ··· 142 165 let test_test json_str pointer_str expected_str = 143 166 try 144 167 let json = parse_json json_str in 145 - let p = Jsont_pointer.of_string pointer_str in 168 + let p = Jsont_pointer.of_string_nav pointer_str in 146 169 let expected = parse_json expected_str in 147 170 let result = Jsont_pointer.test p json ~expected in 148 171 Printf.printf "%b\n" result ··· 153 176 let test_has json_str pointer_str = 154 177 try 155 178 let json = parse_json json_str in 156 - let p = Jsont_pointer.of_string pointer_str in 179 + let p = Jsont_pointer.of_string_nav pointer_str in 157 180 let result = Jsont_pointer.find p json in 158 181 Printf.printf "%b\n" (Option.is_some result) 159 182 with Jsont.Error e ->
+218 -107
doc/tutorial.md
··· 4 4 [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates 5 5 the `jsont-pointer` OCaml library through interactive examples. 6 6 7 + ## JSON Pointer vs JSON Path 8 + 9 + Before diving in, it's worth understanding the difference between JSON 10 + Pointer and JSON Path, as they serve different purposes: 11 + 12 + **JSON Pointer** (RFC 6901) is an *indicator syntax* that specifies a 13 + *single location* within JSON data. It always identifies at most one 14 + value. 15 + 16 + **JSON Path** is a *query syntax* that can *search* JSON data and return 17 + *multiple* values matching specified criteria. 18 + 19 + Use JSON Pointer when you need to address a single, specific location 20 + (like JSON Schema's `$ref`). Use JSON Path when you might need multiple 21 + results (like Kubernetes queries). 22 + 23 + The `jsont-pointer` library implements JSON Pointer and integrates with 24 + the `Jsont.Path` type for representing navigation indices. 25 + 7 26 ## Setup 8 27 9 28 First, let's set up our environment with helper functions: 10 29 11 30 ```ocaml 12 31 # open Jsont_pointer;; 13 - # #install_printer Jsont_pointer_top.printer;; 32 + # #install_printer Jsont_pointer_top.nav_printer;; 33 + # #install_printer Jsont_pointer_top.append_printer;; 14 34 # #install_printer Jsont_pointer_top.json_printer;; 15 35 # #install_printer Jsont_pointer_top.error_printer;; 16 36 # let parse_json s = ··· 47 67 The JSON Pointer `/users/0/name` refers to the string `"Alice"`: 48 68 49 69 ```ocaml 50 - # let ptr = of_string "/users/0/name";; 51 - val ptr : t = [`Mem "users"; `Nth 0; `Mem "name"] 70 + # let ptr = of_string_nav "/users/0/name";; 71 + val ptr : nav t = [Mem "users"; Nth 0; Mem "name"] 52 72 # get ptr users_json;; 53 73 - : Jsont.json = "Alice" 54 74 ``` 55 75 56 - In OCaml, this is represented by the `Jsont_pointer.t` type - a sequence 57 - of navigation steps from the document root to a target value. 76 + In OCaml, this is represented by the `'a Jsont_pointer.t` type - a sequence 77 + of navigation steps from the document root to a target value. The phantom 78 + type parameter `'a` encodes whether this is a navigation pointer or an 79 + append pointer (more on this later). 58 80 59 81 ## Syntax: Reference Tokens 60 82 ··· 78 100 Let's see this in action: 79 101 80 102 ```ocaml 81 - # of_string "";; 82 - - : t = [] 103 + # of_string_nav "";; 104 + - : nav t = [] 83 105 ``` 84 106 85 107 The empty pointer has no reference tokens - it points to the root. 86 108 87 109 ```ocaml 88 - # of_string "/foo";; 89 - - : t = [`Mem "foo"] 110 + # of_string_nav "/foo";; 111 + - : nav t = [Mem "foo"] 90 112 ``` 91 113 92 114 The pointer `/foo` has one token: `foo`. Since it's not a number, it's 93 115 interpreted as an object member name (`Mem`). 94 116 95 117 ```ocaml 96 - # of_string "/foo/0";; 97 - - : t = [`Mem "foo"; `Nth 0] 118 + # of_string_nav "/foo/0";; 119 + - : nav t = [Mem "foo"; Nth 0] 98 120 ``` 99 121 100 122 Here we have two tokens: `foo` (a member name) and `0` (interpreted as 101 123 an array index `Nth`). 102 124 103 125 ```ocaml 104 - # of_string "/foo/bar/baz";; 105 - - : t = [`Mem "foo"; `Mem "bar"; `Mem "baz"] 126 + # of_string_nav "/foo/bar/baz";; 127 + - : nav t = [Mem "foo"; Mem "bar"; Mem "baz"] 106 128 ``` 107 129 108 130 Multiple tokens navigate deeper into nested structures. 109 131 110 132 ### The Index Type 111 133 112 - Each reference token becomes an `Index.t` value in the library: 134 + Each reference token is represented using `Jsont.Path.index`: 113 135 114 136 <!-- $MDX skip --> 115 137 ```ocaml 116 - type t = [ 117 - | `Mem of string (* Object member access *) 118 - | `Nth of int (* Array index access *) 119 - | `End (* The special "-" marker for append operations *) 120 - ] 138 + type index = Jsont.Path.index 139 + (* = Jsont.Path.Mem of string * Jsont.Meta.t 140 + | Jsont.Path.Nth of int * Jsont.Meta.t *) 121 141 ``` 122 142 123 - The `Mem` variant holds the **unescaped** member name - you work with the 124 - actual key string (like `"a/b"`) and the library handles any escaping needed 143 + The `Mem` constructor is for object member access, and `Nth` is for array 144 + index access. The member name is **unescaped** - you work with the actual 145 + key string (like `"a/b"`) and the library handles any escaping needed 125 146 for the JSON Pointer string representation. 126 147 127 148 ### Invalid Syntax ··· 129 150 What happens if a pointer doesn't start with `/`? 130 151 131 152 ```ocaml 132 - # of_string "foo";; 153 + # of_string_nav "foo";; 133 154 Exception: 134 155 Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo. 135 156 ``` ··· 140 161 141 162 ```ocaml 142 163 # of_string_result "foo";; 143 - - : (t, string) result = 164 + - : ([ `Append of append t | `Nav of nav t ], string) result = 144 165 Error "Invalid JSON Pointer: must be empty or start with '/': foo" 145 166 # of_string_result "/valid";; 146 - - : (t, string) result = Ok [`Mem "valid"] 167 + - : ([ `Append of append t | `Nav of nav t ], string) result = 168 + Ok (`Nav [Mem "valid"]) 147 169 ``` 148 170 149 171 ## Evaluation: Navigating JSON ··· 190 212 ### Object Member Access 191 213 192 214 ```ocaml 193 - # get (of_string "/foo") rfc_example ;; 215 + # get (of_string_nav "/foo") rfc_example ;; 194 216 - : Jsont.json = ["bar","baz"] 195 217 ``` 196 218 ··· 199 221 ### Array Index Access 200 222 201 223 ```ocaml 202 - # get (of_string "/foo/0") rfc_example ;; 224 + # get (of_string_nav "/foo/0") rfc_example ;; 203 225 - : Jsont.json = "bar" 204 - # get (of_string "/foo/1") rfc_example ;; 226 + # get (of_string_nav "/foo/1") rfc_example ;; 205 227 - : Jsont.json = "baz" 206 228 ``` 207 229 ··· 212 234 JSON allows empty strings as object keys: 213 235 214 236 ```ocaml 215 - # get (of_string "/") rfc_example ;; 237 + # get (of_string_nav "/") rfc_example ;; 216 238 - : Jsont.json = 0 217 239 ``` 218 240 ··· 224 246 The RFC example includes keys with `/` and `~` characters: 225 247 226 248 ```ocaml 227 - # get (of_string "/a~1b") rfc_example ;; 249 + # get (of_string_nav "/a~1b") rfc_example ;; 228 250 - : Jsont.json = 1 229 251 ``` 230 252 ··· 232 254 [below](#escaping-special-characters). 233 255 234 256 ```ocaml 235 - # get (of_string "/m~0n") rfc_example ;; 257 + # get (of_string_nav "/m~0n") rfc_example ;; 236 258 - : Jsont.json = 8 237 259 ``` 238 260 ··· 242 264 to worry about escaping. The `Mem` variant holds the literal key name: 243 265 244 266 ```ocaml 245 - # let slash_ptr = make [`Mem "a/b"];; 246 - val slash_ptr : t = [`Mem "a/b"] 267 + # let slash_ptr = make [mem "a/b"];; 268 + val slash_ptr : nav t = [Mem "a/b"] 247 269 # to_string slash_ptr;; 248 270 - : string = "/a~1b" 249 271 # get slash_ptr rfc_example ;; ··· 257 279 Most characters don't need escaping in JSON Pointer strings: 258 280 259 281 ```ocaml 260 - # get (of_string "/c%d") rfc_example ;; 282 + # get (of_string_nav "/c%d") rfc_example ;; 261 283 - : Jsont.json = 2 262 - # get (of_string "/e^f") rfc_example ;; 284 + # get (of_string_nav "/e^f") rfc_example ;; 263 285 - : Jsont.json = 3 264 - # get (of_string "/g|h") rfc_example ;; 286 + # get (of_string_nav "/g|h") rfc_example ;; 265 287 - : Jsont.json = 4 266 - # get (of_string "/ ") rfc_example ;; 288 + # get (of_string_nav "/ ") rfc_example ;; 267 289 - : Jsont.json = 7 268 290 ``` 269 291 ··· 274 296 What happens when we try to access something that doesn't exist? 275 297 276 298 ```ocaml 277 - # get_result (of_string "/nonexistent") rfc_example;; 299 + # get_result (of_string_nav "/nonexistent") rfc_example;; 278 300 - : (Jsont.json, Jsont.Error.t) result = 279 301 Error JSON Pointer: member 'nonexistent' not found 280 302 File "-": 281 - # find (of_string "/nonexistent") rfc_example;; 303 + # find (of_string_nav "/nonexistent") rfc_example;; 282 304 - : Jsont.json option = None 283 305 ``` 284 306 285 307 Or an out-of-bounds array index: 286 308 287 309 ```ocaml 288 - # find (of_string "/foo/99") rfc_example;; 310 + # find (of_string_nav "/foo/99") rfc_example;; 289 311 - : Jsont.json option = None 290 312 ``` 291 313 292 314 Or try to index into a non-container: 293 315 294 316 ```ocaml 295 - # find (of_string "/foo/0/invalid") rfc_example;; 317 + # find (of_string_nav "/foo/0/invalid") rfc_example;; 296 318 - : Jsont.json option = None 297 319 ``` 298 320 ··· 300 322 301 323 <!-- $MDX skip --> 302 324 ```ocaml 303 - val get : t -> Jsont.json -> Jsont.json 304 - val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 305 - val find : t -> Jsont.json -> Jsont.json option 325 + val get : nav t -> Jsont.json -> Jsont.json 326 + val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 327 + val find : nav t -> Jsont.json -> Jsont.json option 306 328 ``` 307 329 308 330 ### Array Index Rules ··· 318 340 > note that leading zeros are not allowed 319 341 320 342 ```ocaml 321 - # of_string "/foo/0";; 322 - - : t = [`Mem "foo"; `Nth 0] 343 + # of_string_nav "/foo/0";; 344 + - : nav t = [Mem "foo"; Nth 0] 323 345 ``` 324 346 325 347 Zero itself is fine. 326 348 327 349 ```ocaml 328 - # of_string "/foo/01";; 329 - - : t = [`Mem "foo"; `Mem "01"] 350 + # of_string_nav "/foo/01";; 351 + - : nav t = [Mem "foo"; Mem "01"] 330 352 ``` 331 353 332 354 But `01` has a leading zero, so it's NOT treated as an array index - it 333 355 becomes a member name instead. This protects against accidental octal 334 356 interpretation. 335 357 336 - ## The End-of-Array Marker: `-` 358 + ## The End-of-Array Marker: `-` and Type Safety 337 359 338 360 RFC 6901, Section 4 introduces a special token: 339 361 340 362 > exactly the single character "-", making the new referenced value the 341 363 > (nonexistent) member after the last array element. 342 364 343 - This is primarily useful for JSON Patch operations (RFC 6902). Let's see 344 - how it parses: 365 + This `-` marker is unique to JSON Pointer (JSON Path has no equivalent). 366 + It's primarily useful for JSON Patch operations (RFC 6902) to append 367 + elements to arrays. 368 + 369 + ### Navigation vs Append Pointers 345 370 371 + The `jsont-pointer` library uses **phantom types** to encode the difference 372 + between pointers that can be used for navigation and pointers that target 373 + the "append position": 374 + 375 + <!-- $MDX skip --> 346 376 ```ocaml 347 - # of_string "/foo/-";; 348 - - : t = [`Mem "foo"; `End] 377 + type nav (* A pointer to an existing element *) 378 + type append (* A pointer ending with "-" (append position) *) 379 + type 'a t (* Pointer with phantom type parameter *) 349 380 ``` 350 381 351 - The `-` is recognized as a special `End` index. 352 - 353 - However, you cannot evaluate a pointer containing `-` because it refers 354 - to a position that doesn't exist: 382 + When you parse a pointer, you get either a `nav t` or an `append t`: 355 383 356 384 ```ocaml 357 - # find (of_string "/foo/-") rfc_example;; 358 - - : Jsont.json option = None 385 + # of_string "/foo/0";; 386 + - : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "foo"; Nth 0] 387 + # of_string "/foo/-";; 388 + - : [ `Append of append t | `Nav of nav t ] = `Append [Mem "foo"] /- 359 389 ``` 360 390 361 - The RFC explains this: 391 + The `-` creates an `append` pointer. Note that in the internal 392 + representation, the append position is tracked separately (shown as `/-`). 393 + 394 + ### Why Phantom Types? 395 + 396 + The RFC explains that `-` refers to a *nonexistent* position: 362 397 363 398 > Note that the use of the "-" character to index an array will always 364 399 > result in such an error condition because by definition it refers to 365 400 > a nonexistent array element. 366 401 367 - But we'll see later that `-` is very useful for mutation operations! 402 + So you **cannot use `get` or `find`** with an append pointer - it makes 403 + no sense to retrieve a value from a position that doesn't exist! The 404 + library enforces this at compile time: 405 + 406 + ```ocaml 407 + # (* This won't compile: get requires nav t, not append t *) 408 + (* get (match of_string "/foo/-" with `Append p -> p | _ -> assert false) rfc_example;; *) 409 + ``` 410 + 411 + However, append pointers **are** valid for mutation operations like `add`: 412 + 413 + ```ocaml 414 + # let arr_obj = parse_json {|{"foo":["a","b"]}|};; 415 + val arr_obj : Jsont.json = {"foo":["a","b"]} 416 + # match of_string "/foo/-" with 417 + | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c") 418 + | `Nav _ -> assert false ;; 419 + - : Jsont.json = {"foo":["a","b","c"]} 420 + ``` 421 + 422 + For convenience, use `of_string_nav` when you know a pointer shouldn't 423 + contain `-`: 424 + 425 + ```ocaml 426 + # of_string_nav "/foo/0";; 427 + - : nav t = [Mem "foo"; Nth 0] 428 + # of_string_nav "/foo/-";; 429 + Exception: 430 + Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer. 431 + ``` 432 + 433 + ### Creating Append Pointers Programmatically 434 + 435 + You can convert a navigation pointer to an append pointer using `at_end`: 436 + 437 + ```ocaml 438 + # let nav_ptr = of_string_nav "/foo";; 439 + val nav_ptr : nav t = [Mem "foo"] 440 + # let app_ptr = at_end nav_ptr;; 441 + val app_ptr : append t = [Mem "foo"] /- 442 + # to_string app_ptr;; 443 + - : string = "/foo/-" 444 + ``` 368 445 369 446 ## Mutation Operations 370 447 ··· 372 449 (JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer` 373 450 library provides these operations. 374 451 452 + ### Which Pointer Type for Which Operation? 453 + 454 + The phantom type system enforces correct usage: 455 + 456 + | Operation | Accepts | Because | 457 + |-----------|---------|---------| 458 + | `get`, `find` | `nav t` only | Can't retrieve from non-existent position | 459 + | `remove` | `nav t` only | Can't remove what doesn't exist | 460 + | `replace` | `nav t` only | Can't replace what doesn't exist | 461 + | `test` | `nav t` only | Can't test non-existent position | 462 + | `add` | `_ t` (both) | Can add at existing position OR append | 463 + | `set` | `_ t` (both) | Can set existing position OR append | 464 + | `move`, `copy` | `from:nav t`, `path:_ t` | Source must exist, dest can be append | 465 + 375 466 ### Add 376 467 377 468 The `add` operation inserts a value at a location: ··· 379 470 ```ocaml 380 471 # let obj = parse_json {|{"foo":"bar"}|};; 381 472 val obj : Jsont.json = {"foo":"bar"} 382 - # add (of_string "/baz") obj ~value:(Jsont.Json.string "qux") 473 + # add (of_string_nav "/baz") obj ~value:(Jsont.Json.string "qux") 383 474 ;; 384 475 - : Jsont.json = {"foo":"bar","baz":"qux"} 385 476 ``` ··· 389 480 ```ocaml 390 481 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; 391 482 val arr_obj : Jsont.json = {"foo":["a","b"]} 392 - # add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X") 483 + # add (of_string_nav "/foo/1") arr_obj ~value:(Jsont.Json.string "X") 393 484 ;; 394 485 - : Jsont.json = {"foo":["a","X","b"]} 395 486 ``` 396 487 397 - This is where the `-` marker shines - it appends to the end: 488 + This is where the `-` marker and append pointers shine - they append to the end: 398 489 399 490 ```ocaml 400 - # add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c") 491 + # match of_string "/foo/-" with 492 + | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c") 493 + | `Nav _ -> assert false ;; 494 + - : Jsont.json = {"foo":["a","b","c"]} 495 + ``` 496 + 497 + Or more conveniently using `at_end`: 498 + 499 + ```ocaml 500 + # add (at_end (of_string_nav "/foo")) arr_obj ~value:(Jsont.Json.string "c") 401 501 ;; 402 502 - : Jsont.json = {"foo":["a","b","c"]} 403 503 ``` 404 504 405 505 ### Remove 406 506 407 - The `remove` operation deletes a value: 507 + The `remove` operation deletes a value. It only accepts `nav t` because 508 + you can only remove something that exists: 408 509 409 510 ```ocaml 410 511 # let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};; 411 512 val two_fields : Jsont.json = {"foo":"bar","baz":"qux"} 412 - # remove (of_string "/baz") two_fields ;; 513 + # remove (of_string_nav "/baz") two_fields ;; 413 514 - : Jsont.json = {"foo":"bar"} 414 515 ``` 415 516 ··· 418 519 ```ocaml 419 520 # let three_elem = parse_json {|{"foo":["a","b","c"]}|};; 420 521 val three_elem : Jsont.json = {"foo":["a","b","c"]} 421 - # remove (of_string "/foo/1") three_elem ;; 522 + # remove (of_string_nav "/foo/1") three_elem ;; 422 523 - : Jsont.json = {"foo":["a","c"]} 423 524 ``` 424 525 ··· 427 528 The `replace` operation updates an existing value: 428 529 429 530 ```ocaml 430 - # replace (of_string "/foo") obj ~value:(Jsont.Json.string "baz") 531 + # replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz") 431 532 ;; 432 533 - : Jsont.json = {"foo":"baz"} 433 534 ``` 434 535 435 - Unlike `add`, `replace` requires the target to already exist. 536 + Unlike `add`, `replace` requires the target to already exist (hence `nav t`). 436 537 Attempting to replace a nonexistent path raises an error. 437 538 438 539 ### Move 439 540 440 - The `move` operation relocates a value: 541 + The `move` operation relocates a value. The source (`from`) must be a `nav t` 542 + (you can only move something that exists), but the destination (`path`) can 543 + be either: 441 544 442 545 ```ocaml 443 546 # let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; 444 547 val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}} 445 - # move ~from:(of_string "/foo/bar") ~path:(of_string "/qux/thud") nested 548 + # move ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/qux/thud") nested 446 549 ;; 447 550 - : Jsont.json = {"foo":{},"qux":{"thud":"baz"}} 448 551 ``` 449 552 450 553 ### Copy 451 554 452 - The `copy` operation duplicates a value: 555 + The `copy` operation duplicates a value (same typing as `move`): 453 556 454 557 ```ocaml 455 558 # let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; 456 559 val to_copy : Jsont.json = {"foo":{"bar":"baz"}} 457 - # copy ~from:(of_string "/foo/bar") ~path:(of_string "/foo/qux") to_copy 560 + # copy ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/foo/qux") to_copy 458 561 ;; 459 562 - : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}} 460 563 ``` ··· 464 567 The `test` operation verifies a value (useful in JSON Patch): 465 568 466 569 ```ocaml 467 - # test (of_string "/foo") obj ~expected:(Jsont.Json.string "bar");; 570 + # test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");; 468 571 - : bool = true 469 - # test (of_string "/foo") obj ~expected:(Jsont.Json.string "wrong");; 572 + # test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");; 470 573 - : bool = false 471 574 ``` 472 575 ··· 493 596 and escaping happens automatically during serialization: 494 597 495 598 ```ocaml 496 - # let p = make [`Mem "a/b"];; 497 - val p : t = [`Mem "a/b"] 599 + # let p = make [mem "a/b"];; 600 + val p : nav t = [Mem "a/b"] 498 601 # to_string p;; 499 602 - : string = "/a~1b" 500 - # of_string "/a~1b";; 501 - - : t = [`Mem "a/b"] 603 + # of_string_nav "/a~1b";; 604 + - : nav t = [Mem "a/b"] 502 605 ``` 503 606 504 607 ### Escaping in Action ··· 561 664 This adds percent-encoding on top of the `~0`/`~1` escaping: 562 665 563 666 ```ocaml 564 - # to_uri_fragment (of_string "/foo");; 667 + # to_uri_fragment (of_string_nav "/foo");; 565 668 - : string = "/foo" 566 - # to_uri_fragment (of_string "/a~1b");; 669 + # to_uri_fragment (of_string_nav "/a~1b");; 567 670 - : string = "/a~1b" 568 - # to_uri_fragment (of_string "/c%d");; 671 + # to_uri_fragment (of_string_nav "/c%d");; 569 672 - : string = "/c%25d" 570 - # to_uri_fragment (of_string "/ ");; 673 + # to_uri_fragment (of_string_nav "/ ");; 571 674 - : string = "/%20" 572 675 ``` 573 676 ··· 592 695 Instead of parsing strings, you can build pointers from indices: 593 696 594 697 ```ocaml 595 - # let port_ptr = make [`Mem "database"; `Mem "port"];; 596 - val port_ptr : t = [`Mem "database"; `Mem "port"] 698 + # let port_ptr = make [mem "database"; mem "port"];; 699 + val port_ptr : nav t = [Mem "database"; Mem "port"] 597 700 # to_string port_ptr;; 598 701 - : string = "/database/port" 599 702 ``` 600 703 601 - For array access, use `Nth`: 704 + For array access, use the `nth` helper: 602 705 603 706 ```ocaml 604 - # let first_feature_ptr = make [`Mem "features"; `Nth 0];; 605 - val first_feature_ptr : t = [`Mem "features"; `Nth 0] 707 + # let first_feature_ptr = make [mem "features"; nth 0];; 708 + val first_feature_ptr : nav t = [Mem "features"; Nth 0] 606 709 # to_string first_feature_ptr;; 607 710 - : string = "/features/0" 608 711 ``` 609 712 610 713 ### Pointer Navigation 611 714 612 - You can build pointers incrementally using `append`: 715 + You can build pointers incrementally using the `/` operator (or `append_index`): 613 716 614 717 ```ocaml 615 - # let db_ptr = of_string "/database";; 616 - val db_ptr : t = [`Mem "database"] 617 - # let creds_ptr = append db_ptr (`Mem "credentials");; 618 - val creds_ptr : t = [`Mem "database"; `Mem "credentials"] 619 - # let user_ptr = append creds_ptr (`Mem "username");; 620 - val user_ptr : t = [`Mem "database"; `Mem "credentials"; `Mem "username"] 718 + # let db_ptr = of_string_nav "/database";; 719 + val db_ptr : nav t = [Mem "database"] 720 + # let creds_ptr = db_ptr / mem "credentials";; 721 + val creds_ptr : nav t = [Mem "database"; Mem "credentials"] 722 + # let user_ptr = creds_ptr / mem "username";; 723 + val user_ptr : nav t = [Mem "database"; Mem "credentials"; Mem "username"] 621 724 # to_string user_ptr;; 622 725 - : string = "/database/credentials/username" 623 726 ``` ··· 625 728 Or concatenate two pointers: 626 729 627 730 ```ocaml 628 - # let base = of_string "/api/v1";; 629 - val base : t = [`Mem "api"; `Mem "v1"] 630 - # let endpoint = of_string "/users/0";; 631 - val endpoint : t = [`Mem "users"; `Nth 0] 731 + # let base = of_string_nav "/api/v1";; 732 + val base : nav t = [Mem "api"; Mem "v1"] 733 + # let endpoint = of_string_nav "/users/0";; 734 + val endpoint : nav t = [Mem "users"; Nth 0] 632 735 # to_string (concat base endpoint);; 633 736 - : string = "/api/v1/users/0" 634 737 ``` ··· 660 763 ```ocaml 661 764 # let db_host = 662 765 Jsont.Json.decode 663 - (path (of_string "/database/host") Jsont.string) 766 + (path (of_string_nav "/database/host") Jsont.string) 664 767 config_json 665 768 |> Result.get_ok;; 666 769 val db_host : string = "localhost" 667 770 # let db_port = 668 771 Jsont.Json.decode 669 - (path (of_string "/database/port") Jsont.int) 772 + (path (of_string_nav "/database/port") Jsont.int) 670 773 config_json 671 774 |> Result.get_ok;; 672 775 val db_port : int = 5432 ··· 677 780 ```ocaml 678 781 # let features = 679 782 Jsont.Json.decode 680 - (path (of_string "/features") Jsont.(list string)) 783 + (path (of_string_nav "/features") Jsont.(list string)) 681 784 config_json 682 785 |> Result.get_ok;; 683 786 val features : string list = ["auth"; "logging"; "metrics"] ··· 690 793 ```ocaml 691 794 # let timeout = 692 795 Jsont.Json.decode 693 - (path ~absent:30 (of_string "/database/timeout") Jsont.int) 796 + (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int) 694 797 config_json 695 798 |> Result.get_ok;; 696 799 val timeout : int = 30 ··· 710 813 val org_json : Jsont.json = 711 814 {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}} 712 815 # Jsont.Json.decode 713 - (path (of_string "/organization/owner/name") Jsont.string) 816 + (path (of_string_nav "/organization/owner/name") Jsont.string) 714 817 org_json 715 818 |> Result.get_ok;; 716 819 - : string = "Alice" 717 820 # Jsont.Json.decode 718 - (path (of_string "/organization/members/0/age") Jsont.int) 821 + (path (of_string_nav "/organization/members/0/age") Jsont.int) 719 822 org_json 720 823 |> Result.get_ok;; 721 824 - : int = 28 ··· 727 830 728 831 ```ocaml 729 832 # let raw_port = 730 - match get (of_string "/database/port") config_json with 833 + match get (of_string_nav "/database/port") config_json with 731 834 | Jsont.Number (f, _) -> int_of_float f 732 835 | _ -> failwith "expected number";; 733 836 val raw_port : int = 5432 ··· 738 841 ```ocaml 739 842 # let typed_port = 740 843 Jsont.Json.decode 741 - (path (of_string "/database/port") Jsont.int) 844 + (path (of_string_nav "/database/port") Jsont.int) 742 845 config_json 743 846 |> Result.get_ok;; 744 847 val typed_port : int = 5432 ··· 756 859 3. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index) 757 860 4. **URI Encoding**: Pointers can be percent-encoded for use in URIs 758 861 5. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates 862 + 6. **Type Safety**: Phantom types (`nav t` vs `append t`) prevent misuse of append pointers with retrieval operations 759 863 760 864 The `jsont-pointer` library implements all of this with type-safe OCaml 761 865 interfaces, integration with the `jsont` codec system, and proper error 762 866 handling for malformed pointers and missing values. 867 + 868 + ### Key Points on JSON Pointer vs JSON Path 869 + 870 + - **JSON Pointer** addresses a *single* location (like a file path) 871 + - **JSON Path** queries for *multiple* values (like a search) 872 + - The `-` token is unique to JSON Pointer - it means "append position" for arrays 873 + - The library uses phantom types to enforce that `-` (append) pointers cannot be used with `get`/`find`
+231 -216
src/jsont_pointer.ml
··· 54 54 else None 55 55 end 56 56 57 - (* Index type - represents how a token is interpreted in context *) 58 - module Index = struct 59 - type t = [ `Mem of string | `Nth of int | `End ] 60 - 61 - let pp ppf = function 62 - | `Mem s -> Format.fprintf ppf "/%s" (Token.escape s) 63 - | `Nth n -> Format.fprintf ppf "/%d" n 64 - | `End -> Format.fprintf ppf "/-" 57 + (* Index type - directly reuses Jsont.Path.index *) 58 + type index = Jsont.Path.index 65 59 66 - let equal i1 i2 = match i1, i2 with 67 - | `Mem s1, `Mem s2 -> String.equal s1 s2 68 - | `Nth n1, `Nth n2 -> Int.equal n1 n2 69 - | `End, `End -> true 70 - | _ -> false 60 + (* Convenience constructors *) 61 + let mem ?(meta = Jsont.Meta.none) s : index = Jsont.Path.Mem (s, meta) 62 + let nth ?(meta = Jsont.Meta.none) n : index = Jsont.Path.Nth (n, meta) 71 63 72 - let compare i1 i2 = match i1, i2 with 73 - | `Mem s1, `Mem s2 -> String.compare s1 s2 74 - | `Mem _, _ -> -1 75 - | _, `Mem _ -> 1 76 - | `Nth n1, `Nth n2 -> Int.compare n1 n2 77 - | `Nth _, `End -> -1 78 - | `End, `Nth _ -> 1 79 - | `End, `End -> 0 64 + let pp_index ppf = function 65 + | Jsont.Path.Mem (s, _) -> Format.fprintf ppf "/%s" (Token.escape s) 66 + | Jsont.Path.Nth (n, _) -> Format.fprintf ppf "/%d" n 80 67 81 - let of_path_index (idx : Jsont.Path.index) : t = 82 - match idx with 83 - | Jsont.Path.Mem (s, _meta) -> `Mem s 84 - | Jsont.Path.Nth (n, _meta) -> `Nth n 68 + let equal_index i1 i2 = match i1, i2 with 69 + | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.equal s1 s2 70 + | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.equal n1 n2 71 + | _ -> false 85 72 86 - let to_path_index (idx : t) : Jsont.Path.index option = 87 - match idx with 88 - | `Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none)) 89 - | `Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none)) 90 - | `End -> None 91 - end 73 + let compare_index i1 i2 = match i1, i2 with 74 + | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.compare s1 s2 75 + | Jsont.Path.Mem _, Jsont.Path.Nth _ -> -1 76 + | Jsont.Path.Nth _, Jsont.Path.Mem _ -> 1 77 + | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.compare n1 n2 92 78 93 - (* Internal representation: raw unescaped tokens. 94 - Per RFC 6901, interpretation as member name vs array index 95 - depends on the JSON value type at evaluation time. *) 79 + (* Internal representation: raw unescaped tokens *) 96 80 module Segment = struct 97 - type t = 98 - | Token of string (* Unescaped reference token *) 99 - | End (* The "-" token for end-of-array *) 100 - 101 - let of_escaped_string s = 102 - if s = "-" then End 103 - else Token (Token.unescape s) 81 + type t = string (* Unescaped reference token *) 104 82 105 - let to_escaped_string = function 106 - | Token s -> Token.escape s 107 - | End -> "-" 83 + let of_escaped_string s = Token.unescape s 108 84 109 - (* Convert to Index for a given JSON value type *) 110 - let to_index seg ~for_array = 111 - match seg with 112 - | End -> `End 113 - | Token s -> 114 - if for_array then 115 - match Token.is_valid_array_index s with 116 - | Some n -> `Nth n 117 - | None -> `Mem s (* Invalid index becomes member for error msg *) 118 - else 119 - `Mem s 85 + let to_escaped_string s = Token.escape s 120 86 121 - (* Convert from Index *) 122 87 let of_index = function 123 - | `End -> End 124 - | `Mem s -> Token s 125 - | `Nth n -> Token (string_of_int n) 88 + | Jsont.Path.Mem (s, _) -> s 89 + | Jsont.Path.Nth (n, _) -> string_of_int n 90 + 91 + let to_index s : index = 92 + match Token.is_valid_array_index s with 93 + | Some n -> nth n 94 + | None -> mem s 126 95 end 127 96 128 - (* Pointer type - list of segments *) 129 - type t = Segment.t list 97 + (* Phantom types *) 98 + type nav 99 + type append 130 100 131 - let root = [] 101 + (* Pointer type with phantom type parameter *) 102 + type _ t = { 103 + segments : Segment.t list; 104 + is_append : bool; (* true if ends with "-" *) 105 + } 132 106 133 - let is_root p = p = [] 107 + let root = { segments = []; is_append = false } 134 108 135 - (* Convert indices to segments *) 136 - let make indices = List.map Segment.of_index indices 109 + let is_root p = p.segments = [] && not p.is_append 137 110 138 - (* Convert segments to indices, assuming array context for numeric tokens *) 139 - let indices p = List.map (fun seg -> Segment.to_index seg ~for_array:true) p 111 + let make indices = 112 + { segments = List.map Segment.of_index indices; is_append = false } 140 113 141 - let append p idx = p @ [Segment.of_index idx] 114 + let ( / ) p idx = 115 + { segments = p.segments @ [Segment.of_index idx]; is_append = false } 142 116 143 - let concat p1 p2 = p1 @ p2 117 + let append_index = ( / ) 144 118 145 - let parent p = match List.rev p with 119 + let at_end p = 120 + { segments = p.segments; is_append = true } 121 + 122 + let concat p1 p2 = 123 + { segments = p1.segments @ p2.segments; is_append = false } 124 + 125 + let parent p = 126 + match List.rev p.segments with 146 127 | [] -> None 147 - | _ :: rest -> Some (List.rev rest) 128 + | _ :: rest -> Some { segments = List.rev rest; is_append = false } 148 129 149 - let last p = match List.rev p with 130 + let last p = 131 + match List.rev p.segments with 150 132 | [] -> None 151 - | seg :: _ -> Some (Segment.to_index seg ~for_array:true) 133 + | seg :: _ -> Some (Segment.to_index seg) 134 + 135 + let indices (type a) (p : a t) = List.map Segment.to_index p.segments 152 136 153 137 (* Parsing *) 154 138 155 - let of_string s = 156 - if s = "" then root 139 + let parse_segments s = 140 + if s = "" then [] 157 141 else if s.[0] <> '/' then 158 142 Jsont.Error.msgf Jsont.Meta.none 159 143 "Invalid JSON Pointer: must be empty or start with '/': %s" s ··· 162 146 let tokens = String.split_on_char '/' rest in 163 147 List.map Segment.of_escaped_string tokens 164 148 149 + let of_string s : [ `Nav of nav t | `Append of append t ] = 150 + let segments = parse_segments s in 151 + (* Check if ends with "-" *) 152 + match List.rev segments with 153 + | "-" :: rest -> 154 + (* Validate that "-" only appears at the end *) 155 + if List.exists (( = ) "-") rest then 156 + Jsont.Error.msgf Jsont.Meta.none 157 + "Invalid JSON Pointer: '-' can only appear at the end"; 158 + `Append { segments = List.rev rest; is_append = true } 159 + | _ -> 160 + (* Validate no "-" anywhere *) 161 + if List.exists (( = ) "-") segments then 162 + Jsont.Error.msgf Jsont.Meta.none 163 + "Invalid JSON Pointer: '-' can only appear at the end"; 164 + `Nav { segments; is_append = false } 165 + 166 + let of_string_nav s : nav t = 167 + match of_string s with 168 + | `Nav p -> p 169 + | `Append _ -> 170 + Jsont.Error.msgf Jsont.Meta.none 171 + "Invalid JSON Pointer: '-' not allowed in navigation pointer" 172 + 165 173 let of_string_result s = 166 174 try Ok (of_string s) 167 175 with Jsont.Error e -> Error (Jsont.Error.to_string e) ··· 195 203 in 196 204 loop 0 197 205 198 - let of_uri_fragment s = 199 - of_string (percent_decode s) 206 + let of_uri_fragment s = of_string (percent_decode s) 207 + 208 + let of_uri_fragment_nav s = of_string_nav (percent_decode s) 200 209 201 210 let of_uri_fragment_result s = 202 211 try Ok (of_uri_fragment s) ··· 204 213 205 214 (* Serialization *) 206 215 207 - let to_string p = 208 - if p = [] then "" 209 - else 210 - let b = Buffer.create 64 in 211 - List.iter (fun seg -> 212 - Buffer.add_char b '/'; 213 - Buffer.add_string b (Segment.to_escaped_string seg) 214 - ) p; 215 - Buffer.contents b 216 + let to_string (type a) (p : a t) = 217 + let base = 218 + if p.segments = [] then "" 219 + else 220 + let b = Buffer.create 64 in 221 + List.iter (fun seg -> 222 + Buffer.add_char b '/'; 223 + Buffer.add_string b (Segment.to_escaped_string seg) 224 + ) p.segments; 225 + Buffer.contents b 226 + in 227 + if p.is_append then base ^ "/-" else base 216 228 217 229 (* URI fragment percent-encoding *) 218 230 let needs_percent_encoding c = 219 - (* RFC 3986 fragment: unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" *) 220 - (* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" *) 221 - (* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *) 222 231 not ( 223 232 (c >= 'A' && c <= 'Z') || 224 233 (c >= 'a' && c <= 'z') || ··· 247 256 ) s; 248 257 Buffer.contents b 249 258 250 - let to_uri_fragment p = 251 - percent_encode (to_string p) 259 + let to_uri_fragment p = percent_encode (to_string p) 252 260 253 - let pp ppf p = 254 - Format.pp_print_string ppf (to_string p) 261 + let pp ppf p = Format.pp_print_string ppf (to_string p) 255 262 256 - let pp_verbose ppf p = 257 - let pp_index ppf = function 258 - | `Mem s -> Format.fprintf ppf {|`Mem "%s"|} s 259 - | `Nth n -> Format.fprintf ppf "`Nth %d" n 260 - | `End -> Format.fprintf ppf "`End" 263 + let pp_verbose (type a) ppf (p : a t) = 264 + let pp_idx ppf seg = 265 + match Token.is_valid_array_index seg with 266 + | Some n -> Format.fprintf ppf "Nth %d" n 267 + | None -> Format.fprintf ppf {|Mem "%s"|} seg 261 268 in 262 - Format.fprintf ppf "[%a]" 263 - (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_index) 264 - (indices p) 269 + Format.fprintf ppf "[%a]%s" 270 + (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_idx) 271 + p.segments 272 + (if p.is_append then " /-" else "") 265 273 266 274 (* Comparison *) 267 275 268 - let segment_equal s1 s2 = match s1, s2 with 269 - | Segment.Token t1, Segment.Token t2 -> String.equal t1 t2 270 - | Segment.End, Segment.End -> true 271 - | _ -> false 276 + let equal (type a b) (p1 : a t) (p2 : b t) = 277 + List.equal String.equal p1.segments p2.segments && 278 + p1.is_append = p2.is_append 272 279 273 - let segment_compare s1 s2 = match s1, s2 with 274 - | Segment.Token t1, Segment.Token t2 -> String.compare t1 t2 275 - | Segment.Token _, Segment.End -> -1 276 - | Segment.End, Segment.Token _ -> 1 277 - | Segment.End, Segment.End -> 0 278 - 279 - let equal p1 p2 = 280 - List.equal segment_equal p1 p2 281 - 282 - let compare p1 p2 = 283 - List.compare segment_compare p1 p2 280 + let compare (type a b) (p1 : a t) (p2 : b t) = 281 + match List.compare String.compare p1.segments p2.segments with 282 + | 0 -> Bool.compare p1.is_append p2.is_append 283 + | n -> n 284 284 285 285 (* Path conversion *) 286 286 287 - let segment_of_path_index (idx : Jsont.Path.index) : Segment.t = 288 - match idx with 289 - | Jsont.Path.Mem (s, _meta) -> Segment.Token s 290 - | Jsont.Path.Nth (n, _meta) -> Segment.Token (string_of_int n) 291 - 292 - let of_path (p : Jsont.Path.t) : t = 293 - List.rev_map segment_of_path_index (Jsont.Path.rev_indices p) 294 - 295 - let to_path p = 296 - let rec convert acc = function 297 - | [] -> Some acc 298 - | Segment.End :: _ -> None 299 - | Segment.Token s :: rest -> 300 - (* For path conversion, we need to decide if it's a member or index. 301 - We use array context for numeric tokens since Jsont.Path distinguishes. *) 302 - let acc' = match Token.is_valid_array_index s with 303 - | Some n -> Jsont.Path.nth ~meta:Jsont.Meta.none n acc 304 - | None -> Jsont.Path.mem ~meta:Jsont.Meta.none s acc 305 - in 306 - convert acc' rest 307 - in 308 - convert Jsont.Path.root p 287 + let of_path (p : Jsont.Path.t) : nav t = 288 + let segments = List.rev_map Segment.of_index (Jsont.Path.rev_indices p) in 289 + { segments; is_append = false } 309 290 310 - let to_path_exn p = 311 - match to_path p with 312 - | Some path -> path 313 - | None -> 314 - Jsont.Error.msgf Jsont.Meta.none 315 - "Cannot convert JSON Pointer with '-' index to Jsont.Path" 291 + let to_path (p : nav t) : Jsont.Path.t = 292 + List.fold_left (fun acc seg -> 293 + match Token.is_valid_array_index seg with 294 + | Some n -> Jsont.Path.nth n acc 295 + | None -> Jsont.Path.mem seg acc 296 + ) Jsont.Path.root p.segments 316 297 317 298 (* Evaluation helpers *) 318 299 ··· 332 313 if n < 0 || n >= List.length arr then None 333 314 else Some (List.nth arr n) 334 315 335 - (* Evaluation *) 316 + (* Evaluation - only for nav pointers *) 336 317 337 - let rec eval_get p json = 338 - match p with 318 + let rec eval_get segments json = 319 + match segments with 339 320 | [] -> json 340 - | Segment.End :: _ -> 341 - Jsont.Error.msgf (Jsont.Json.meta json) 342 - "JSON Pointer: '-' (end marker) refers to nonexistent array element" 343 - | Segment.Token token :: rest -> 321 + | token :: rest -> 344 322 (match json with 345 323 | Jsont.Object (members, _) -> 346 - (* For objects, token is always a member name *) 347 324 (match get_member token members with 348 325 | Some (_, value) -> eval_get rest value 349 326 | None -> 350 327 Jsont.Error.msgf (Jsont.Json.meta json) 351 328 "JSON Pointer: member '%s' not found" token) 352 329 | Jsont.Array (elements, _) -> 353 - (* For arrays, token must be a valid array index *) 354 330 (match Token.is_valid_array_index token with 355 331 | Some n -> 356 332 (match get_nth n elements with ··· 367 343 "JSON Pointer: cannot index into %s with '%s'" 368 344 (json_sort_string json) token) 369 345 370 - let get p json = eval_get p json 346 + let get (p : nav t) json = eval_get p.segments json 371 347 372 348 let get_result p json = 373 349 try Ok (get p json) ··· 431 407 Jsont.Error.msgf (Jsont.Json.meta json) 432 408 "JSON Pointer: cannot navigate through %s" (json_sort_string json) 433 409 434 - (* Mutation: set *) 410 + (* Mutation: set - works with any pointer type *) 435 411 436 - let rec eval_set p value json = 437 - match p with 438 - | [] -> value 439 - | [Segment.End] -> 412 + let rec eval_set_segments segments is_append value json = 413 + match segments, is_append with 414 + | [], false -> value 415 + | [], true -> 416 + (* Append to array *) 440 417 (match json with 441 418 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta) 442 419 | _ -> 443 420 Jsont.Error.msgf (Jsont.Json.meta json) 444 421 "JSON Pointer: '-' can only be used on arrays, got %s" 445 422 (json_sort_string json)) 446 - | Segment.End :: _ -> 447 - Jsont.Error.msgf (Jsont.Json.meta json) 448 - "JSON Pointer: '-' (end marker) refers to nonexistent array element" 449 - | [Segment.Token token] -> 423 + | [token], false -> 450 424 navigate_to_child token json 451 425 ~on_object:(fun members meta -> 452 426 if Option.is_some (get_member token members) then ··· 463 437 ~on_other:(fun () -> 464 438 Jsont.Error.msgf (Jsont.Json.meta json) 465 439 "JSON Pointer: cannot set in %s" (json_sort_string json)) 466 - | Segment.Token token :: rest -> 440 + | [token], true -> 441 + (* Navigate to token, then append *) 467 442 navigate_to_child token json 468 443 ~on_object:(fun members meta -> 469 444 match get_member token members with 470 445 | Some (_, child) -> 471 - Jsont.Object (set_member token (eval_set rest value child) members, meta) 446 + let child' = eval_set_segments [] true value child in 447 + Jsont.Object (set_member token child' members, meta) 472 448 | None -> error_member_not_found json token) 473 449 ~on_array:(fun elements meta n -> 474 450 match get_nth n elements with 475 451 | Some child -> 476 - Jsont.Array (replace_at n (eval_set rest value child) elements, meta) 452 + let child' = eval_set_segments [] true value child in 453 + Jsont.Array (replace_at n child' elements, meta) 454 + | None -> error_index_out_of_bounds json n) 455 + ~on_other:(fun () -> error_cannot_navigate json) 456 + | token :: rest, _ -> 457 + navigate_to_child token json 458 + ~on_object:(fun members meta -> 459 + match get_member token members with 460 + | Some (_, child) -> 461 + Jsont.Object (set_member token (eval_set_segments rest is_append value child) members, meta) 462 + | None -> error_member_not_found json token) 463 + ~on_array:(fun elements meta n -> 464 + match get_nth n elements with 465 + | Some child -> 466 + Jsont.Array (replace_at n (eval_set_segments rest is_append value child) elements, meta) 477 467 | None -> error_index_out_of_bounds json n) 478 468 ~on_other:(fun () -> error_cannot_navigate json) 479 469 480 - let set p json ~value = eval_set p value json 470 + let set (type a) (p : a t) json ~value = 471 + eval_set_segments p.segments p.is_append value json 481 472 482 - (* Mutation: add (RFC 6902 semantics) *) 473 + (* Mutation: add (RFC 6902 semantics) - works with any pointer type *) 483 474 484 - let rec eval_add p value json = 485 - match p with 486 - | [] -> value 487 - | [Segment.End] -> 475 + let rec eval_add_segments segments is_append value json = 476 + match segments, is_append with 477 + | [], false -> value 478 + | [], true -> 479 + (* Append to array *) 488 480 (match json with 489 481 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta) 490 482 | _ -> 491 483 Jsont.Error.msgf (Jsont.Json.meta json) 492 484 "JSON Pointer: '-' can only be used on arrays, got %s" 493 485 (json_sort_string json)) 494 - | Segment.End :: _ -> 495 - Jsont.Error.msgf (Jsont.Json.meta json) 496 - "JSON Pointer: '-' in non-final position" 497 - | [Segment.Token token] -> 486 + | [token], false -> 498 487 navigate_to_child token json 499 488 ~on_object:(fun members meta -> 500 489 Jsont.Object (set_member token value members, meta)) ··· 509 498 ~on_other:(fun () -> 510 499 Jsont.Error.msgf (Jsont.Json.meta json) 511 500 "JSON Pointer: cannot add to %s" (json_sort_string json)) 512 - | Segment.Token token :: rest -> 501 + | [token], true -> 502 + (* Navigate to token, then append *) 513 503 navigate_to_child token json 514 504 ~on_object:(fun members meta -> 515 505 match get_member token members with 516 506 | Some (_, child) -> 517 - Jsont.Object (set_member token (eval_add rest value child) members, meta) 507 + let child' = eval_add_segments [] true value child in 508 + Jsont.Object (set_member token child' members, meta) 518 509 | None -> error_member_not_found json token) 519 510 ~on_array:(fun elements meta n -> 520 511 match get_nth n elements with 521 512 | Some child -> 522 - Jsont.Array (replace_at n (eval_add rest value child) elements, meta) 513 + let child' = eval_add_segments [] true value child in 514 + Jsont.Array (replace_at n child' elements, meta) 515 + | None -> error_index_out_of_bounds json n) 516 + ~on_other:(fun () -> error_cannot_navigate json) 517 + | token :: rest, _ -> 518 + navigate_to_child token json 519 + ~on_object:(fun members meta -> 520 + match get_member token members with 521 + | Some (_, child) -> 522 + Jsont.Object (set_member token (eval_add_segments rest is_append value child) members, meta) 523 + | None -> error_member_not_found json token) 524 + ~on_array:(fun elements meta n -> 525 + match get_nth n elements with 526 + | Some child -> 527 + Jsont.Array (replace_at n (eval_add_segments rest is_append value child) elements, meta) 523 528 | None -> error_index_out_of_bounds json n) 524 529 ~on_other:(fun () -> error_cannot_navigate json) 525 530 526 - let add p json ~value = eval_add p value json 531 + let add (type a) (p : a t) json ~value = 532 + eval_add_segments p.segments p.is_append value json 527 533 528 - (* Mutation: remove *) 534 + (* Mutation: remove - only for nav pointers *) 529 535 530 - let rec eval_remove p json = 531 - match p with 536 + let rec eval_remove_segments segments json = 537 + match segments with 532 538 | [] -> 533 539 Jsont.Error.msgf Jsont.Meta.none "JSON Pointer: cannot remove root document" 534 - | [Segment.End] -> 535 - Jsont.Error.msgf (Jsont.Json.meta json) 536 - "JSON Pointer: '-' refers to nonexistent element" 537 - | Segment.End :: _ -> 538 - Jsont.Error.msgf (Jsont.Json.meta json) 539 - "JSON Pointer: '-' in non-final position" 540 - | [Segment.Token token] -> 540 + | [token] -> 541 541 navigate_to_child token json 542 542 ~on_object:(fun members meta -> 543 543 if Option.is_some (get_member token members) then ··· 554 554 ~on_other:(fun () -> 555 555 Jsont.Error.msgf (Jsont.Json.meta json) 556 556 "JSON Pointer: cannot remove from %s" (json_sort_string json)) 557 - | Segment.Token token :: rest -> 557 + | token :: rest -> 558 558 navigate_to_child token json 559 559 ~on_object:(fun members meta -> 560 560 match get_member token members with 561 561 | Some (_, child) -> 562 - Jsont.Object (set_member token (eval_remove rest child) members, meta) 562 + Jsont.Object (set_member token (eval_remove_segments rest child) members, meta) 563 563 | None -> error_member_not_found json token) 564 564 ~on_array:(fun elements meta n -> 565 565 match get_nth n elements with 566 566 | Some child -> 567 - Jsont.Array (replace_at n (eval_remove rest child) elements, meta) 567 + Jsont.Array (replace_at n (eval_remove_segments rest child) elements, meta) 568 568 | None -> error_index_out_of_bounds json n) 569 569 ~on_other:(fun () -> error_cannot_navigate json) 570 570 571 - let remove p json = eval_remove p json 571 + let remove (p : nav t) json = eval_remove_segments p.segments json 572 572 573 - (* Mutation: replace *) 573 + (* Mutation: replace - only for nav pointers *) 574 574 575 - let replace p json ~value = 576 - (* Replace requires the target to exist, unlike add *) 575 + let replace (p : nav t) json ~value = 577 576 let _ = get p json in (* Will raise if not found *) 578 - eval_set p value json 577 + eval_set_segments p.segments false value json 579 578 580 579 (* Mutation: move *) 581 580 582 - let rec is_prefix_of p1 p2 = 583 - match p1, p2 with 584 - | [], _ -> true 585 - | _, [] -> false 586 - | h1 :: t1, h2 :: t2 -> segment_equal h1 h2 && is_prefix_of t1 t2 587 - 588 - let move ~from ~path json = 581 + let move ~(from : nav t) ~(path : _ t) json = 589 582 (* Check for cycle: path cannot be a proper prefix of from *) 590 - if is_prefix_of path from && not (equal path from) then 583 + let from_segs = from.segments in 584 + let path_segs = path.segments in 585 + let rec is_prefix p1 p2 = match p1, p2 with 586 + | [], _ -> true 587 + | _, [] -> false 588 + | h1 :: t1, h2 :: t2 -> String.equal h1 h2 && is_prefix t1 t2 589 + in 590 + if is_prefix path_segs from_segs && 591 + not (List.equal String.equal path_segs from_segs && path.is_append = false) then 591 592 Jsont.Error.msgf Jsont.Meta.none 592 593 "JSON Pointer: move would create cycle (path is prefix of from)"; 593 594 let value = get from json in ··· 596 597 597 598 (* Mutation: copy *) 598 599 599 - let copy ~from ~path json = 600 + let copy ~(from : nav t) ~(path : _ t) json = 600 601 let value = get from json in 601 602 add path json ~value 602 603 603 604 (* Mutation: test *) 604 605 605 - let test p json ~expected = 606 + let test (p : nav t) json ~expected = 606 607 Option.fold ~none:false ~some:(Jsont.Json.equal expected) (find p json) 607 608 608 609 (* Jsont codec *) 609 610 610 - let jsont : t Jsont.t = 611 + let jsont : [ `Nav of nav t | `Append of append t ] Jsont.t = 611 612 let dec _meta s = of_string s in 612 - let enc p = to_string p in 613 + let enc = function 614 + | `Nav p -> to_string p 615 + | `Append p -> to_string p 616 + in 613 617 Jsont.Base.string (Jsont.Base.map 614 618 ~kind:"JSON Pointer" 615 619 ~doc:"RFC 6901 JSON Pointer" 616 620 ~dec ~enc ()) 617 621 618 - let jsont_uri_fragment : t Jsont.t = 622 + let jsont_nav : nav t Jsont.t = 623 + let dec _meta s = of_string_nav s in 624 + let enc p = to_string p in 625 + Jsont.Base.string (Jsont.Base.map 626 + ~kind:"JSON Pointer (nav)" 627 + ~doc:"RFC 6901 JSON Pointer (navigation only)" 628 + ~dec ~enc ()) 629 + 630 + let jsont_uri_fragment : [ `Nav of nav t | `Append of append t ] Jsont.t = 619 631 let dec _meta s = of_uri_fragment s in 620 - let enc p = to_uri_fragment p in 632 + let enc = function 633 + | `Nav p -> to_uri_fragment p 634 + | `Append p -> to_uri_fragment p 635 + in 621 636 Jsont.Base.string (Jsont.Base.map 622 637 ~kind:"JSON Pointer (URI fragment)" 623 638 ~doc:"RFC 6901 JSON Pointer in URI fragment encoding" ··· 625 640 626 641 (* Query combinators *) 627 642 628 - let path ?absent p t = 643 + let path ?absent (p : nav t) t = 629 644 let dec json = 630 645 match find p json with 631 646 | Some value -> ··· 642 657 Jsont.map Jsont.json ~dec ~enc:(fun _ -> 643 658 Jsont.Error.msgf Jsont.Meta.none "path: encode not supported") 644 659 645 - let set_path ?(allow_absent = false) t p v = 660 + let set_path (type a) ?(allow_absent = false) t (p : a t) v = 646 661 let encoded = match Jsont.Json.encode' t v with 647 662 | Ok json -> json 648 663 | Error e -> raise (Jsont.Error e) ··· 655 670 in 656 671 Jsont.map Jsont.json ~dec ~enc:(fun j -> j) 657 672 658 - let update_path ?absent p t = 673 + let update_path ?absent (p : nav t) t = 659 674 let dec json = 660 675 let value = match find p json with 661 676 | Some v -> v ··· 681 696 in 682 697 Jsont.map Jsont.json ~dec ~enc:(fun j -> j) 683 698 684 - let delete_path ?(allow_absent = false) p = 699 + let delete_path ?(allow_absent = false) (p : nav t) = 685 700 let dec json = 686 701 if allow_absent then 687 702 match find p json with
+167 -116
src/jsont_pointer.mli
··· 9 9 JSON Pointer parsing, serialization, and evaluation compatible with 10 10 {!Jsont} codecs. 11 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: 12 + {1 JSON Pointer vs JSON Path} 13 + 14 + JSON Pointer (RFC 6901) and {!Jsont.Path} serve similar purposes but 15 + have important differences: 16 + 17 + {ul 18 + {- {b JSON Pointer} is a {e string syntax} for addressing JSON values, 19 + designed for use in URIs and JSON documents (like JSON Patch). 20 + It uses [/] as separator and has escape sequences ([~0], [~1]).} 21 + {- {b Jsont.Path} is an {e OCaml data structure} for programmatic 22 + navigation, with no string representation defined.}} 23 + 24 + A key difference is the [-] token: JSON Pointer's [-] refers to the 25 + (nonexistent) element {e after} the last array element. This is used 26 + for append operations in JSON Patch but is meaningless for retrieval. 27 + {!Jsont.Path} has no equivalent concept. 28 + 29 + This library uses phantom types to enforce this distinction at compile 30 + time: pointers that may contain [-] ({!append} pointers) cannot be 31 + passed to retrieval functions like {!get}. 32 + 33 + {2 Example} 34 + 35 + Given the JSON document: 14 36 {v 15 37 { 16 38 "foo": ["bar", "baz"], ··· 27 49 {- ["/foo/0"] - the string ["bar"]} 28 50 {- ["/"] - the integer [0] (empty string key)} 29 51 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])} 30 - {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}} 52 + {- ["/m~0n"] - the integer [2] ([~0] escapes [~])} 53 + {- ["/foo/-"] - nonexistent; only valid for mutations}} 31 54 32 55 {1:tokens Reference Tokens} 33 56 ··· 60 83 61 84 (** {1 Indices} 62 85 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 86 + Indices are the individual navigation steps in a JSON Pointer. 87 + This library reuses {!Jsont.Path.index} directly - the JSON Pointer 88 + specific [-] token is handled separately via phantom types on the 89 + pointer type itself. *) 67 90 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 - ] 91 + type index = Jsont.Path.index 92 + (** The type for navigation indices. This is exactly {!Jsont.Path.index}: 93 + either [Jsont.Path.Mem (name, meta)] for object member access or 94 + [Jsont.Path.Nth (n, meta)] for array index access. *) 83 95 84 - val pp : Format.formatter -> t -> unit 85 - (** [pp] formats an index in JSON Pointer string notation. *) 96 + val mem : ?meta:Jsont.Meta.t -> string -> index 97 + (** [mem ?meta s] is [Jsont.Path.Mem (s, meta)]. 98 + Convenience constructor for object member access. 99 + [meta] defaults to {!Jsont.Meta.none}. *) 86 100 87 - val equal : t -> t -> bool 88 - (** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *) 101 + val nth : ?meta:Jsont.Meta.t -> int -> index 102 + (** [nth ?meta n] is [Jsont.Path.Nth (n, meta)]. 103 + Convenience constructor for array index access. 104 + [meta] defaults to {!Jsont.Meta.none}. *) 89 105 90 - val compare : t -> t -> int 91 - (** [compare i1 i2] is a total order on indices. *) 106 + val pp_index : Format.formatter -> index -> unit 107 + (** [pp_index] formats an index in JSON Pointer string notation. *) 92 108 93 - (** {2:jsont_conv Conversion with Jsont.Path} *) 109 + val equal_index : index -> index -> bool 110 + (** [equal_index i1 i2] is [true] iff [i1] and [i2] are the same index. *) 94 111 95 - val of_path_index : Jsont.Path.index -> t 96 - (** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *) 112 + val compare_index : index -> index -> int 113 + (** [compare_index i1 i2] is a total order on indices. *) 97 114 98 - val to_path_index : t -> Jsont.Path.index option 99 - (** [to_path_index idx] converts to a {!Jsont.Path.index}. 100 - Returns [None] for [`End] since it has no equivalent in 101 - {!Jsont.Path}. *) 102 - end 115 + (** {1 Pointers} 103 116 104 - (** {1 Pointers} *) 117 + JSON Pointers use phantom types to distinguish between: 118 + {ul 119 + {- {!nav} pointers that reference existing elements (safe for all operations)} 120 + {- {!append} pointers that end with [-] (only valid for {!add} and {!set})}} 105 121 106 - type t 107 - (** The type for JSON Pointers. A pointer is a sequence of {!Index.t} 108 - values representing a path from the root of a JSON document to 109 - a specific value. *) 122 + This ensures at compile time that you cannot accidentally try to 123 + retrieve a nonexistent "end of array" position. *) 124 + 125 + type 'a t 126 + (** The type for JSON Pointers. The phantom type ['a] indicates whether 127 + the pointer can be used for navigation ([nav]) or only for append 128 + operations ([append]). *) 129 + 130 + type nav 131 + (** Phantom type for pointers that reference existing elements. 132 + These can be used with all operations including {!get} and {!find}. *) 133 + 134 + type append 135 + (** Phantom type for pointers ending with [-] (the "after last element" 136 + position). These can only be used with {!add} and {!set}. *) 110 137 111 - val root : t 138 + val root : nav t 112 139 (** [root] is the empty pointer that references the whole document. 113 140 In string form this is [""]. *) 114 141 115 - val is_root : t -> bool 142 + val is_root : _ t -> bool 116 143 (** [is_root p] is [true] iff [p] is the {!root} pointer. *) 117 144 118 - val make : Index.t list -> t 119 - (** [make indices] creates a pointer from a list of indices. 145 + val make : index list -> nav t 146 + (** [make indices] creates a navigation pointer from a list of indices. 120 147 The list is ordered from root to target (i.e., the first element 121 148 is the first step from the root). *) 122 149 123 - val indices : t -> Index.t list 124 - (** [indices p] returns the indices of [p] from root to target. *) 150 + val ( / ) : nav t -> index -> nav t 151 + (** [p / idx] appends [idx] to pointer [p]. Operator form of {!append_index}. *) 152 + 153 + val append_index : nav t -> index -> nav t 154 + (** [append_index p idx] appends [idx] to the end of pointer [p]. *) 125 155 126 - val append : t -> Index.t -> t 127 - (** [append p idx] appends [idx] to the end of pointer [p]. *) 156 + val at_end : nav t -> append t 157 + (** [at_end p] creates an append pointer by adding [-] to [p]. 158 + The resulting pointer refers to the position after the last element 159 + of the array at [p]. Only valid for use with {!add} and {!set}. *) 128 160 129 - val concat : t -> t -> t 161 + val concat : nav t -> nav t -> nav t 130 162 (** [concat p1 p2] appends all indices of [p2] to [p1]. *) 131 163 132 - val parent : t -> t option 164 + val parent : nav t -> nav t option 133 165 (** [parent p] returns the parent pointer of [p], or [None] if [p] 134 166 is the {!root}. *) 135 167 136 - val last : t -> Index.t option 168 + val last : nav t -> index option 137 169 (** [last p] returns the last index of [p], or [None] if [p] is 138 170 the {!root}. *) 139 171 172 + val indices : _ t -> index list 173 + (** [indices p] returns the indices of [p] from root to target. 174 + Note: for append pointers, this returns the indices of the path 175 + portion; the [-] (append position) is not represented as an index. *) 176 + 140 177 (** {2:parsing Parsing} *) 141 178 142 - val of_string : string -> t 179 + val of_string : string -> [ `Nav of nav t | `Append of append t ] 143 180 (** [of_string s] parses a JSON Pointer from its string representation. 144 181 182 + Returns [`Nav p] for pointers without [-], or [`Append p] for 183 + pointers ending with [-]. 184 + 145 185 The string must be either empty (representing the root) or start 146 186 with [/]. Each segment between [/] characters is unescaped as a 147 - reference token. Segments that are valid non-negative integers 148 - without leading zeros become [`Nth] indices; the string [-] 149 - becomes [`End]; all others become [`Mem]. 187 + reference token. 150 188 151 189 @raise Jsont.Error if [s] has invalid syntax: 152 190 - Non-empty string not starting with [/] 153 191 - Invalid escape sequence ([~] not followed by [0] or [1]) 154 - - Array index with leading zeros 155 - - Array index that overflows [int] *) 192 + - [-] appears in non-final position *) 193 + 194 + val of_string_nav : string -> nav t 195 + (** [of_string_nav s] parses a JSON Pointer that must not contain [-]. 196 + 197 + @raise Jsont.Error if [s] has invalid syntax or contains [-]. *) 156 198 157 - val of_string_result : string -> (t, string) result 199 + val of_string_result : string -> ([ `Nav of nav t | `Append of append t ], string) result 158 200 (** [of_string_result s] is like {!of_string} but returns a result 159 201 instead of raising. *) 160 202 161 - val of_uri_fragment : string -> t 203 + val of_uri_fragment : string -> [ `Nav of nav t | `Append of append t ] 162 204 (** [of_uri_fragment s] parses a JSON Pointer from URI fragment form. 163 205 164 206 This is like {!of_string} but first percent-decodes the string ··· 167 209 168 210 @raise Jsont.Error on invalid syntax or invalid percent-encoding. *) 169 211 170 - val of_uri_fragment_result : string -> (t, string) result 212 + val of_uri_fragment_nav : string -> nav t 213 + (** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires 214 + the pointer to not contain [-]. 215 + 216 + @raise Jsont.Error if invalid or contains [-]. *) 217 + 218 + val of_uri_fragment_result : string -> ([ `Nav of nav t | `Append of append t ], string) result 171 219 (** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns 172 220 a result instead of raising. *) 173 221 174 222 (** {2:serializing Serializing} *) 175 223 176 - val to_string : t -> string 224 + val to_string : _ t -> string 177 225 (** [to_string p] serializes [p] to its JSON Pointer string representation. 178 226 179 227 Returns [""] for the root pointer, otherwise [/] followed by 180 - escaped reference tokens joined by [/]. *) 228 + escaped reference tokens joined by [/]. Append pointers include 229 + the trailing [/-]. *) 181 230 182 - val to_uri_fragment : t -> string 231 + val to_uri_fragment : _ t -> string 183 232 (** [to_uri_fragment p] serializes [p] to URI fragment form. 184 233 185 234 This is like {!to_string} but additionally percent-encodes 186 235 characters that are not allowed in URI fragments per RFC 3986. 187 236 The leading [#] is {b not} included in the result. *) 188 237 189 - val pp : Format.formatter -> t -> unit 238 + val pp : Format.formatter -> _ t -> unit 190 239 (** [pp] formats a pointer using {!to_string}. *) 191 240 192 - val pp_verbose : Format.formatter -> t -> unit 241 + val pp_verbose : Format.formatter -> _ t -> unit 193 242 (** [pp_verbose] formats a pointer showing its index structure. 194 - For example, [/foo/0/-] is formatted as [[`Mem "foo"; `Nth 0; `End]]. 243 + For example, [/foo/0] is formatted as [[Mem "foo"; Nth 0]]. 244 + Append pointers show [/-] at the end. 195 245 Useful for debugging and understanding pointer structure. *) 196 246 197 247 (** {2:comparison Comparison} *) 198 248 199 - val equal : t -> t -> bool 200 - (** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *) 249 + val equal : _ t -> _ t -> bool 250 + (** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices 251 + and the same append status. *) 201 252 202 - val compare : t -> t -> int 253 + val compare : _ t -> _ t -> int 203 254 (** [compare p1 p2] is a total order on pointers, comparing indices 204 - lexicographically. *) 255 + lexicographically. Append pointers sort after nav pointers with 256 + the same prefix. *) 205 257 206 258 (** {2:jsont_path Conversion with Jsont.Path} *) 207 259 208 - val of_path : Jsont.Path.t -> t 209 - (** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *) 260 + val of_path : Jsont.Path.t -> nav t 261 + (** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. 262 + Always returns a {!nav} pointer since {!Jsont.Path} has no [-] concept. *) 210 263 211 - val to_path : t -> Jsont.Path.t option 212 - (** [to_path p] converts to a {!Jsont.Path.t}. 213 - Returns [None] if [p] contains an [`End] index. *) 214 - 215 - val to_path_exn : t -> Jsont.Path.t 216 - (** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error} 217 - if conversion fails. *) 264 + val to_path : nav t -> Jsont.Path.t 265 + (** [to_path p] converts a navigation pointer to a {!Jsont.Path.t}. *) 218 266 219 267 (** {1 Evaluation} 220 268 221 269 These functions evaluate a JSON Pointer against a {!Jsont.json} value 222 - to retrieve the referenced value. *) 270 + to retrieve the referenced value. They only accept {!nav} pointers 271 + since {!append} pointers refer to nonexistent positions. *) 223 272 224 - val get : t -> Jsont.json -> Jsont.json 273 + val get : nav t -> Jsont.json -> Jsont.json 225 274 (** [get p json] retrieves the value at pointer [p] in [json]. 226 275 227 276 @raise Jsont.Error if: 228 277 - The pointer references a nonexistent object member 229 278 - The pointer references an out-of-bounds array index 230 - - The pointer contains [`End] (since [-] always refers 231 - to a nonexistent element) 232 - - An index type doesn't match the JSON value (e.g., [`Nth] 279 + - An index type doesn't match the JSON value (e.g., [Nth] 233 280 on an object) *) 234 281 235 - val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 282 + val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 236 283 (** [get_result p json] is like {!get} but returns a result. *) 237 284 238 - val find : t -> Jsont.json -> Jsont.json option 285 + val find : nav t -> Jsont.json -> Jsont.json option 239 286 (** [find p json] is like {!get} but returns [None] instead of 240 287 raising when the pointer doesn't resolve to a value. *) 241 288 ··· 247 294 operations. 248 295 249 296 All mutation functions return a new JSON value with the modification 250 - applied; they do not mutate the input. *) 297 + applied; they do not mutate the input. 298 + 299 + Functions that support the [-] token ({!add}, {!set}) accept any 300 + pointer type ([_ t]). Functions that require an existing element 301 + ({!remove}, {!replace}) only accept {!nav} pointers. *) 251 302 252 - val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json 303 + val set : _ t -> Jsont.json -> value:Jsont.json -> Jsont.json 253 304 (** [set p json ~value] replaces the value at pointer [p] with [value]. 254 305 255 - For [`End] on arrays, appends [value] to the end of the array. 306 + For {!append} pointers, appends [value] to the end of the array. 256 307 257 308 @raise Jsont.Error if the pointer doesn't resolve to an existing 258 - location (except for [`End] on arrays). *) 309 + location (except for {!append} pointers on arrays). *) 259 310 260 - val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json 311 + val add : _ t -> Jsont.json -> value:Jsont.json -> Jsont.json 261 312 (** [add p json ~value] adds [value] at the location specified by [p]. 262 313 263 314 The behavior depends on the target: 264 315 {ul 265 316 {- For objects: If the member exists, it is replaced. If it doesn't 266 317 exist, a new member is added.} 267 - {- For arrays with [`Nth]: Inserts [value] {e before} the 318 + {- For arrays with [Nth]: Inserts [value] {e before} the 268 319 specified index, shifting subsequent elements. The index must be 269 320 valid (0 to length inclusive).} 270 - {- For arrays with [`End]: Appends [value] to the array.}} 321 + {- For {!append} pointers: Appends [value] to the array.}} 271 322 272 323 @raise Jsont.Error if: 273 324 - The parent of the target location doesn't exist 274 - - An array index is out of bounds (except for [`End]) 325 + - An array index is out of bounds (except for {!append} pointers) 275 326 - The parent is not an object or array *) 276 327 277 - val remove : t -> Jsont.json -> Jsont.json 328 + val remove : nav t -> Jsont.json -> Jsont.json 278 329 (** [remove p json] removes the value at pointer [p]. 279 330 280 331 For objects, removes the member. For arrays, removes the element ··· 282 333 283 334 @raise Jsont.Error if: 284 335 - [p] is the root (cannot remove the root) 285 - - The pointer doesn't resolve to an existing value 286 - - The pointer contains [`End] *) 336 + - The pointer doesn't resolve to an existing value *) 287 337 288 - val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json 338 + val replace : nav t -> Jsont.json -> value:Jsont.json -> Jsont.json 289 339 (** [replace p json ~value] replaces the value at pointer [p] with [value]. 290 340 291 341 Unlike {!add}, this requires the target to exist. 292 342 293 - @raise Jsont.Error if: 294 - - The pointer doesn't resolve to an existing value 295 - - The pointer contains [`End] *) 343 + @raise Jsont.Error if the pointer doesn't resolve to an existing value. *) 296 344 297 - val move : from:t -> path:t -> Jsont.json -> Jsont.json 345 + val move : from:nav t -> path:_ t -> Jsont.json -> Jsont.json 298 346 (** [move ~from ~path json] moves the value from [from] to [path]. 299 347 300 348 This is equivalent to {!remove} at [from] followed by {!add} ··· 302 350 303 351 @raise Jsont.Error if: 304 352 - [from] doesn't resolve to a value 305 - - [path] is a proper prefix of [from] (would create a cycle) 306 - - Either pointer contains [`End] *) 353 + - [path] is a proper prefix of [from] (would create a cycle) *) 307 354 308 - val copy : from:t -> path:t -> Jsont.json -> Jsont.json 355 + val copy : from:nav t -> path:_ t -> Jsont.json -> Jsont.json 309 356 (** [copy ~from ~path json] copies the value from [from] to [path]. 310 357 311 358 This is equivalent to {!get} at [from] followed by {!add} 312 359 at [path] with the retrieved value. 313 360 314 - @raise Jsont.Error if: 315 - - [from] doesn't resolve to a value 316 - - Either pointer contains [`End] *) 361 + @raise Jsont.Error if [from] doesn't resolve to a value. *) 317 362 318 - val test : t -> Jsont.json -> expected:Jsont.json -> bool 363 + val test : nav t -> Jsont.json -> expected:Jsont.json -> bool 319 364 (** [test p json ~expected] tests if the value at [p] equals [expected]. 320 365 321 366 Returns [true] if the values are equal according to {!Jsont.Json.equal}, ··· 329 374 These types and functions integrate JSON Pointers with the {!Jsont} 330 375 codec system. *) 331 376 332 - val jsont : t Jsont.t 377 + val jsont : [ `Nav of nav t | `Append of append t ] Jsont.t 333 378 (** [jsont] is a {!Jsont.t} codec for JSON Pointers. 334 379 335 380 On decode, parses a JSON string as a JSON Pointer using {!of_string}. 336 381 On encode, serializes a pointer to a JSON string using {!to_string}. *) 337 382 338 - val jsont_uri_fragment : t Jsont.t 383 + val jsont_nav : nav t Jsont.t 384 + (** [jsont_nav] is a {!Jsont.t} codec for navigation JSON Pointers. 385 + 386 + On decode, parses using {!of_string_nav} (fails on [-]). 387 + On encode, serializes using {!to_string}. *) 388 + 389 + val jsont_uri_fragment : [ `Nav of nav t | `Append of append t ] Jsont.t 339 390 (** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding. 340 391 341 392 On decode, parses using {!of_uri_fragment}. ··· 346 397 These combinators integrate with jsont's query system, allowing 347 398 JSON Pointers to be used with jsont codecs for typed access. *) 348 399 349 - val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t 400 + val path : ?absent:'a -> nav t -> 'a Jsont.t -> 'a Jsont.t 350 401 (** [path p t] decodes the value at pointer [p] using codec [t]. 351 402 352 403 If [absent] is provided and the pointer doesn't resolve, returns ··· 354 405 355 406 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *) 356 407 357 - val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t 408 + val set_path : ?allow_absent:bool -> 'a Jsont.t -> _ t -> 'a -> Jsont.json Jsont.t 358 409 (** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t]. 359 410 360 411 If [allow_absent] is [true] (default [false]), creates missing ··· 362 413 363 414 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *) 364 415 365 - val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t 416 + val update_path : ?absent:'a -> nav t -> 'a Jsont.t -> Jsont.json Jsont.t 366 417 (** [update_path p t] recodes the value at pointer [p] with codec [t]. 367 418 368 419 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *) 369 420 370 - val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t 421 + val delete_path : ?allow_absent:bool -> nav t -> Jsont.json Jsont.t 371 422 (** [delete_path p] removes the value at pointer [p]. 372 423 373 424 If [allow_absent] is [true] (default [false]), does nothing if
+4 -4
test/comprehensive.t
··· 4 4 $ ./test_pointer.exe parse "/foo/bar" 5 5 OK: [Mem:foo, Mem:bar] 6 6 $ ./test_pointer.exe parse "/foo/-" 7 - OK: [Mem:foo, End] 7 + OK: [Mem:foo, /-] 8 8 $ ./test_pointer.exe parse "/foo/1" 9 9 OK: [Mem:foo, Nth:1] 10 10 $ ./test_pointer.exe parse "/foo/~0" ··· 144 144 $ ./test_pointer.exe has 'null' '' 145 145 true 146 146 147 - Has with '-' (end marker - should be false for get, points to nonexistent): 147 + Has with '-' (end marker - now errors because has uses of_string_nav): 148 148 $ ./test_pointer.exe has '["foo"]' '/-' 149 - false 149 + ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer 150 150 $ ./test_pointer.exe has '[]' '/-' 151 - false 151 + ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer
+1 -2
test/eval.t
··· 65 65 66 66 Error: end marker not allowed in get: 67 67 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-" 68 - ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element 69 - File "-": 68 + ERROR: Invalid JSON Pointer: '-' not allowed in navigation pointer 70 69 71 70 Error: navigating through primitive (string): 72 71 $ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0"
+2 -2
test/parse.t
··· 40 40 41 41 End-of-array marker: 42 42 $ ./test_pointer.exe parse "/-" 43 - OK: [End] 43 + OK: [, /-] 44 44 $ ./test_pointer.exe parse "/foo/-" 45 - OK: [Mem:foo, End] 45 + OK: [Mem:foo, /-] 46 46 47 47 Multiple levels: 48 48 $ ./test_pointer.exe parse "/a/b/c"
+48 -25
test/test_pointer.ml
··· 17 17 | Ok s -> s 18 18 | Error e -> failwith e 19 19 20 + (* Helper to get indices from either nav or append pointer *) 21 + let indices_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t 22 + | `Append of Jsont_pointer.append Jsont_pointer.t ]) = 23 + match result with 24 + | `Nav p -> Jsont_pointer.indices p 25 + | `Append p -> Jsont_pointer.indices p 26 + 27 + (* Helper to convert to string from either nav or append pointer *) 28 + let to_string_of_result (result : [ `Nav of Jsont_pointer.nav Jsont_pointer.t 29 + | `Append of Jsont_pointer.append Jsont_pointer.t ]) = 30 + match result with 31 + | `Nav p -> Jsont_pointer.to_string p 32 + | `Append p -> Jsont_pointer.to_string p 33 + 20 34 (* Test: parse pointer and print indices *) 21 35 let test_parse pointer_str = 22 36 try 23 - let p = Jsont_pointer.of_string pointer_str in 24 - let indices = Jsont_pointer.indices p in 37 + let result = Jsont_pointer.of_string pointer_str in 38 + let indices = indices_of_result result in 25 39 let index_strs = List.map (fun idx -> 26 40 match idx with 27 - | `Mem s -> Printf.sprintf "Mem:%s" s 28 - | `Nth n -> Printf.sprintf "Nth:%d" n 29 - | `End -> "End" 41 + | Jsont.Path.Mem (s, _) -> Printf.sprintf "Mem:%s" s 42 + | Jsont.Path.Nth (n, _) -> Printf.sprintf "Nth:%d" n 30 43 ) indices in 31 - Printf.printf "OK: [%s]\n" (String.concat ", " index_strs) 44 + let suffix = match result with `Nav _ -> "" | `Append _ -> ", /-" in 45 + Printf.printf "OK: [%s%s]\n" (String.concat ", " index_strs) suffix 32 46 with Jsont.Error e -> 33 47 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 34 48 35 49 (* Test: roundtrip pointer string *) 36 50 let test_roundtrip pointer_str = 37 51 try 38 - let p = Jsont_pointer.of_string pointer_str in 39 - let s = Jsont_pointer.to_string p in 52 + let result = Jsont_pointer.of_string pointer_str in 53 + let s = to_string_of_result result in 40 54 if s = pointer_str then 41 55 Printf.printf "OK: %s\n" s 42 56 else ··· 48 62 let test_eval json_path pointer_str = 49 63 try 50 64 let json = parse_json (read_file json_path) in 51 - let p = Jsont_pointer.of_string pointer_str in 65 + let p = Jsont_pointer.of_string_nav pointer_str in 52 66 let result = Jsont_pointer.get p json in 53 67 Printf.printf "OK: %s\n" (json_to_string result) 54 68 with ··· 73 87 (* Test: URI fragment roundtrip *) 74 88 let test_uri_fragment pointer_str = 75 89 try 76 - let p = Jsont_pointer.of_string pointer_str in 77 - let frag = Jsont_pointer.to_uri_fragment p in 78 - let p2 = Jsont_pointer.of_uri_fragment frag in 79 - let s2 = Jsont_pointer.to_string p2 in 90 + let result = Jsont_pointer.of_string pointer_str in 91 + let frag = match result with 92 + | `Nav p -> Jsont_pointer.to_uri_fragment p 93 + | `Append p -> Jsont_pointer.to_uri_fragment p 94 + in 95 + let result2 = Jsont_pointer.of_uri_fragment frag in 96 + let s2 = to_string_of_result result2 in 80 97 if s2 = pointer_str then 81 98 Printf.printf "OK: %s -> %s\n" pointer_str frag 82 99 else ··· 88 105 let test_add json_str pointer_str value_str = 89 106 try 90 107 let json = parse_json json_str in 91 - let p = Jsont_pointer.of_string pointer_str in 92 108 let value = parse_json value_str in 93 - let result = Jsont_pointer.add p json ~value in 109 + let result = match Jsont_pointer.of_string pointer_str with 110 + | `Nav p -> Jsont_pointer.add p json ~value 111 + | `Append p -> Jsont_pointer.add p json ~value 112 + in 94 113 Printf.printf "%s\n" (json_to_string result) 95 114 with Jsont.Error e -> 96 115 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) ··· 99 118 let test_remove json_str pointer_str = 100 119 try 101 120 let json = parse_json json_str in 102 - let p = Jsont_pointer.of_string pointer_str in 121 + let p = Jsont_pointer.of_string_nav pointer_str in 103 122 let result = Jsont_pointer.remove p json in 104 123 Printf.printf "%s\n" (json_to_string result) 105 124 with Jsont.Error e -> ··· 109 128 let test_replace json_str pointer_str value_str = 110 129 try 111 130 let json = parse_json json_str in 112 - let p = Jsont_pointer.of_string pointer_str in 131 + let p = Jsont_pointer.of_string_nav pointer_str in 113 132 let value = parse_json value_str in 114 133 let result = Jsont_pointer.replace p json ~value in 115 134 Printf.printf "%s\n" (json_to_string result) ··· 120 139 let test_move json_str from_str path_str = 121 140 try 122 141 let json = parse_json json_str in 123 - let from = Jsont_pointer.of_string from_str in 124 - let path = Jsont_pointer.of_string path_str in 125 - let result = Jsont_pointer.move ~from ~path json in 142 + let from = Jsont_pointer.of_string_nav from_str in 143 + let result = match Jsont_pointer.of_string path_str with 144 + | `Nav path -> Jsont_pointer.move ~from ~path json 145 + | `Append path -> Jsont_pointer.move ~from ~path json 146 + in 126 147 Printf.printf "%s\n" (json_to_string result) 127 148 with Jsont.Error e -> 128 149 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) ··· 131 152 let test_copy json_str from_str path_str = 132 153 try 133 154 let json = parse_json json_str in 134 - let from = Jsont_pointer.of_string from_str in 135 - let path = Jsont_pointer.of_string path_str in 136 - let result = Jsont_pointer.copy ~from ~path json in 155 + let from = Jsont_pointer.of_string_nav from_str in 156 + let result = match Jsont_pointer.of_string path_str with 157 + | `Nav path -> Jsont_pointer.copy ~from ~path json 158 + | `Append path -> Jsont_pointer.copy ~from ~path json 159 + in 137 160 Printf.printf "%s\n" (json_to_string result) 138 161 with Jsont.Error e -> 139 162 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) ··· 142 165 let test_test json_str pointer_str expected_str = 143 166 try 144 167 let json = parse_json json_str in 145 - let p = Jsont_pointer.of_string pointer_str in 168 + let p = Jsont_pointer.of_string_nav pointer_str in 146 169 let expected = parse_json expected_str in 147 170 let result = Jsont_pointer.test p json ~expected in 148 171 Printf.printf "%b\n" result ··· 153 176 let test_has json_str pointer_str = 154 177 try 155 178 let json = parse_json json_str in 156 - let p = Jsont_pointer.of_string pointer_str in 179 + let p = Jsont_pointer.of_string_nav pointer_str in 157 180 let result = Jsont_pointer.find p json in 158 181 Printf.printf "%b\n" (Option.is_some result) 159 182 with Jsont.Error e ->