RFC6901 JSON Pointer implementation in OCaml using jsont
at 080bc4e420a0149d2cc4f301f7d1fc5ccb730615 374 lines 14 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** RFC 6901 JSON Pointer implementation for jsont. 7 8 This module provides {{:https://www.rfc-editor.org/rfc/rfc6901}RFC 6901} 9 JSON Pointer parsing, serialization, and evaluation compatible with 10 {!Jsont} codecs. 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: 14 {v 15 { 16 "foo": ["bar", "baz"], 17 "": 0, 18 "a/b": 1, 19 "m~n": 2 20 } 21 v} 22 23 The following JSON Pointers evaluate to: 24 {ul 25 {- [""] - the whole document} 26 {- ["/foo"] - the array [\["bar", "baz"\]]} 27 {- ["/foo/0"] - the string ["bar"]} 28 {- ["/"] - the integer [0] (empty string key)} 29 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])} 30 {- ["/m~0n"] - the integer [2] ([~0] escapes [~])}} 31 32 {1:tokens Reference Tokens} 33 34 JSON Pointer uses escape sequences for special characters in reference 35 tokens. The character [~] must be encoded as [~0] and [/] as [~1]. 36 When unescaping, [~1] is processed before [~0] to correctly handle 37 sequences like [~01] which should become [~1], not [/]. *) 38 39(** {1 Reference tokens} 40 41 Reference tokens are the individual segments between [/] characters 42 in a JSON Pointer string. They require escaping of [~] and [/]. *) 43module Token : sig 44 45 type t = string 46 (** The type for unescaped reference tokens. These are plain strings 47 representing object member names or array index strings. *) 48 49 val escape : t -> string 50 (** [escape s] escapes special characters in [s] for use in a JSON Pointer. 51 Specifically, [~] becomes [~0] and [/] becomes [~1]. *) 52 53 val unescape : string -> t 54 (** [unescape s] unescapes a JSON Pointer reference token. 55 Specifically, [~1] becomes [/] and [~0] becomes [~]. 56 57 @raise Jsont.Error if [s] contains invalid escape sequences 58 (a [~] not followed by [0] or [1]). *) 59end 60 61(** {1 Indices} 62 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 [-]. *) 66module Index : sig 67 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 ] 83 84 val pp : Format.formatter -> t -> unit 85 (** [pp] formats an index in JSON Pointer string notation. *) 86 87 val equal : t -> t -> bool 88 (** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *) 89 90 val compare : t -> t -> int 91 (** [compare i1 i2] is a total order on indices. *) 92 93 (** {2:jsont_conv Conversion with Jsont.Path} *) 94 95 val of_path_index : Jsont.Path.index -> t 96 (** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *) 97 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}. *) 102end 103 104(** {1 Pointers} *) 105 106type 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. *) 110 111val root : t 112(** [root] is the empty pointer that references the whole document. 113 In string form this is [""]. *) 114 115val is_root : t -> bool 116(** [is_root p] is [true] iff [p] is the {!root} pointer. *) 117 118val make : Index.t list -> t 119(** [make indices] creates a pointer from a list of indices. 120 The list is ordered from root to target (i.e., the first element 121 is the first step from the root). *) 122 123val indices : t -> Index.t list 124(** [indices p] returns the indices of [p] from root to target. *) 125 126val append : t -> Index.t -> t 127(** [append p idx] appends [idx] to the end of pointer [p]. *) 128 129val concat : t -> t -> t 130(** [concat p1 p2] appends all indices of [p2] to [p1]. *) 131 132val parent : t -> t option 133(** [parent p] returns the parent pointer of [p], or [None] if [p] 134 is the {!root}. *) 135 136val last : t -> Index.t option 137(** [last p] returns the last index of [p], or [None] if [p] is 138 the {!root}. *) 139 140(** {2:parsing Parsing} *) 141 142val of_string : string -> t 143(** [of_string s] parses a JSON Pointer from its string representation. 144 145 The string must be either empty (representing the root) or start 146 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]. 150 151 @raise Jsont.Error if [s] has invalid syntax: 152 - Non-empty string not starting with [/] 153 - Invalid escape sequence ([~] not followed by [0] or [1]) 154 - Array index with leading zeros 155 - Array index that overflows [int] *) 156 157val of_string_result : string -> (t, string) result 158(** [of_string_result s] is like {!of_string} but returns a result 159 instead of raising. *) 160 161val of_uri_fragment : string -> t 162(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form. 163 164 This is like {!of_string} but first percent-decodes the string 165 according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}. 166 The leading [#] should {b not} be included in [s]. 167 168 @raise Jsont.Error on invalid syntax or invalid percent-encoding. *) 169 170val of_uri_fragment_result : string -> (t, string) result 171(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns 172 a result instead of raising. *) 173 174(** {2:serializing Serializing} *) 175 176val to_string : t -> string 177(** [to_string p] serializes [p] to its JSON Pointer string representation. 178 179 Returns [""] for the root pointer, otherwise [/] followed by 180 escaped reference tokens joined by [/]. *) 181 182val to_uri_fragment : t -> string 183(** [to_uri_fragment p] serializes [p] to URI fragment form. 184 185 This is like {!to_string} but additionally percent-encodes 186 characters that are not allowed in URI fragments per RFC 3986. 187 The leading [#] is {b not} included in the result. *) 188 189val pp : Format.formatter -> t -> unit 190(** [pp] formats a pointer using {!to_string}. *) 191 192val pp_verbose : Format.formatter -> t -> unit 193(** [pp_verbose] formats a pointer showing its index structure. 194 For example, [/foo/0/-] is formatted as [[`Mem "foo"; `Nth 0; `End]]. 195 Useful for debugging and understanding pointer structure. *) 196 197(** {2:comparison Comparison} *) 198 199val equal : t -> t -> bool 200(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *) 201 202val compare : t -> t -> int 203(** [compare p1 p2] is a total order on pointers, comparing indices 204 lexicographically. *) 205 206(** {2:jsont_path Conversion with Jsont.Path} *) 207 208val of_path : Jsont.Path.t -> t 209(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *) 210 211val 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 215val to_path_exn : t -> Jsont.Path.t 216(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error} 217 if conversion fails. *) 218 219(** {1 Evaluation} 220 221 These functions evaluate a JSON Pointer against a {!Jsont.json} value 222 to retrieve the referenced value. *) 223 224val get : t -> Jsont.json -> Jsont.json 225(** [get p json] retrieves the value at pointer [p] in [json]. 226 227 @raise Jsont.Error if: 228 - The pointer references a nonexistent object member 229 - 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] 233 on an object) *) 234 235val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 236(** [get_result p json] is like {!get} but returns a result. *) 237 238val find : t -> Jsont.json -> Jsont.json option 239(** [find p json] is like {!get} but returns [None] instead of 240 raising when the pointer doesn't resolve to a value. *) 241 242(** {1 Mutation} 243 244 These functions modify a {!Jsont.json} value at a location specified 245 by a JSON Pointer. They are designed to support 246 {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch} 247 operations. 248 249 All mutation functions return a new JSON value with the modification 250 applied; they do not mutate the input. *) 251 252val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json 253(** [set p json ~value] replaces the value at pointer [p] with [value]. 254 255 For [`End] on arrays, appends [value] to the end of the array. 256 257 @raise Jsont.Error if the pointer doesn't resolve to an existing 258 location (except for [`End] on arrays). *) 259 260val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json 261(** [add p json ~value] adds [value] at the location specified by [p]. 262 263 The behavior depends on the target: 264 {ul 265 {- For objects: If the member exists, it is replaced. If it doesn't 266 exist, a new member is added.} 267 {- For arrays with [`Nth]: Inserts [value] {e before} the 268 specified index, shifting subsequent elements. The index must be 269 valid (0 to length inclusive).} 270 {- For arrays with [`End]: Appends [value] to the array.}} 271 272 @raise Jsont.Error if: 273 - The parent of the target location doesn't exist 274 - An array index is out of bounds (except for [`End]) 275 - The parent is not an object or array *) 276 277val remove : t -> Jsont.json -> Jsont.json 278(** [remove p json] removes the value at pointer [p]. 279 280 For objects, removes the member. For arrays, removes the element 281 and shifts subsequent elements. 282 283 @raise Jsont.Error if: 284 - [p] is the root (cannot remove the root) 285 - The pointer doesn't resolve to an existing value 286 - The pointer contains [`End] *) 287 288val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json 289(** [replace p json ~value] replaces the value at pointer [p] with [value]. 290 291 Unlike {!add}, this requires the target to exist. 292 293 @raise Jsont.Error if: 294 - The pointer doesn't resolve to an existing value 295 - The pointer contains [`End] *) 296 297val move : from:t -> path:t -> Jsont.json -> Jsont.json 298(** [move ~from ~path json] moves the value from [from] to [path]. 299 300 This is equivalent to {!remove} at [from] followed by {!add} 301 at [path] with the removed value. 302 303 @raise Jsont.Error if: 304 - [from] doesn't resolve to a value 305 - [path] is a proper prefix of [from] (would create a cycle) 306 - Either pointer contains [`End] *) 307 308val copy : from:t -> path:t -> Jsont.json -> Jsont.json 309(** [copy ~from ~path json] copies the value from [from] to [path]. 310 311 This is equivalent to {!get} at [from] followed by {!add} 312 at [path] with the retrieved value. 313 314 @raise Jsont.Error if: 315 - [from] doesn't resolve to a value 316 - Either pointer contains [`End] *) 317 318val test : t -> Jsont.json -> expected:Jsont.json -> bool 319(** [test p json ~expected] tests if the value at [p] equals [expected]. 320 321 Returns [true] if the values are equal according to {!Jsont.Json.equal}, 322 [false] otherwise. Also returns [false] (rather than raising) if the 323 pointer doesn't resolve. 324 325 Note: This implements the semantics of the JSON Patch "test" operation. *) 326 327(** {1 Jsont Integration} 328 329 These types and functions integrate JSON Pointers with the {!Jsont} 330 codec system. *) 331 332val jsont : t Jsont.t 333(** [jsont] is a {!Jsont.t} codec for JSON Pointers. 334 335 On decode, parses a JSON string as a JSON Pointer using {!of_string}. 336 On encode, serializes a pointer to a JSON string using {!to_string}. *) 337 338val jsont_uri_fragment : t Jsont.t 339(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding. 340 341 On decode, parses using {!of_uri_fragment}. 342 On encode, serializes using {!to_uri_fragment}. *) 343 344(** {2:query Query combinators} 345 346 These combinators integrate with jsont's query system, allowing 347 JSON Pointers to be used with jsont codecs for typed access. *) 348 349val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t 350(** [path p t] decodes the value at pointer [p] using codec [t]. 351 352 If [absent] is provided and the pointer doesn't resolve, returns 353 [absent] instead of raising. 354 355 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *) 356 357val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t 358(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t]. 359 360 If [allow_absent] is [true] (default [false]), creates missing 361 intermediate structure as needed. 362 363 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *) 364 365val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t 366(** [update_path p t] recodes the value at pointer [p] with codec [t]. 367 368 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *) 369 370val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t 371(** [delete_path p] removes the value at pointer [p]. 372 373 If [allow_absent] is [true] (default [false]), does nothing if 374 the pointer doesn't resolve instead of raising. *)