RFC6901 JSON Pointer implementation in OCaml using jsont

mdx

Changed files
+332 -11
doc
+3 -1
doc/dune
··· 1 1 (mdx 2 2 (deps 3 3 %{bin:jsonpp} 4 - rfc6901_example.json)) 4 + rfc6901_example.json 5 + config.json) 6 + (libraries jsont jsont.bytesrw jsont_pointer))
+329 -10
doc/tutorial.md
··· 86 86 87 87 Each reference token becomes an `Index.t` value in the library: 88 88 89 + <!-- $MDX skip --> 89 90 ```ocaml 90 91 type t = 91 92 | Mem of string (* Object member access *) ··· 120 121 121 122 In the library, this is the `Jsont_pointer.get` function: 122 123 124 + <!-- $MDX skip --> 123 125 ```ocaml 124 126 val get : t -> Jsont.json -> Jsont.json 125 127 ``` ··· 154 156 The empty pointer returns the whole document. In OCaml, this is 155 157 `Jsont_pointer.root`: 156 158 159 + <!-- $MDX skip --> 157 160 ```ocaml 158 161 val root : t 159 162 (** The empty pointer that references the whole document. *) ··· 218 221 **Important**: When using the OCaml library programmatically, you don't need 219 222 to worry about escaping. The `Index.Mem` variant holds the literal key name: 220 223 224 + <!-- $MDX skip --> 221 225 ```ocaml 222 226 (* To access the key "a/b", just use the literal string *) 223 227 let pointer = Jsont_pointer.make [Mem "a/b"] ··· 280 284 281 285 The library provides both exception-raising and result-returning variants: 282 286 287 + <!-- $MDX skip --> 283 288 ```ocaml 284 289 val get : t -> Jsont.json -> Jsont.json 285 290 val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result ··· 365 370 366 371 In OCaml: 367 372 373 + <!-- $MDX skip --> 368 374 ```ocaml 369 375 val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json 370 376 ``` ··· 470 476 to think about escaping. The `Index.Mem` variant stores unescaped strings, 471 477 and escaping happens automatically during serialization: 472 478 479 + <!-- $MDX skip --> 473 480 ```ocaml 474 481 (* Create a pointer to key "a/b" - no escaping needed *) 475 482 let p = Jsont_pointer.make [Mem "a/b"] ··· 484 491 485 492 The `Token` module exposes the escaping functions if you need them: 486 493 494 + <!-- $MDX skip --> 487 495 ```ocaml 488 496 module Token : sig 489 497 val escape : string -> string (* "a/b" -> "a~1b" *) ··· 612 620 613 621 The library provides functions for URI fragment encoding: 614 622 623 + <!-- $MDX skip --> 615 624 ```ocaml 616 625 val to_uri_fragment : t -> string 617 626 val of_uri_fragment : string -> t ··· 656 665 657 666 ## Jsont Integration 658 667 659 - The library integrates with the `Jsont` codec system for typed access: 668 + The library integrates with the `Jsont` codec system, allowing you to 669 + combine JSON Pointer navigation with typed decoding. This is powerful 670 + because you can point to a location in a JSON document and decode it 671 + directly to an OCaml type. 672 + 673 + Let's set up our OCaml environment and explore these features: 674 + 675 + ```ocaml 676 + # open Jsont_pointer;; 677 + # let parse_json s = 678 + match Jsont_bytesrw.decode_string Jsont.json s with 679 + | Ok json -> json 680 + | Error e -> failwith e;; 681 + val parse_json : string -> Jsont.json = <fun> 682 + # let json_to_string json = 683 + match Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json with 684 + | Ok s -> s 685 + | Error e -> failwith e;; 686 + val json_to_string : Jsont.json -> string = <fun> 687 + ``` 688 + 689 + ### Working with JSON Values 690 + 691 + Let's create a sample configuration document: 692 + 693 + ```ocaml 694 + # let config_json = parse_json {|{ 695 + "database": { 696 + "host": "localhost", 697 + "port": 5432, 698 + "credentials": {"username": "admin", "password": "secret"} 699 + }, 700 + "features": ["auth", "logging", "metrics"] 701 + }|};; 702 + val config_json : Jsont.json = 703 + Jsont.Object 704 + ([(("database", <abstr>), 705 + Jsont.Object 706 + ([(("host", <abstr>), Jsont.String ("localhost", <abstr>)); 707 + (("port", <abstr>), Jsont.Number (5432., <abstr>)); 708 + (("credentials", <abstr>), 709 + Jsont.Object 710 + ([(("username", <abstr>), Jsont.String ("admin", <abstr>)); 711 + (("password", <abstr>), Jsont.String ("secret", <abstr>))], 712 + <abstr>))], 713 + <abstr>)); 714 + (("features", <abstr>), 715 + Jsont.Array 716 + ([Jsont.String ("auth", <abstr>); Jsont.String ("logging", <abstr>); 717 + Jsont.String ("metrics", <abstr>)], 718 + <abstr>))], 719 + <abstr>) 720 + ``` 721 + 722 + ### Creating and Using Pointers 723 + 724 + Create a pointer and use it to extract values: 725 + 726 + ```ocaml 727 + # let host_ptr = of_string "/database/host";; 728 + val host_ptr : t = <abstr> 729 + # let host_value = get host_ptr config_json;; 730 + val host_value : Jsont.json = Jsont.String ("localhost", <abstr>) 731 + # match host_value with 732 + | Jsont.String (s, _) -> s 733 + | _ -> failwith "expected string";; 734 + - : string = "localhost" 735 + ``` 736 + 737 + ### Building Pointers Programmatically 738 + 739 + Instead of parsing strings, you can build pointers from indices: 740 + 741 + ```ocaml 742 + # let port_ptr = make [Mem "database"; Mem "port"];; 743 + val port_ptr : t = <abstr> 744 + # to_string port_ptr;; 745 + - : string = "/database/port" 746 + # match get port_ptr config_json with 747 + | Jsont.Number (n, _) -> int_of_float n 748 + | _ -> failwith "expected number";; 749 + - : int = 5432 750 + ``` 751 + 752 + For array access, use `Nth`: 753 + 754 + ```ocaml 755 + # let first_feature_ptr = make [Mem "features"; Nth 0];; 756 + val first_feature_ptr : t = <abstr> 757 + # match get first_feature_ptr config_json with 758 + | Jsont.String (s, _) -> s 759 + | _ -> failwith "expected string";; 760 + - : string = "auth" 761 + ``` 762 + 763 + ### Pointer Navigation 764 + 765 + You can build pointers incrementally using `append`: 766 + 767 + ```ocaml 768 + # let db_ptr = of_string "/database";; 769 + val db_ptr : t = <abstr> 770 + # let creds_ptr = append db_ptr (Mem "credentials");; 771 + val creds_ptr : t = <abstr> 772 + # let user_ptr = append creds_ptr (Mem "username");; 773 + val user_ptr : t = <abstr> 774 + # to_string user_ptr;; 775 + - : string = "/database/credentials/username" 776 + # match get user_ptr config_json with 777 + | Jsont.String (s, _) -> s 778 + | _ -> failwith "expected string";; 779 + - : string = "admin" 780 + ``` 781 + 782 + ### Safe Access with `find` 783 + 784 + Use `find` when you're not sure if a path exists: 785 + 786 + ```ocaml 787 + # find (of_string "/database/timeout") config_json;; 788 + - : Jsont.json option = None 789 + # find (of_string "/database/host") config_json |> Option.is_some;; 790 + - : bool = true 791 + ``` 792 + 793 + ### Typed Access with `path` 794 + 795 + The `path` combinator combines pointer navigation with typed decoding: 796 + 797 + ```ocaml 798 + # let db_host = 799 + Jsont.Json.decode 800 + (path (of_string "/database/host") Jsont.string) 801 + config_json 802 + |> Result.get_ok;; 803 + val db_host : string = "localhost" 804 + # let db_port = 805 + Jsont.Json.decode 806 + (path (of_string "/database/port") Jsont.int) 807 + config_json 808 + |> Result.get_ok;; 809 + val db_port : int = 5432 810 + ``` 811 + 812 + Extract a list of strings: 813 + 814 + ```ocaml 815 + # let features = 816 + Jsont.Json.decode 817 + (path (of_string "/features") Jsont.(list string)) 818 + config_json 819 + |> Result.get_ok;; 820 + val features : string list = ["auth"; "logging"; "metrics"] 821 + ``` 822 + 823 + ### Default Values with `~absent` 824 + 825 + Use `~absent` to provide a default when a path doesn't exist: 826 + 827 + ```ocaml 828 + # let timeout = 829 + Jsont.Json.decode 830 + (path ~absent:30 (of_string "/database/timeout") Jsont.int) 831 + config_json 832 + |> Result.get_ok;; 833 + val timeout : int = 30 834 + ``` 835 + 836 + ### Mutation Operations 837 + 838 + The library provides mutation functions for modifying JSON: 839 + 840 + ```ocaml 841 + # let sample = parse_json {|{"name": "Alice", "scores": [85, 92, 78]}|};; 842 + val sample : Jsont.json = 843 + Jsont.Object 844 + ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 845 + (("scores", <abstr>), 846 + Jsont.Array 847 + ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>); 848 + Jsont.Number (78., <abstr>)], 849 + <abstr>))], 850 + <abstr>) 851 + ``` 852 + 853 + **Add** a new field: 854 + 855 + ```ocaml 856 + # let with_email = add (of_string "/email") sample 857 + ~value:(Jsont.Json.string "alice@example.com");; 858 + val with_email : Jsont.json = 859 + Jsont.Object 860 + ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 861 + (("scores", <abstr>), 862 + Jsont.Array 863 + ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>); 864 + Jsont.Number (78., <abstr>)], 865 + <abstr>)); 866 + (("email", <abstr>), Jsont.String ("alice@example.com", <abstr>))], 867 + <abstr>) 868 + # json_to_string with_email;; 869 + - : string = 870 + "{\"name\":\"Alice\",\"scores\":[85,92,78],\"email\":\"alice@example.com\"}" 871 + ``` 872 + 873 + **Add** to an array using `-` (append): 874 + 875 + ```ocaml 876 + # let with_new_score = add (of_string "/scores/-") sample 877 + ~value:(Jsont.Json.number 95.);; 878 + val with_new_score : Jsont.json = 879 + Jsont.Object 880 + ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 881 + (("scores", <abstr>), 882 + Jsont.Array 883 + ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>); 884 + Jsont.Number (78., <abstr>); Jsont.Number (95., <abstr>)], 885 + <abstr>))], 886 + <abstr>) 887 + # json_to_string with_new_score;; 888 + - : string = "{\"name\":\"Alice\",\"scores\":[85,92,78,95]}" 889 + ``` 890 + 891 + **Replace** an existing value: 892 + 893 + ```ocaml 894 + # let renamed = replace (of_string "/name") sample 895 + ~value:(Jsont.Json.string "Bob");; 896 + val renamed : Jsont.json = 897 + Jsont.Object 898 + ([(("name", <abstr>), Jsont.String ("Bob", <abstr>)); 899 + (("scores", <abstr>), 900 + Jsont.Array 901 + ([Jsont.Number (85., <abstr>); Jsont.Number (92., <abstr>); 902 + Jsont.Number (78., <abstr>)], 903 + <abstr>))], 904 + <abstr>) 905 + # json_to_string renamed;; 906 + - : string = "{\"name\":\"Bob\",\"scores\":[85,92,78]}" 907 + ``` 908 + 909 + **Remove** a value: 910 + 911 + ```ocaml 912 + # let without_first = remove (of_string "/scores/0") sample;; 913 + val without_first : Jsont.json = 914 + Jsont.Object 915 + ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 916 + (("scores", <abstr>), 917 + Jsont.Array 918 + ([Jsont.Number (92., <abstr>); Jsont.Number (78., <abstr>)], <abstr>))], 919 + <abstr>) 920 + # json_to_string without_first;; 921 + - : string = "{\"name\":\"Alice\",\"scores\":[92,78]}" 922 + ``` 923 + 924 + ### Nested Path Extraction 925 + 926 + You can extract values from deeply nested structures: 660 927 661 928 ```ocaml 662 - (* Codec for JSON Pointers as JSON strings *) 663 - val jsont : t Jsont.t 929 + # let org_json = parse_json {|{ 930 + "organization": { 931 + "owner": {"name": "Alice", "email": "alice@example.com", "age": 35}, 932 + "members": [{"name": "Bob", "email": "bob@example.com", "age": 28}] 933 + } 934 + }|};; 935 + val org_json : Jsont.json = 936 + Jsont.Object 937 + ([(("organization", <abstr>), 938 + Jsont.Object 939 + ([(("owner", <abstr>), 940 + Jsont.Object 941 + ([(("name", <abstr>), Jsont.String ("Alice", <abstr>)); 942 + (("email", <abstr>), 943 + Jsont.String ("alice@example.com", <abstr>)); 944 + (("age", <abstr>), Jsont.Number (35., <abstr>))], 945 + <abstr>)); 946 + (("members", <abstr>), 947 + Jsont.Array 948 + ([Jsont.Object 949 + ([(("name", <abstr>), Jsont.String ("Bob", <abstr>)); 950 + (("email", <abstr>), 951 + Jsont.String ("bob@example.com", <abstr>)); 952 + (("age", <abstr>), Jsont.Number (28., <abstr>))], 953 + <abstr>)], 954 + <abstr>))], 955 + <abstr>))], 956 + <abstr>) 957 + # Jsont.Json.decode 958 + (path (of_string "/organization/owner/name") Jsont.string) 959 + org_json 960 + |> Result.get_ok;; 961 + - : string = "Alice" 962 + # Jsont.Json.decode 963 + (path (of_string "/organization/members/0/age") Jsont.int) 964 + org_json 965 + |> Result.get_ok;; 966 + - : int = 28 967 + ``` 664 968 665 - (* Query combinators *) 666 - val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t 667 - val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t 668 - val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t 669 - val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t 969 + ### Comparison: Raw vs Typed Access 970 + 971 + **Raw access** requires pattern matching: 972 + 973 + ```ocaml 974 + # let raw_port = 975 + match get (of_string "/database/port") config_json with 976 + | Jsont.Number (f, _) -> int_of_float f 977 + | _ -> failwith "expected number";; 978 + val raw_port : int = 5432 670 979 ``` 671 980 672 - These allow you to use JSON Pointers with typed codecs rather than raw 673 - `Jsont.json` values. 981 + **Typed access** is cleaner and type-safe: 982 + 983 + ```ocaml 984 + # let typed_port = 985 + Jsont.Json.decode 986 + (path (of_string "/database/port") Jsont.int) 987 + config_json 988 + |> Result.get_ok;; 989 + val typed_port : int = 5432 990 + ``` 991 + 992 + The typed approach catches mismatches at decode time with clear errors. 674 993 675 994 ## Summary 676 995