(*--------------------------------------------------------------------------- Copyright (c) 2025 Anil Madhavapeddy . All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) (** RFC 6901 JSON Pointer implementation for jsont. This module provides {{:https://www.rfc-editor.org/rfc/rfc6901}RFC 6901} JSON Pointer parsing, serialization, and evaluation compatible with {!Jsont} codecs. {1 JSON Pointer vs JSON Path} JSON Pointer (RFC 6901) and {!Jsont.Path} serve similar purposes but have important differences: {ul {- {b JSON Pointer} is a {e string syntax} for addressing JSON values, designed for use in URIs and JSON documents (like JSON Patch). It uses [/] as separator and has escape sequences ([~0], [~1]).} {- {b Jsont.Path} is an {e OCaml data structure} for programmatic navigation, with no string representation defined.}} A key difference is the [-] token: JSON Pointer's [-] refers to the (nonexistent) element {e after} the last array element. This is used for append operations in JSON Patch but is meaningless for retrieval. {!Jsont.Path} has no equivalent concept. This library uses phantom types to enforce this distinction at compile time: pointers that may contain [-] ({!append} pointers) cannot be passed to retrieval functions like {!get}. {2 Example} Given the JSON document: {v { "foo": ["bar", "baz"], "": 0, "a/b": 1, "m~n": 2 } v} The following JSON Pointers evaluate to: {ul {- [""] - the whole document} {- ["/foo"] - the array [\["bar", "baz"\]]} {- ["/foo/0"] - the string ["bar"]} {- ["/"] - the integer [0] (empty string key)} {- ["/a~1b"] - the integer [1] ([~1] escapes [/])} {- ["/m~0n"] - the integer [2] ([~0] escapes [~])} {- ["/foo/-"] - nonexistent; only valid for mutations}} {1:tokens Reference Tokens} JSON Pointer uses escape sequences for special characters in reference tokens. The character [~] must be encoded as [~0] and [/] as [~1]. When unescaping, [~1] is processed before [~0] to correctly handle sequences like [~01] which should become [~1], not [/]. *) (** {1 Reference tokens} Reference tokens are the individual segments between [/] characters in a JSON Pointer string. They require escaping of [~] and [/]. *) module Token : sig type t = string (** The type for unescaped reference tokens. These are plain strings representing object member names or array index strings. *) val escape : t -> string (** [escape s] escapes special characters in [s] for use in a JSON Pointer. Specifically, [~] becomes [~0] and [/] becomes [~1]. *) val unescape : string -> t (** [unescape s] unescapes a JSON Pointer reference token. Specifically, [~1] becomes [/] and [~0] becomes [~]. @raise Jsont.Error.Error if [s] contains invalid escape sequences (a [~] not followed by [0] or [1]). *) end (** {1 Indices} Indices are the individual navigation steps in a JSON Pointer. This library reuses {!Jsont.Path.index} directly - the JSON Pointer specific [-] token is handled separately via phantom types on the pointer type itself. *) type index = Jsont.Path.index (** The type for navigation indices. This is exactly {!Jsont.Path.index}: either [Jsont.Path.Mem (name, meta)] for object member access or [Jsont.Path.Nth (n, meta)] for array index access. *) val mem : ?meta:Jsont.Meta.t -> string -> index (** [mem ?meta s] is [Jsont.Path.Mem (s, meta)]. Convenience constructor for object member access. [meta] defaults to {!Jsont.Meta.none}. *) val nth : ?meta:Jsont.Meta.t -> int -> index (** [nth ?meta n] is [Jsont.Path.Nth (n, meta)]. Convenience constructor for array index access. [meta] defaults to {!Jsont.Meta.none}. *) val pp_index : Format.formatter -> index -> unit (** [pp_index] formats an index in JSON Pointer string notation. *) val equal_index : index -> index -> bool (** [equal_index i1 i2] is [true] iff [i1] and [i2] are the same index. *) val compare_index : index -> index -> int (** [compare_index i1 i2] is a total order on indices. *) (** {1 Pointers} JSON Pointers use phantom types to distinguish between: {ul {- {!nav} pointers that reference existing elements (safe for all operations)} {- {!append} pointers that end with [-] (only valid for {!add} and {!set})}} This ensures at compile time that you cannot accidentally try to retrieve a nonexistent "end of array" position. *) type 'a t (** The type for JSON Pointers. The phantom type ['a] indicates whether the pointer can be used for navigation ([nav]) or only for append operations ([append]). *) type nav (** Phantom type for pointers that reference existing elements. These can be used with all operations including {!get} and {!find}. *) type append (** Phantom type for pointers ending with [-] (the "after last element" position). These can only be used with {!add} and {!set}. *) (** {2 Existential wrapper} The {!type:any} type wraps a pointer of unknown phantom type, allowing ergonomic use with mutation operations like {!set} and {!add} without needing to pattern match on the pointer kind. *) type any = Any : _ t -> any (** Existential wrapper for pointers. Use this when you don't need to distinguish between navigation and append pointers at the type level, such as when using {!set} or {!add} which accept either kind. *) val root : nav t (** [root] is the empty pointer that references the whole document. In string form this is [""]. *) val is_root : _ t -> bool (** [is_root p] is [true] iff [p] is the {!root} pointer. *) val make : index list -> nav t (** [make indices] creates a navigation pointer from a list of indices. The list is ordered from root to target (i.e., the first element is the first step from the root). *) val ( / ) : nav t -> index -> nav t (** [p / idx] appends [idx] to pointer [p]. Operator form of {!append_index}. *) val append_index : nav t -> index -> nav t (** [append_index p idx] appends [idx] to the end of pointer [p]. *) val at_end : nav t -> append t (** [at_end p] creates an append pointer by adding [-] to [p]. The resulting pointer refers to the position after the last element of the array at [p]. Only valid for use with {!add} and {!set}. *) val concat : nav t -> nav t -> nav t (** [concat p1 p2] appends all indices of [p2] to [p1]. *) val parent : nav t -> nav t option (** [parent p] returns the parent pointer of [p], or [None] if [p] is the {!root}. *) val last : nav t -> index option (** [last p] returns the last index of [p], or [None] if [p] is the {!root}. *) val indices : _ t -> index list (** [indices p] returns the indices of [p] from root to target. Note: for append pointers, this returns the indices of the path portion; the [-] (append position) is not represented as an index. *) (** {2:coercion Coercion and inspection} *) val any : _ t -> any (** [any p] wraps a typed pointer in the existential {!type:any} type. Use this when you have a [nav t] or [append t] but need an {!type:any} for use with functions like {!set} or {!add}. *) val is_nav : any -> bool (** [is_nav p] is [true] if [p] is a navigation pointer (not an append pointer ending with [-]). *) val to_nav : any -> nav t option (** [to_nav p] returns [Some nav_p] if [p] is a navigation pointer, or [None] if it's an append pointer. *) val to_nav_exn : any -> nav t (** [to_nav_exn p] returns the navigation pointer if [p] is one. @raise Jsont.Error.Error if [p] is an append pointer. *) (** {2:parsing Parsing} *) val of_string : string -> any (** [of_string s] parses a JSON Pointer from its string representation. Returns an {!type:any} pointer that can be used directly with mutation operations like {!set} and {!add}. For retrieval operations like {!get}, use {!of_string_nav} instead. The string must be either empty (representing the root) or start with [/]. Each segment between [/] characters is unescaped as a reference token. @raise Jsont.Error.Error if [s] has invalid syntax: - Non-empty string not starting with [/] - Invalid escape sequence ([~] not followed by [0] or [1]) - [-] appears in non-final position *) val of_string_kind : string -> [ `Nav of nav t | `Append of append t ] (** [of_string_kind s] parses a JSON Pointer and returns a tagged variant indicating whether it's a navigation or append pointer. Use this when you need to handle navigation and append pointers differently, or when you need a typed pointer for operations that require a specific kind. @raise Jsont.Error.Error if [s] has invalid syntax. *) val of_string_nav : string -> nav t (** [of_string_nav s] parses a JSON Pointer that must not contain [-]. Use this when you need a {!nav} pointer for retrieval operations like {!get} or {!find}. @raise Jsont.Error.Error if [s] has invalid syntax or contains [-]. *) val of_string_result : string -> (any, string) result (** [of_string_result s] is like {!of_string} but returns a result instead of raising. *) val of_uri_fragment : string -> any (** [of_uri_fragment s] parses a JSON Pointer from URI fragment form. This is like {!of_string} but first percent-decodes the string according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}. The leading [#] should {b not} be included in [s]. @raise Jsont.Error.Error on invalid syntax or invalid percent-encoding. *) val of_uri_fragment_nav : string -> nav t (** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires the pointer to not contain [-]. @raise Jsont.Error.Error if invalid or contains [-]. *) val of_uri_fragment_result : string -> (any, string) result (** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns a result instead of raising. *) (** {2:serializing Serializing} *) val to_string : _ t -> string (** [to_string p] serializes [p] to its JSON Pointer string representation. Returns [""] for the root pointer, otherwise [/] followed by escaped reference tokens joined by [/]. Append pointers include the trailing [/-]. *) val to_uri_fragment : _ t -> string (** [to_uri_fragment p] serializes [p] to URI fragment form. This is like {!to_string} but additionally percent-encodes characters that are not allowed in URI fragments per RFC 3986. The leading [#] is {b not} included in the result. *) val pp : Format.formatter -> _ t -> unit (** [pp] formats a pointer using {!to_string}. *) val pp_verbose : Format.formatter -> _ t -> unit (** [pp_verbose] formats a pointer showing its index structure. For example, [/foo/0] is formatted as [[Mem "foo"; Nth 0]]. Append pointers show [/-] at the end. Useful for debugging and understanding pointer structure. *) (** {2:comparison Comparison} *) val equal : _ t -> _ t -> bool (** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices and the same append status. *) val compare : _ t -> _ t -> int (** [compare p1 p2] is a total order on pointers, comparing indices lexicographically. Append pointers sort after nav pointers with the same prefix. *) (** {2:jsont_path Conversion with Jsont.Path} *) val of_path : Jsont.Path.t -> nav t (** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. Always returns a {!nav} pointer since {!Jsont.Path} has no [-] concept. *) val to_path : nav t -> Jsont.Path.t (** [to_path p] converts a navigation pointer to a {!Jsont.Path.t}. *) (** {1 Evaluation} These functions evaluate a JSON Pointer against a {!type:Jsont.json} value to retrieve the referenced value. They only accept {!nav} pointers since {!append} pointers refer to nonexistent positions. *) val get : nav t -> Jsont.json -> Jsont.json (** [get p json] retrieves the value at pointer [p] in [json]. @raise Jsont.Error.Error if: - The pointer references a nonexistent object member - The pointer references an out-of-bounds array index - An index type doesn't match the JSON value (e.g., [Nth] on an object) *) val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result (** [get_result p json] is like {!get} but returns a result. *) val find : nav t -> Jsont.json -> Jsont.json option (** [find p json] is like {!get} but returns [None] instead of raising when the pointer doesn't resolve to a value. *) (** {1 Mutation} These functions modify a {!type:Jsont.json} value at a location specified by a JSON Pointer. They are designed to support {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch} operations. All mutation functions return a new JSON value with the modification applied; they do not mutate the input. Functions that support the [-] token ({!set}, {!add}, {!move}, {!copy}) accept {!type:any} pointers, making them easy to use with {!of_string}. Functions that require an existing element ({!remove}, {!replace}) only accept {!nav} pointers. *) val set : any -> Jsont.json -> value:Jsont.json -> Jsont.json (** [set p json ~value] replaces the value at pointer [p] with [value]. For {!append} pointers, appends [value] to the end of the array. This accepts {!type:any} pointers directly from {!of_string}: {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]} @raise Jsont.Error.Error if the pointer doesn't resolve to an existing location (except for {!append} pointers on arrays). *) val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json (** [add p json ~value] adds [value] at the location specified by [p]. The behavior depends on the target: {ul {- For objects: If the member exists, it is replaced. If it doesn't exist, a new member is added.} {- For arrays with [Nth]: Inserts [value] {e before} the specified index, shifting subsequent elements. The index must be valid (0 to length inclusive).} {- For {!append} pointers: Appends [value] to the array.}} @raise Jsont.Error.Error if: - The parent of the target location doesn't exist - An array index is out of bounds (except for {!append} pointers) - The parent is not an object or array *) val remove : nav t -> Jsont.json -> Jsont.json (** [remove p json] removes the value at pointer [p]. For objects, removes the member. For arrays, removes the element and shifts subsequent elements. @raise Jsont.Error.Error if: - [p] is the root (cannot remove the root) - The pointer doesn't resolve to an existing value *) val replace : nav t -> Jsont.json -> value:Jsont.json -> Jsont.json (** [replace p json ~value] replaces the value at pointer [p] with [value]. Unlike {!add}, this requires the target to exist. @raise Jsont.Error.Error if the pointer doesn't resolve to an existing value. *) val move : from:nav t -> path:any -> Jsont.json -> Jsont.json (** [move ~from ~path json] moves the value from [from] to [path]. This is equivalent to {!remove} at [from] followed by {!add} at [path] with the removed value. @raise Jsont.Error.Error if: - [from] doesn't resolve to a value - [path] is a proper prefix of [from] (would create a cycle) *) val copy : from:nav t -> path:any -> Jsont.json -> Jsont.json (** [copy ~from ~path json] copies the value from [from] to [path]. This is equivalent to {!get} at [from] followed by {!add} at [path] with the retrieved value. @raise Jsont.Error.Error if [from] doesn't resolve to a value. *) val test : nav t -> Jsont.json -> expected:Jsont.json -> bool (** [test p json ~expected] tests if the value at [p] equals [expected]. Returns [true] if the values are equal according to {!Jsont.Json.equal}, [false] otherwise. Also returns [false] (rather than raising) if the pointer doesn't resolve. Note: This implements the semantics of the JSON Patch "test" operation. *) (** {1 Jsont Integration} These types and functions integrate JSON Pointers with the {!Jsont} codec system. *) val jsont : any Jsont.t (** [jsont] is a {!Jsont.t} codec for JSON Pointers. On decode, parses a JSON string as a JSON Pointer using {!of_string}. On encode, serializes a pointer to a JSON string using {!to_string}. *) val jsont_kind : [ `Nav of nav t | `Append of append t ] Jsont.t (** [jsont_kind] is a {!Jsont.t} codec for JSON Pointers that preserves the pointer kind in the type. On decode, parses using {!of_string_kind}. On encode, serializes using {!to_string}. *) val jsont_nav : nav t Jsont.t (** [jsont_nav] is a {!Jsont.t} codec for navigation JSON Pointers. On decode, parses using {!of_string_nav} (fails on [-]). On encode, serializes using {!to_string}. *) val jsont_uri_fragment : any Jsont.t (** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding. On decode, parses using {!of_uri_fragment}. On encode, serializes using {!to_uri_fragment}. *) (** {2:query Query combinators} These combinators integrate with jsont's query system, allowing JSON Pointers to be used with jsont codecs for typed access. *) val path : ?absent:'a -> nav t -> 'a Jsont.t -> 'a Jsont.t (** [path p t] decodes the value at pointer [p] using codec [t]. If [absent] is provided and the pointer doesn't resolve, returns [absent] instead of raising. This is similar to {!Jsont.path} but uses JSON Pointer syntax. *) val set_path : ?allow_absent:bool -> 'a Jsont.t -> any -> 'a -> Jsont.json Jsont.t (** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t]. If [allow_absent] is [true] (default [false]), creates missing intermediate structure as needed. This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *) val update_path : ?absent:'a -> nav t -> 'a Jsont.t -> Jsont.json Jsont.t (** [update_path p t] recodes the value at pointer [p] with codec [t]. This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *) val delete_path : ?allow_absent:bool -> nav t -> Jsont.json Jsont.t (** [delete_path p] removes the value at pointer [p]. If [allow_absent] is [true] (default [false]), does nothing if the pointer doesn't resolve instead of raising. *) (** {1:jmap JMAP Extended Pointers} {{:https://www.rfc-editor.org/rfc/rfc8620#section-3.7}RFC 8620 Section 3.7} extends JSON Pointer with a wildcard token [*] for mapping through arrays. This is used in JMAP result references. The wildcard semantics are: {ul {- When the current value is an array and the token is [*], apply the rest of the pointer to each element, collecting results into a new array.} {- If a mapped result is itself an array, its contents are flattened into the output (i.e., array of arrays becomes a single array).}} Example: Given [{"list": \[{"id": "a"}, {"id": "b"}\]}], the extended pointer [/list/*/id] evaluates to [["a", "b"]]. {b Note}: These extended pointers are {e not} valid RFC 6901 JSON Pointers. They should only be used for JMAP result reference resolution. *) module Jmap : sig (** JMAP extended JSON Pointer with wildcard support. *) type t (** The type for JMAP extended pointers. Unlike standard pointers, these may contain [*] tokens for array mapping. *) val of_string : string -> t (** [of_string s] parses a JMAP extended pointer. The syntax is the same as RFC 6901 JSON Pointer, except [*] is allowed as a reference token for array mapping. @raise Jsont.Error.Error if [s] has invalid syntax. *) val of_string_result : string -> (t, string) result (** [of_string_result s] is like {!of_string} but returns a result. *) val to_string : t -> string (** [to_string p] serializes [p] to string form. *) val pp : Format.formatter -> t -> unit (** [pp] formats a pointer using {!to_string}. *) val eval : t -> Jsont.json -> Jsont.json (** [eval p json] evaluates the extended pointer [p] against [json]. For [*] tokens on arrays, maps through all elements and collects results. Results that are arrays are flattened into the output. @raise Jsont.Error.Error if: - A standard token doesn't resolve (member not found, index out of bounds) - [*] is used on a non-array value - [-] appears in the pointer (not supported in JMAP extended pointers) *) val eval_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result (** [eval_result p json] is like {!eval} but returns a result. *) val find : t -> Jsont.json -> Jsont.json option (** [find p json] is like {!eval} but returns [None] on errors. *) val jsont : t Jsont.t (** [jsont] is a {!Jsont.t} codec for JMAP extended pointers. *) (** {2:combinators Query combinators} These combinators integrate JMAP extended pointers with jsont codecs, enabling typed extraction from JSON using pointer paths. *) val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t (** [path p codec] extracts the value at pointer [p] and decodes it with [codec]. If [absent] is provided and the pointer doesn't resolve, returns [absent]. Otherwise raises on pointer resolution failure. Example: Extract all thread IDs from an Email/get response: {[ let thread_ids = Jmap.path (Jmap.of_string "/list/*/threadId") (Jsont.list Jsont.string) ]} @raise Jsont.Error.Error if the pointer fails to resolve (and no [absent]) or if decoding with [codec] fails. *) val path_list : t -> 'a Jsont.t -> 'a list Jsont.t (** [path_list p codec] extracts the array at pointer [p] and decodes each element with [codec]. This is a convenience for the common JMAP pattern where wildcards produce arrays that need element-wise decoding: {[ (* These are equivalent: *) Jmap.path_list (Jmap.of_string "/list/*/id") Jsont.string Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string) ]} @raise Jsont.Error.Error if pointer resolution fails, the result is not an array, or any element fails to decode. *) end