RFC6901 JSON Pointer implementation in OCaml using jsont

jmap-extension

+11
doc/config.json
··· 1 + { 2 + "database": { 3 + "host": "localhost", 4 + "port": 5432, 5 + "credentials": { 6 + "username": "admin", 7 + "password": "secret" 8 + } 9 + }, 10 + "features": ["auth", "logging", "metrics"] 11 + }
+133
src/jsont_pointer.ml
··· 706 706 remove p json 707 707 in 708 708 Jsont.map Jsont.json ~dec ~enc:(fun j -> j) 709 + 710 + (* JMAP Extended Pointers - RFC 8620 Section 3.7 *) 711 + module Jmap = struct 712 + (* Extended segment type: regular tokens or wildcard *) 713 + type segment = 714 + | Token of string (* Unescaped reference token *) 715 + | Wildcard (* The * token for array mapping *) 716 + 717 + type t = segment list 718 + 719 + let parse_segments s = 720 + if s = "" then [] 721 + else if s.[0] <> '/' then 722 + Jsont.Error.msgf Jsont.Meta.none 723 + "Invalid JMAP Pointer: must be empty or start with '/': %s" s 724 + else 725 + let rest = String.sub s 1 (String.length s - 1) in 726 + let tokens = String.split_on_char '/' rest in 727 + List.map (fun tok -> 728 + if tok = "*" then Wildcard 729 + else if tok = "-" then 730 + Jsont.Error.msgf Jsont.Meta.none 731 + "Invalid JMAP Pointer: '-' not supported in result reference paths" 732 + else Token (Token.unescape tok) 733 + ) tokens 734 + 735 + let of_string s = parse_segments s 736 + 737 + let of_string_result s = 738 + try Ok (of_string s) 739 + with Jsont.Error e -> Error (Jsont.Error.to_string e) 740 + 741 + let segment_to_string = function 742 + | Token s -> Token.escape s 743 + | Wildcard -> "*" 744 + 745 + let to_string p = 746 + if p = [] then "" 747 + else 748 + let b = Buffer.create 64 in 749 + List.iter (fun seg -> 750 + Buffer.add_char b '/'; 751 + Buffer.add_string b (segment_to_string seg) 752 + ) p; 753 + Buffer.contents b 754 + 755 + let pp ppf p = Format.pp_print_string ppf (to_string p) 756 + 757 + (* Evaluation with wildcard support *) 758 + let rec eval_segments segments json = 759 + match segments with 760 + | [] -> json 761 + | Wildcard :: rest -> 762 + (* Wildcard: map through array, flatten results *) 763 + (match json with 764 + | Jsont.Array (elements, meta) -> 765 + let results = List.map (eval_segments rest) elements in 766 + (* Flatten: if a result is an array, inline its contents *) 767 + let flattened = List.concat_map (function 768 + | Jsont.Array (elems, _) -> elems 769 + | other -> [other] 770 + ) results in 771 + Jsont.Array (flattened, meta) 772 + | _ -> 773 + Jsont.Error.msgf (Jsont.Json.meta json) 774 + "JMAP Pointer: '*' can only be used on arrays, got %s" 775 + (json_sort_string json)) 776 + | Token token :: rest -> 777 + (* Standard token: navigate into object or array *) 778 + (match json with 779 + | Jsont.Object (members, _) -> 780 + (match get_member token members with 781 + | Some (_, value) -> eval_segments rest value 782 + | None -> 783 + Jsont.Error.msgf (Jsont.Json.meta json) 784 + "JMAP Pointer: member '%s' not found" token) 785 + | Jsont.Array (elements, _) -> 786 + (match Token.is_valid_array_index token with 787 + | Some n -> 788 + (match get_nth n elements with 789 + | Some value -> eval_segments rest value 790 + | None -> 791 + Jsont.Error.msgf (Jsont.Json.meta json) 792 + "JMAP Pointer: index %d out of bounds (array has %d elements)" 793 + n (List.length elements)) 794 + | None -> 795 + Jsont.Error.msgf (Jsont.Json.meta json) 796 + "JMAP Pointer: invalid array index '%s'" token) 797 + | _ -> 798 + Jsont.Error.msgf (Jsont.Json.meta json) 799 + "JMAP Pointer: cannot index into %s with '%s'" 800 + (json_sort_string json) token) 801 + 802 + let eval p json = eval_segments p json 803 + 804 + let eval_result p json = 805 + try Ok (eval p json) 806 + with Jsont.Error e -> Error e 807 + 808 + let find p json = 809 + try Some (eval p json) 810 + with Jsont.Error _ -> None 811 + 812 + let jsont : t Jsont.t = 813 + let dec _meta s = of_string s in 814 + let enc p = to_string p in 815 + Jsont.Base.string (Jsont.Base.map 816 + ~kind:"JMAP Pointer" 817 + ~doc:"RFC 8620 JMAP extended JSON Pointer" 818 + ~dec ~enc ()) 819 + 820 + (* Query combinators *) 821 + 822 + let path ?absent p codec = 823 + let dec json = 824 + match find p json with 825 + | Some extracted -> 826 + (match Jsont.Json.decode' codec extracted with 827 + | Ok v -> v 828 + | Error e -> raise (Jsont.Error e)) 829 + | None -> 830 + match absent with 831 + | Some v -> v 832 + | None -> 833 + Jsont.Error.msgf Jsont.Meta.none 834 + "JMAP Pointer %s: path not found" (to_string p) 835 + in 836 + Jsont.map Jsont.json ~dec ~enc:(fun _ -> 837 + Jsont.Error.msgf Jsont.Meta.none "Jmap.path: encode not supported") 838 + 839 + let path_list p elem_codec = 840 + path p (Jsont.list elem_codec) 841 + end
+101
src/jsont_pointer.mli
··· 423 423 424 424 If [allow_absent] is [true] (default [false]), does nothing if 425 425 the pointer doesn't resolve instead of raising. *) 426 + 427 + (** {1:jmap JMAP Extended Pointers} 428 + 429 + {{:https://www.rfc-editor.org/rfc/rfc8620#section-3.7}RFC 8620 Section 3.7} 430 + extends JSON Pointer with a wildcard token [*] for mapping through arrays. 431 + This is used in JMAP result references. 432 + 433 + The wildcard semantics are: 434 + {ul 435 + {- When the current value is an array and the token is [*], apply the rest 436 + of the pointer to each element, collecting results into a new array.} 437 + {- If a mapped result is itself an array, its contents are flattened into 438 + the output (i.e., array of arrays becomes a single array).}} 439 + 440 + Example: Given [{"list": \[{"id": "a"}, {"id": "b"}\]}], the extended 441 + pointer [/list/*/id] evaluates to [["a", "b"]]. 442 + 443 + {b Note}: These extended pointers are {e not} valid RFC 6901 JSON Pointers. 444 + They should only be used for JMAP result reference resolution. *) 445 + 446 + module Jmap : sig 447 + (** JMAP extended JSON Pointer with wildcard support. *) 448 + 449 + type t 450 + (** The type for JMAP extended pointers. Unlike standard pointers, these 451 + may contain [*] tokens for array mapping. *) 452 + 453 + val of_string : string -> t 454 + (** [of_string s] parses a JMAP extended pointer. 455 + 456 + The syntax is the same as RFC 6901 JSON Pointer, except [*] is allowed 457 + as a reference token for array mapping. 458 + 459 + @raise Jsont.Error if [s] has invalid syntax. *) 460 + 461 + val of_string_result : string -> (t, string) result 462 + (** [of_string_result s] is like {!of_string} but returns a result. *) 463 + 464 + val to_string : t -> string 465 + (** [to_string p] serializes [p] to string form. *) 466 + 467 + val pp : Format.formatter -> t -> unit 468 + (** [pp] formats a pointer using {!to_string}. *) 469 + 470 + val eval : t -> Jsont.json -> Jsont.json 471 + (** [eval p json] evaluates the extended pointer [p] against [json]. 472 + 473 + For [*] tokens on arrays, maps through all elements and collects results. 474 + Results that are arrays are flattened into the output. 475 + 476 + @raise Jsont.Error if: 477 + - A standard token doesn't resolve (member not found, index out of bounds) 478 + - [*] is used on a non-array value 479 + - [-] appears in the pointer (not supported in JMAP extended pointers) *) 480 + 481 + val eval_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 482 + (** [eval_result p json] is like {!eval} but returns a result. *) 483 + 484 + val find : t -> Jsont.json -> Jsont.json option 485 + (** [find p json] is like {!eval} but returns [None] on errors. *) 486 + 487 + val jsont : t Jsont.t 488 + (** [jsont] is a {!Jsont.t} codec for JMAP extended pointers. *) 489 + 490 + (** {2:combinators Query combinators} 491 + 492 + These combinators integrate JMAP extended pointers with jsont codecs, 493 + enabling typed extraction from JSON using pointer paths. *) 494 + 495 + val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t 496 + (** [path p codec] extracts the value at pointer [p] and decodes it with [codec]. 497 + 498 + If [absent] is provided and the pointer doesn't resolve, returns [absent]. 499 + Otherwise raises on pointer resolution failure. 500 + 501 + Example: Extract all thread IDs from an Email/get response: 502 + {[ 503 + let thread_ids = 504 + Jmap.path 505 + (Jmap.of_string "/list/*/threadId") 506 + (Jsont.list Jsont.string) 507 + ]} 508 + 509 + @raise Jsont.Error if the pointer fails to resolve (and no [absent]) 510 + or if decoding with [codec] fails. *) 511 + 512 + val path_list : t -> 'a Jsont.t -> 'a list Jsont.t 513 + (** [path_list p codec] extracts the array at pointer [p] and decodes each 514 + element with [codec]. 515 + 516 + This is a convenience for the common JMAP pattern where wildcards produce 517 + arrays that need element-wise decoding: 518 + {[ 519 + (* These are equivalent: *) 520 + Jmap.path_list (Jmap.of_string "/list/*/id") Jsont.string 521 + Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string) 522 + ]} 523 + 524 + @raise Jsont.Error if pointer resolution fails, the result is not an array, 525 + or any element fails to decode. *) 526 + end
+4
src/top/dune
··· 1 + (library 2 + (name jsont_pointer_top) 3 + (public_name jsont-pointer.top) 4 + (libraries jsont-pointer jsont.bytesrw compiler-libs.toplevel))
+23
src/top/jsont_pointer_top.ml
··· 1 + (* Toplevel printers for Jsont_pointer.t, Jsont.json, and Jsont.Error.t 2 + 3 + Usage in toplevel: 4 + #require "jsont-pointer.top";; 5 + #install_printer Jsont_pointer_top.nav_printer;; 6 + #install_printer Jsont_pointer_top.append_printer;; 7 + #install_printer Jsont_pointer_top.json_printer;; 8 + #install_printer Jsont_pointer_top.error_printer;; 9 + *) 10 + 11 + let nav_printer ppf (p : Jsont_pointer.nav Jsont_pointer.t) = 12 + Jsont_pointer.pp_verbose ppf p 13 + 14 + let append_printer ppf (p : Jsont_pointer.append Jsont_pointer.t) = 15 + Jsont_pointer.pp_verbose ppf p 16 + 17 + let json_printer ppf (json : Jsont.json) = 18 + match Jsont_bytesrw.encode_string Jsont.json json with 19 + | Ok s -> Format.pp_print_string ppf s 20 + | Error e -> Format.fprintf ppf "<json encoding error: %s>" e 21 + 22 + let error_printer ppf (e : Jsont.Error.t) = 23 + Format.pp_print_string ppf (Jsont.Error.to_string e)
+44
src/top/jsont_pointer_top.mli
··· 1 + (** Toplevel printers for {!Jsont_pointer}, {!Jsont.json}, and {!Jsont.Error.t}. 2 + 3 + To use in the OCaml toplevel or utop: 4 + {[ 5 + #require "jsont-pointer.top";; 6 + #install_printer Jsont_pointer_top.nav_printer;; 7 + #install_printer Jsont_pointer_top.append_printer;; 8 + #install_printer Jsont_pointer_top.json_printer;; 9 + #install_printer Jsont_pointer_top.error_printer;; 10 + ]} 11 + 12 + After installation, JSON Pointers will display their structure: 13 + {[ 14 + # Jsont_pointer.of_string_nav "/foo/0";; 15 + - : Jsont_pointer.nav Jsont_pointer.t = [Mem "foo"; Nth 0] 16 + ]} 17 + 18 + JSON values will display as formatted JSON strings: 19 + {[ 20 + # Jsont_bytesrw.decode_string Jsont.json {|{"foo": [1, 2]}|};; 21 + - : Jsont.json = {"foo": [1, 2]} 22 + ]} 23 + 24 + And errors will display as readable messages: 25 + {[ 26 + # Jsont_pointer.of_string "invalid";; 27 + Exception: Jsont.Error: Invalid JSON Pointer: must be empty or start with '/' 28 + ]} *) 29 + 30 + val nav_printer : Format.formatter -> Jsont_pointer.nav Jsont_pointer.t -> unit 31 + (** [nav_printer] formats a navigation JSON Pointer showing its index structure. 32 + Suitable for use with [#install_printer]. *) 33 + 34 + val append_printer : Format.formatter -> Jsont_pointer.append Jsont_pointer.t -> unit 35 + (** [append_printer] formats an append JSON Pointer showing its index structure. 36 + Suitable for use with [#install_printer]. *) 37 + 38 + val json_printer : Format.formatter -> Jsont.json -> unit 39 + (** [json_printer] formats a {!Jsont.json} value as a human-readable 40 + JSON string. Suitable for use with [#install_printer]. *) 41 + 42 + val error_printer : Format.formatter -> Jsont.Error.t -> unit 43 + (** [error_printer] formats a {!Jsont.Error.t} as a human-readable 44 + error message. Suitable for use with [#install_printer]. *)
+25
test/data/jmap_emails.json
··· 1 + { 2 + "accountId": "A1", 3 + "state": "123456", 4 + "list": [ 5 + { 6 + "id": "msg1023", 7 + "threadId": "trd194", 8 + "from": [{"email": "alice@example.com"}], 9 + "subject": "Hello" 10 + }, 11 + { 12 + "id": "msg223", 13 + "threadId": "trd114", 14 + "from": [{"email": "bob@example.com"}], 15 + "subject": "Re: Project" 16 + }, 17 + { 18 + "id": "msg110", 19 + "threadId": "trd99", 20 + "from": [{"email": "carol@example.com"}], 21 + "subject": "Meeting" 22 + } 23 + ], 24 + "notFound": [] 25 + }
+19
test/data/jmap_threads.json
··· 1 + { 2 + "accountId": "A1", 3 + "state": "123456", 4 + "list": [ 5 + { 6 + "id": "trd194", 7 + "emailIds": ["msg1020", "msg1021", "msg1023"] 8 + }, 9 + { 10 + "id": "trd114", 11 + "emailIds": ["msg201", "msg223"] 12 + }, 13 + { 14 + "id": "trd99", 15 + "emailIds": ["msg42"] 16 + } 17 + ], 18 + "notFound": [] 19 + }
+3 -1
test/dune
··· 10 10 data/nulls.json 11 11 data/booleans.json 12 12 data/unicode.json 13 - data/numeric_keys.json)) 13 + data/numeric_keys.json 14 + data/jmap_threads.json 15 + data/jmap_emails.json))
+175
test/jmap.t
··· 1 + JMAP Extended JSON Pointer Tests (RFC 8620 Section 3.7) 2 + 3 + This tests the wildcard (*) extension to JSON Pointer for JMAP result references. 4 + 5 + Parsing JMAP extended pointers: 6 + 7 + Basic pointers (no wildcards, same as RFC 6901): 8 + $ ./test_pointer.exe jmap-parse "" 9 + OK: (root) 10 + $ ./test_pointer.exe jmap-parse "/foo" 11 + OK: /foo 12 + $ ./test_pointer.exe jmap-parse "/foo/0" 13 + OK: /foo/0 14 + $ ./test_pointer.exe jmap-parse "/a~1b" 15 + OK: /a~1b 16 + 17 + Wildcard pointers: 18 + $ ./test_pointer.exe jmap-parse "/*" 19 + OK: /* 20 + $ ./test_pointer.exe jmap-parse "/list/*" 21 + OK: /list/* 22 + $ ./test_pointer.exe jmap-parse "/list/*/id" 23 + OK: /list/*/id 24 + $ ./test_pointer.exe jmap-parse "/list/*/emailIds" 25 + OK: /list/*/emailIds 26 + $ ./test_pointer.exe jmap-parse "/a/*/b/*/c" 27 + OK: /a/*/b/*/c 28 + 29 + Error: "-" not allowed in JMAP pointers: 30 + $ ./test_pointer.exe jmap-parse "/-" 31 + ERROR: Invalid JMAP Pointer: '-' not supported in result reference paths 32 + $ ./test_pointer.exe jmap-parse "/foo/-" 33 + ERROR: Invalid JMAP Pointer: '-' not supported in result reference paths 34 + 35 + Error: Invalid syntax: 36 + $ ./test_pointer.exe jmap-parse "foo" 37 + ERROR: Invalid JMAP Pointer: must be empty or start with '/': foo 38 + $ ./test_pointer.exe jmap-parse "/~" 39 + ERROR: Invalid JSON Pointer: incomplete escape sequence at end 40 + 41 + Evaluation without wildcards: 42 + 43 + Root pointer: 44 + $ ./test_pointer.exe jmap-eval '{"foo":"bar"}' "" 45 + OK: {"foo":"bar"} 46 + 47 + Simple member access: 48 + $ ./test_pointer.exe jmap-eval '{"foo":"bar"}' "/foo" 49 + OK: "bar" 50 + 51 + Array index: 52 + $ ./test_pointer.exe jmap-eval '{"arr":[1,2,3]}' "/arr/1" 53 + OK: 2 54 + 55 + Nested access: 56 + $ ./test_pointer.exe jmap-eval '{"a":{"b":{"c":"deep"}}}' "/a/b/c" 57 + OK: "deep" 58 + 59 + Evaluation with wildcards (RFC 8620 examples): 60 + 61 + Extract single field from each object in array: 62 + $ ./test_pointer.exe jmap-eval '{"list":[{"id":"a"},{"id":"b"},{"id":"c"}]}' "/list/*/id" 63 + OK: ["a","b","c"] 64 + 65 + Extract threadId from Email/get response (RFC 8620 pattern): 66 + $ ./test_pointer.exe jmap-eval-file data/jmap_emails.json "/list/*/threadId" 67 + OK: ["trd194","trd114","trd99"] 68 + 69 + Extract emailIds from Thread/get response (RFC 8620 pattern) - results flattened: 70 + $ ./test_pointer.exe jmap-eval-file data/jmap_threads.json "/list/*/emailIds" 71 + OK: ["msg1020","msg1021","msg1023","msg201","msg223","msg42"] 72 + 73 + Extract nested field: 74 + $ ./test_pointer.exe jmap-eval '{"items":[{"data":{"value":1}},{"data":{"value":2}}]}' "/items/*/data/value" 75 + OK: [1,2] 76 + 77 + Wildcard on empty array: 78 + $ ./test_pointer.exe jmap-eval '{"list":[]}' "/list/*/id" 79 + OK: [] 80 + 81 + Multiple wildcards (nested arrays): 82 + $ ./test_pointer.exe jmap-eval '{"a":[{"b":[{"c":1},{"c":2}]},{"b":[{"c":3}]}]}' "/a/*/b/*/c" 83 + OK: [1,2,3] 84 + 85 + Wildcard returning non-arrays (no flattening needed): 86 + $ ./test_pointer.exe jmap-eval '{"list":[{"name":"alice"},{"name":"bob"}]}' "/list/*/name" 87 + OK: ["alice","bob"] 88 + 89 + Flattening behavior - arrays of arrays become flat: 90 + $ ./test_pointer.exe jmap-eval '{"items":[{"tags":["a","b"]},{"tags":["c"]},{"tags":["d","e","f"]}]}' "/items/*/tags" 91 + OK: ["a","b","c","d","e","f"] 92 + 93 + Error cases: 94 + 95 + Wildcard on non-array: 96 + $ ./test_pointer.exe jmap-eval '{"obj":{"a":1}}' "/obj/*" 97 + ERROR: JMAP Pointer: '*' can only be used on arrays, got object 98 + File "-": 99 + $ ./test_pointer.exe jmap-eval '"string"' "/*" 100 + ERROR: JMAP Pointer: '*' can only be used on arrays, got string 101 + File "-": 102 + 103 + Member not found: 104 + $ ./test_pointer.exe jmap-eval '{"foo":"bar"}' "/baz" 105 + ERROR: JMAP Pointer: member 'baz' not found 106 + File "-": 107 + 108 + Index out of bounds: 109 + $ ./test_pointer.exe jmap-eval '{"arr":[1,2]}' "/arr/5" 110 + ERROR: JMAP Pointer: index 5 out of bounds (array has 2 elements) 111 + File "-": 112 + 113 + Member not found after wildcard: 114 + $ ./test_pointer.exe jmap-eval '{"list":[{"a":1},{"b":2}]}' "/list/*/a" 115 + ERROR: JMAP Pointer: member 'a' not found 116 + File "-": 117 + 118 + Real JMAP patterns: 119 + 120 + Get IDs from query response: 121 + $ ./test_pointer.exe jmap-eval '{"queryState":"abc","ids":["id1","id2","id3"]}' "/ids" 122 + OK: ["id1","id2","id3"] 123 + 124 + Get created IDs from changes response: 125 + $ ./test_pointer.exe jmap-eval '{"oldState":"a","newState":"b","created":["f1","f4"],"updated":[],"destroyed":[]}' "/created" 126 + OK: ["f1","f4"] 127 + 128 + Complex nested extraction: 129 + $ ./test_pointer.exe jmap-eval '{"results":[{"emails":[{"from":"a@b.com"},{"from":"c@d.com"}]},{"emails":[{"from":"e@f.com"}]}]}' "/results/*/emails/*/from" 130 + OK: ["a@b.com","c@d.com","e@f.com"] 131 + 132 + Typed extraction with Jmap.path combinator: 133 + 134 + Extract string list with wildcard: 135 + $ ./test_pointer.exe jmap-path-strings '{"list":[{"id":"a"},{"id":"b"},{"id":"c"}]}' "/list/*/id" 136 + OK: [a, b, c] 137 + 138 + Extract IDs from JMAP-style response: 139 + $ ./test_pointer.exe jmap-path-strings '{"ids":["id1","id2","id3"]}' "/ids" 140 + OK: [id1, id2, id3] 141 + 142 + Extract threadIds (JMAP Email/get pattern): 143 + $ ./test_pointer.exe jmap-path-strings '{"list":[{"threadId":"t1"},{"threadId":"t2"}]}' "/list/*/threadId" 144 + OK: [t1, t2] 145 + 146 + Extract integers: 147 + $ ./test_pointer.exe jmap-path-ints '{"items":[{"count":10},{"count":20},{"count":30}]}' "/items/*/count" 148 + OK: [10, 20, 30] 149 + 150 + Extract single string value: 151 + $ ./test_pointer.exe jmap-path-single '{"account":{"id":"acc123"}}' "/account/id" 152 + OK: acc123 153 + 154 + Extract with absent default (path exists): 155 + $ ./test_pointer.exe jmap-path-absent '{"name":"alice"}' "/name" "default" 156 + OK: alice 157 + 158 + Extract with absent default (path missing): 159 + $ ./test_pointer.exe jmap-path-absent '{"other":"value"}' "/name" "default" 160 + OK: default 161 + 162 + Nested wildcard extraction: 163 + $ ./test_pointer.exe jmap-path-strings '{"a":[{"b":[{"c":"x"},{"c":"y"}]},{"b":[{"c":"z"}]}]}' "/a/*/b/*/c" 164 + OK: [x, y, z] 165 + 166 + Empty array result: 167 + $ ./test_pointer.exe jmap-path-strings '{"list":[]}' "/list/*/id" 168 + OK: [] 169 + 170 + Type mismatch error (expecting strings, got ints): 171 + $ ./test_pointer.exe jmap-path-strings '{"list":[{"id":1},{"id":2}]}' "/list/*/id" 172 + ERROR: Expected string but found number 173 + File "-": 174 + File "-": at index 0 of 175 + File "-": array<string>
+121
test/test_pointer.ml
··· 182 182 with Jsont.Error e -> 183 183 Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 184 184 185 + (* Test: JMAP extended pointer parse *) 186 + let test_jmap_parse pointer_str = 187 + try 188 + let p = Jsont_pointer.Jmap.of_string pointer_str in 189 + let s = Jsont_pointer.Jmap.to_string p in 190 + if s = "" then Printf.printf "OK: (root)\n" 191 + else Printf.printf "OK: %s\n" s 192 + with Jsont.Error e -> 193 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 194 + 195 + (* Test: JMAP extended pointer evaluation *) 196 + let test_jmap_eval json_str pointer_str = 197 + try 198 + let json = parse_json json_str in 199 + let p = Jsont_pointer.Jmap.of_string pointer_str in 200 + let result = Jsont_pointer.Jmap.eval p json in 201 + Printf.printf "OK: %s\n" (json_to_string result) 202 + with 203 + | Jsont.Error e -> 204 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 205 + | Failure e -> 206 + Printf.printf "FAIL: %s\n" e 207 + 208 + (* Test: JMAP extended pointer evaluation from file *) 209 + let test_jmap_eval_file json_path pointer_str = 210 + try 211 + let json = parse_json (read_file json_path) in 212 + let p = Jsont_pointer.Jmap.of_string pointer_str in 213 + let result = Jsont_pointer.Jmap.eval p json in 214 + Printf.printf "OK: %s\n" (json_to_string result) 215 + with 216 + | Jsont.Error e -> 217 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 218 + | Failure e -> 219 + Printf.printf "FAIL: %s\n" e 220 + 221 + (* Test: JMAP path combinator - extract strings *) 222 + let test_jmap_path_strings json_str pointer_str = 223 + try 224 + let json = parse_json json_str in 225 + let p = Jsont_pointer.Jmap.of_string pointer_str in 226 + let codec = Jsont_pointer.Jmap.path_list p Jsont.string in 227 + let result = match Jsont.Json.decode' codec json with 228 + | Ok v -> v 229 + | Error e -> raise (Jsont.Error e) 230 + in 231 + Printf.printf "OK: [%s]\n" (String.concat ", " result) 232 + with 233 + | Jsont.Error e -> 234 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 235 + | Failure e -> 236 + Printf.printf "FAIL: %s\n" e 237 + 238 + (* Test: JMAP path combinator - extract ints *) 239 + let test_jmap_path_ints json_str pointer_str = 240 + try 241 + let json = parse_json json_str in 242 + let p = Jsont_pointer.Jmap.of_string pointer_str in 243 + let codec = Jsont_pointer.Jmap.path_list p Jsont.int in 244 + let result = match Jsont.Json.decode' codec json with 245 + | Ok v -> v 246 + | Error e -> raise (Jsont.Error e) 247 + in 248 + Printf.printf "OK: [%s]\n" (String.concat ", " (List.map string_of_int result)) 249 + with 250 + | Jsont.Error e -> 251 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 252 + | Failure e -> 253 + Printf.printf "FAIL: %s\n" e 254 + 255 + (* Test: JMAP path combinator - extract single value *) 256 + let test_jmap_path_single json_str pointer_str = 257 + try 258 + let json = parse_json json_str in 259 + let p = Jsont_pointer.Jmap.of_string pointer_str in 260 + let codec = Jsont_pointer.Jmap.path p Jsont.string in 261 + let result = match Jsont.Json.decode' codec json with 262 + | Ok v -> v 263 + | Error e -> raise (Jsont.Error e) 264 + in 265 + Printf.printf "OK: %s\n" result 266 + with 267 + | Jsont.Error e -> 268 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 269 + | Failure e -> 270 + Printf.printf "FAIL: %s\n" e 271 + 272 + (* Test: JMAP path combinator with absent *) 273 + let test_jmap_path_absent json_str pointer_str default = 274 + try 275 + let json = parse_json json_str in 276 + let p = Jsont_pointer.Jmap.of_string pointer_str in 277 + let codec = Jsont_pointer.Jmap.path ~absent:default p Jsont.string in 278 + let result = match Jsont.Json.decode' codec json with 279 + | Ok v -> v 280 + | Error e -> raise (Jsont.Error e) 281 + in 282 + Printf.printf "OK: %s\n" result 283 + with 284 + | Jsont.Error e -> 285 + Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e) 286 + | Failure e -> 287 + Printf.printf "FAIL: %s\n" e 288 + 185 289 let () = 186 290 match Array.to_list Sys.argv with 187 291 | _ :: "parse" :: pointer :: _ -> ··· 210 314 test_test json pointer expected 211 315 | _ :: "has" :: json :: pointer :: _ -> 212 316 test_has json pointer 317 + | _ :: "jmap-parse" :: pointer :: _ -> 318 + test_jmap_parse pointer 319 + | _ :: "jmap-eval" :: json :: pointer :: _ -> 320 + test_jmap_eval json pointer 321 + | _ :: "jmap-eval-file" :: json_path :: pointer :: _ -> 322 + test_jmap_eval_file json_path pointer 323 + | _ :: "jmap-path-strings" :: json :: pointer :: _ -> 324 + test_jmap_path_strings json pointer 325 + | _ :: "jmap-path-ints" :: json :: pointer :: _ -> 326 + test_jmap_path_ints json pointer 327 + | _ :: "jmap-path-single" :: json :: pointer :: _ -> 328 + test_jmap_path_single json pointer 329 + | _ :: "jmap-path-absent" :: json :: pointer :: default :: _ -> 330 + test_jmap_path_absent json pointer default 213 331 | _ -> 214 332 Printf.printf "Usage:\n"; 215 333 Printf.printf " test_pointer parse <pointer>\n"; ··· 225 343 Printf.printf " test_pointer copy <json> <from> <path>\n"; 226 344 Printf.printf " test_pointer test <json> <pointer> <expected>\n"; 227 345 Printf.printf " test_pointer has <json> <pointer>\n"; 346 + Printf.printf " test_pointer jmap-parse <pointer>\n"; 347 + Printf.printf " test_pointer jmap-eval <json> <pointer>\n"; 348 + Printf.printf " test_pointer jmap-eval-file <json-file> <pointer>\n"; 228 349 exit 1