RFC6901 JSON Pointer implementation in OCaml using jsont

escape

Changed files
+54 -148
doc
src
+53 -147
doc/tutorial.md
··· 11 ```ocaml 12 # open Jsont_pointer;; 13 # #install_printer Jsont_pointer_top.printer;; 14 # let parse_json s = 15 match Jsont_bytesrw.decode_string Jsont.json s with 16 | Ok json -> json 17 | Error e -> failwith e;; 18 val parse_json : string -> Jsont.json = <fun> 19 - # let json_to_string json = 20 - match Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json with 21 - | Ok s -> s 22 - | Error e -> failwith e;; 23 - val json_to_string : Jsont.json -> string = <fun> 24 ``` 25 26 ## What is JSON Pointer? ··· 44 ] 45 }|};; 46 val users_json : Jsont.json = 47 - Jsont.Object 48 - ([(("users", <abstr>), 49 - Jsont.Array 50 - ([Jsont.Object 51 - ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 52 - (("age", <abstr>), Jsont.Number (30., <abstr>))], 53 - <abstr>); 54 - Jsont.Object 55 - ([(("name", <abstr>), Jsont.String ("Bob", <abstr>)); 56 - (("age", <abstr>), Jsont.Number (25., <abstr>))], 57 - <abstr>)], 58 - <abstr>))], 59 - <abstr>) 60 ``` 61 62 The JSON Pointer `/users/0/name` refers to the string `"Alice"`: ··· 65 # let ptr = of_string "/users/0/name";; 66 val ptr : t = [`Mem "users"; `Nth 0; `Mem "name"] 67 # get ptr users_json;; 68 - - : Jsont.json = Jsont.String ("Alice", <abstr>) 69 ``` 70 71 In OCaml, this is represented by the `Jsont_pointer.t` type - a sequence ··· 186 "m~n": 8 187 }|};; 188 val rfc_example : Jsont.json = 189 - Jsont.Object 190 - ([(("foo", <abstr>), 191 - Jsont.Array 192 - ([Jsont.String ("bar", <abstr>); Jsont.String ("baz", <abstr>)], 193 - <abstr>)); 194 - (("", <abstr>), Jsont.Number (0., <abstr>)); 195 - (("a/b", <abstr>), Jsont.Number (1., <abstr>)); 196 - (("c%d", <abstr>), Jsont.Number (2., <abstr>)); 197 - (("e^f", <abstr>), Jsont.Number (3., <abstr>)); 198 - (("g|h", <abstr>), Jsont.Number (4., <abstr>)); 199 - (("i\\j", <abstr>), Jsont.Number (5., <abstr>)); 200 - (("k\"l", <abstr>), Jsont.Number (6., <abstr>)); 201 - ((" ", <abstr>), Jsont.Number (7., <abstr>)); 202 - (("m~n", <abstr>), Jsont.Number (8., <abstr>))], 203 - <abstr>) 204 ``` 205 206 This document is carefully constructed to exercise various edge cases! ··· 208 ### The Root Pointer 209 210 ```ocaml 211 - # get root rfc_example |> json_to_string;; 212 - - : string = 213 - "{\"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}" 214 ``` 215 216 The empty pointer (`root`) returns the whole document. ··· 218 ### Object Member Access 219 220 ```ocaml 221 - # get (of_string "/foo") rfc_example |> json_to_string;; 222 - - : string = "[\"bar\",\"baz\"]" 223 ``` 224 225 `/foo` accesses the member named `foo`, which is an array. ··· 227 ### Array Index Access 228 229 ```ocaml 230 - # get (of_string "/foo/0") rfc_example |> json_to_string;; 231 - - : string = "\"bar\"" 232 - # get (of_string "/foo/1") rfc_example |> json_to_string;; 233 - - : string = "\"baz\"" 234 ``` 235 236 `/foo/0` first goes to `foo`, then accesses index 0 of the array. ··· 240 JSON allows empty strings as object keys: 241 242 ```ocaml 243 - # get (of_string "/") rfc_example |> json_to_string;; 244 - - : string = "0" 245 ``` 246 247 The pointer `/` has one token: the empty string. This accesses the member ··· 252 The RFC example includes keys with `/` and `~` characters: 253 254 ```ocaml 255 - # get (of_string "/a~1b") rfc_example |> json_to_string;; 256 - - : string = "1" 257 ``` 258 259 The token `a~1b` refers to the key `a/b`. We'll explain this escaping 260 [below](#escaping-special-characters). 261 262 ```ocaml 263 - # get (of_string "/m~0n") rfc_example |> json_to_string;; 264 - - : string = "8" 265 ``` 266 267 The token `m~0n` refers to the key `m~n`. ··· 274 val slash_ptr : t = [`Mem "a/b"] 275 # to_string slash_ptr;; 276 - : string = "/a~1b" 277 - # get slash_ptr rfc_example |> json_to_string;; 278 - - : string = "1" 279 ``` 280 281 The library escapes it when converting to string. ··· 285 Most characters don't need escaping in JSON Pointer strings: 286 287 ```ocaml 288 - # get (of_string "/c%d") rfc_example |> json_to_string;; 289 - - : string = "2" 290 - # get (of_string "/e^f") rfc_example |> json_to_string;; 291 - - : string = "3" 292 - # get (of_string "/g|h") rfc_example |> json_to_string;; 293 - - : string = "4" 294 - # get (of_string "/ ") rfc_example |> json_to_string;; 295 - - : string = "7" 296 ``` 297 298 Even a space is a valid key character! ··· 404 405 ```ocaml 406 # let obj = parse_json {|{"foo":"bar"}|};; 407 - val obj : Jsont.json = 408 - Jsont.Object ([(("foo", <abstr>), Jsont.String ("bar", <abstr>))], <abstr>) 409 # add (of_string "/baz") obj ~value:(Jsont.Json.string "qux") 410 - |> json_to_string;; 411 - - : string = "{\"foo\":\"bar\",\"baz\":\"qux\"}" 412 ``` 413 414 For arrays, `add` inserts BEFORE the specified index: 415 416 ```ocaml 417 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; 418 - val arr_obj : Jsont.json = 419 - Jsont.Object 420 - ([(("foo", <abstr>), 421 - Jsont.Array 422 - ([Jsont.String ("a", <abstr>); Jsont.String ("b", <abstr>)], <abstr>))], 423 - <abstr>) 424 # add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X") 425 - |> json_to_string;; 426 - - : string = "{\"foo\":[\"a\",\"X\",\"b\"]}" 427 ``` 428 429 This is where the `-` marker shines - it appends to the end: 430 431 ```ocaml 432 # add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c") 433 - |> json_to_string;; 434 - - : string = "{\"foo\":[\"a\",\"b\",\"c\"]}" 435 ``` 436 437 ### Remove ··· 440 441 ```ocaml 442 # let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};; 443 - val two_fields : Jsont.json = 444 - Jsont.Object 445 - ([(("foo", <abstr>), Jsont.String ("bar", <abstr>)); 446 - (("baz", <abstr>), Jsont.String ("qux", <abstr>))], 447 - <abstr>) 448 - # remove (of_string "/baz") two_fields |> json_to_string;; 449 - - : string = "{\"foo\":\"bar\"}" 450 ``` 451 452 For arrays, it removes and shifts: 453 454 ```ocaml 455 # let three_elem = parse_json {|{"foo":["a","b","c"]}|};; 456 - val three_elem : Jsont.json = 457 - Jsont.Object 458 - ([(("foo", <abstr>), 459 - Jsont.Array 460 - ([Jsont.String ("a", <abstr>); Jsont.String ("b", <abstr>); 461 - Jsont.String ("c", <abstr>)], 462 - <abstr>))], 463 - <abstr>) 464 - # remove (of_string "/foo/1") three_elem |> json_to_string;; 465 - - : string = "{\"foo\":[\"a\",\"c\"]}" 466 ``` 467 468 ### Replace ··· 471 472 ```ocaml 473 # replace (of_string "/foo") obj ~value:(Jsont.Json.string "baz") 474 - |> json_to_string;; 475 - - : string = "{\"foo\":\"baz\"}" 476 ``` 477 478 Unlike `add`, `replace` requires the target to already exist. ··· 484 485 ```ocaml 486 # let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; 487 - val nested : Jsont.json = 488 - Jsont.Object 489 - ([(("foo", <abstr>), 490 - Jsont.Object 491 - ([(("bar", <abstr>), Jsont.String ("baz", <abstr>))], <abstr>)); 492 - (("qux", <abstr>), Jsont.Object ([], <abstr>))], 493 - <abstr>) 494 # move ~from:(of_string "/foo/bar") ~path:(of_string "/qux/thud") nested 495 - |> json_to_string;; 496 - - : string = "{\"foo\":{},\"qux\":{\"thud\":\"baz\"}}" 497 ``` 498 499 ### Copy ··· 502 503 ```ocaml 504 # let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; 505 - val to_copy : Jsont.json = 506 - Jsont.Object 507 - ([(("foo", <abstr>), 508 - Jsont.Object 509 - ([(("bar", <abstr>), Jsont.String ("baz", <abstr>))], <abstr>))], 510 - <abstr>) 511 # copy ~from:(of_string "/foo/bar") ~path:(of_string "/foo/qux") to_copy 512 - |> json_to_string;; 513 - - : string = "{\"foo\":{\"bar\":\"baz\",\"qux\":\"baz\"}}" 514 ``` 515 516 ### Test ··· 704 "features": ["auth", "logging", "metrics"] 705 }|};; 706 val config_json : Jsont.json = 707 - Jsont.Object 708 - ([(("database", <abstr>), 709 - Jsont.Object 710 - ([(("host", <abstr>), Jsont.String ("localhost", <abstr>)); 711 - (("port", <abstr>), Jsont.Number (5432., <abstr>)); 712 - (("credentials", <abstr>), 713 - Jsont.Object 714 - ([(("username", <abstr>), Jsont.String ("admin", <abstr>)); 715 - (("password", <abstr>), Jsont.String ("secret", <abstr>))], 716 - <abstr>))], 717 - <abstr>)); 718 - (("features", <abstr>), 719 - Jsont.Array 720 - ([Jsont.String ("auth", <abstr>); Jsont.String ("logging", <abstr>); 721 - Jsont.String ("metrics", <abstr>)], 722 - <abstr>))], 723 - <abstr>) 724 ``` 725 726 ### Typed Access with `path` ··· 778 } 779 }|};; 780 val org_json : Jsont.json = 781 - Jsont.Object 782 - ([(("organization", <abstr>), 783 - Jsont.Object 784 - ([(("owner", <abstr>), 785 - Jsont.Object 786 - ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 787 - (("email", <abstr>), 788 - Jsont.String ("alice@example.com", <abstr>)); 789 - (("age", <abstr>), Jsont.Number (35., <abstr>))], 790 - <abstr>)); 791 - (("members", <abstr>), 792 - Jsont.Array 793 - ([Jsont.Object 794 - ([(("name", <abstr>), Jsont.String ("Bob", <abstr>)); 795 - (("email", <abstr>), 796 - Jsont.String ("bob@example.com", <abstr>)); 797 - (("age", <abstr>), Jsont.Number (28., <abstr>))], 798 - <abstr>)], 799 - <abstr>))], 800 - <abstr>))], 801 - <abstr>) 802 # Jsont.Json.decode 803 (path (of_string "/organization/owner/name") Jsont.string) 804 org_json
··· 11 ```ocaml 12 # open Jsont_pointer;; 13 # #install_printer Jsont_pointer_top.printer;; 14 + # #install_printer Jsont_pointer_top.json_printer;; 15 # let parse_json s = 16 match Jsont_bytesrw.decode_string Jsont.json s with 17 | Ok json -> json 18 | Error e -> failwith e;; 19 val parse_json : string -> Jsont.json = <fun> 20 ``` 21 22 ## What is JSON Pointer? ··· 40 ] 41 }|};; 42 val users_json : Jsont.json = 43 + {"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]} 44 ``` 45 46 The JSON Pointer `/users/0/name` refers to the string `"Alice"`: ··· 49 # let ptr = of_string "/users/0/name";; 50 val ptr : t = [`Mem "users"; `Nth 0; `Mem "name"] 51 # get ptr users_json;; 52 + - : Jsont.json = "Alice" 53 ``` 54 55 In OCaml, this is represented by the `Jsont_pointer.t` type - a sequence ··· 170 "m~n": 8 171 }|};; 172 val rfc_example : Jsont.json = 173 + {"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} 174 ``` 175 176 This document is carefully constructed to exercise various edge cases! ··· 178 ### The Root Pointer 179 180 ```ocaml 181 + # get root rfc_example ;; 182 + - : Jsont.json = 183 + {"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} 184 ``` 185 186 The empty pointer (`root`) returns the whole document. ··· 188 ### Object Member Access 189 190 ```ocaml 191 + # get (of_string "/foo") rfc_example ;; 192 + - : Jsont.json = ["bar","baz"] 193 ``` 194 195 `/foo` accesses the member named `foo`, which is an array. ··· 197 ### Array Index Access 198 199 ```ocaml 200 + # get (of_string "/foo/0") rfc_example ;; 201 + - : Jsont.json = "bar" 202 + # get (of_string "/foo/1") rfc_example ;; 203 + - : Jsont.json = "baz" 204 ``` 205 206 `/foo/0` first goes to `foo`, then accesses index 0 of the array. ··· 210 JSON allows empty strings as object keys: 211 212 ```ocaml 213 + # get (of_string "/") rfc_example ;; 214 + - : Jsont.json = 0 215 ``` 216 217 The pointer `/` has one token: the empty string. This accesses the member ··· 222 The RFC example includes keys with `/` and `~` characters: 223 224 ```ocaml 225 + # get (of_string "/a~1b") rfc_example ;; 226 + - : Jsont.json = 1 227 ``` 228 229 The token `a~1b` refers to the key `a/b`. We'll explain this escaping 230 [below](#escaping-special-characters). 231 232 ```ocaml 233 + # get (of_string "/m~0n") rfc_example ;; 234 + - : Jsont.json = 8 235 ``` 236 237 The token `m~0n` refers to the key `m~n`. ··· 244 val slash_ptr : t = [`Mem "a/b"] 245 # to_string slash_ptr;; 246 - : string = "/a~1b" 247 + # get slash_ptr rfc_example ;; 248 + - : Jsont.json = 1 249 ``` 250 251 The library escapes it when converting to string. ··· 255 Most characters don't need escaping in JSON Pointer strings: 256 257 ```ocaml 258 + # get (of_string "/c%d") rfc_example ;; 259 + - : Jsont.json = 2 260 + # get (of_string "/e^f") rfc_example ;; 261 + - : Jsont.json = 3 262 + # get (of_string "/g|h") rfc_example ;; 263 + - : Jsont.json = 4 264 + # get (of_string "/ ") rfc_example ;; 265 + - : Jsont.json = 7 266 ``` 267 268 Even a space is a valid key character! ··· 374 375 ```ocaml 376 # let obj = parse_json {|{"foo":"bar"}|};; 377 + val obj : Jsont.json = {"foo":"bar"} 378 # add (of_string "/baz") obj ~value:(Jsont.Json.string "qux") 379 + ;; 380 + - : Jsont.json = {"foo":"bar","baz":"qux"} 381 ``` 382 383 For arrays, `add` inserts BEFORE the specified index: 384 385 ```ocaml 386 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; 387 + val arr_obj : Jsont.json = {"foo":["a","b"]} 388 # add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X") 389 + ;; 390 + - : Jsont.json = {"foo":["a","X","b"]} 391 ``` 392 393 This is where the `-` marker shines - it appends to the end: 394 395 ```ocaml 396 # add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c") 397 + ;; 398 + - : Jsont.json = {"foo":["a","b","c"]} 399 ``` 400 401 ### Remove ··· 404 405 ```ocaml 406 # let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};; 407 + val two_fields : Jsont.json = {"foo":"bar","baz":"qux"} 408 + # remove (of_string "/baz") two_fields ;; 409 + - : Jsont.json = {"foo":"bar"} 410 ``` 411 412 For arrays, it removes and shifts: 413 414 ```ocaml 415 # let three_elem = parse_json {|{"foo":["a","b","c"]}|};; 416 + val three_elem : Jsont.json = {"foo":["a","b","c"]} 417 + # remove (of_string "/foo/1") three_elem ;; 418 + - : Jsont.json = {"foo":["a","c"]} 419 ``` 420 421 ### Replace ··· 424 425 ```ocaml 426 # replace (of_string "/foo") obj ~value:(Jsont.Json.string "baz") 427 + ;; 428 + - : Jsont.json = {"foo":"baz"} 429 ``` 430 431 Unlike `add`, `replace` requires the target to already exist. ··· 437 438 ```ocaml 439 # let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; 440 + val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}} 441 # move ~from:(of_string "/foo/bar") ~path:(of_string "/qux/thud") nested 442 + ;; 443 + - : Jsont.json = {"foo":{},"qux":{"thud":"baz"}} 444 ``` 445 446 ### Copy ··· 449 450 ```ocaml 451 # let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; 452 + val to_copy : Jsont.json = {"foo":{"bar":"baz"}} 453 # copy ~from:(of_string "/foo/bar") ~path:(of_string "/foo/qux") to_copy 454 + ;; 455 + - : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}} 456 ``` 457 458 ### Test ··· 646 "features": ["auth", "logging", "metrics"] 647 }|};; 648 val config_json : Jsont.json = 649 + {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]} 650 ``` 651 652 ### Typed Access with `path` ··· 704 } 705 }|};; 706 val org_json : Jsont.json = 707 + {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}} 708 # Jsont.Json.decode 709 (path (of_string "/organization/owner/name") Jsont.string) 710 org_json
+1 -1
src/jsont_pointer.ml
··· 255 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" 261 in
··· 255 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" 261 in