RFC6901 JSON Pointer implementation in OCaml using jsont
at main 22 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 {1 JSON Pointer vs JSON Path} 13 14 JSON Pointer (RFC 6901) and {!Jsont.Path} serve similar purposes but 15 have important differences: 16 17 {ul 18 {- {b JSON Pointer} is a {e string syntax} for addressing JSON values, 19 designed for use in URIs and JSON documents (like JSON Patch). 20 It uses [/] as separator and has escape sequences ([~0], [~1]).} 21 {- {b Jsont.Path} is an {e OCaml data structure} for programmatic 22 navigation, with no string representation defined.}} 23 24 A key difference is the [-] token: JSON Pointer's [-] refers to the 25 (nonexistent) element {e after} the last array element. This is used 26 for append operations in JSON Patch but is meaningless for retrieval. 27 {!Jsont.Path} has no equivalent concept. 28 29 This library uses phantom types to enforce this distinction at compile 30 time: pointers that may contain [-] ({!append} pointers) cannot be 31 passed to retrieval functions like {!get}. 32 33 {2 Example} 34 35 Given the JSON document: 36 {v 37 { 38 "foo": ["bar", "baz"], 39 "": 0, 40 "a/b": 1, 41 "m~n": 2 42 } 43 v} 44 45 The following JSON Pointers evaluate to: 46 {ul 47 {- [""] - the whole document} 48 {- ["/foo"] - the array [\["bar", "baz"\]]} 49 {- ["/foo/0"] - the string ["bar"]} 50 {- ["/"] - the integer [0] (empty string key)} 51 {- ["/a~1b"] - the integer [1] ([~1] escapes [/])} 52 {- ["/m~0n"] - the integer [2] ([~0] escapes [~])} 53 {- ["/foo/-"] - nonexistent; only valid for mutations}} 54 55 {1:tokens Reference Tokens} 56 57 JSON Pointer uses escape sequences for special characters in reference 58 tokens. The character [~] must be encoded as [~0] and [/] as [~1]. 59 When unescaping, [~1] is processed before [~0] to correctly handle 60 sequences like [~01] which should become [~1], not [/]. *) 61 62(** {1 Reference tokens} 63 64 Reference tokens are the individual segments between [/] characters 65 in a JSON Pointer string. They require escaping of [~] and [/]. *) 66module Token : sig 67 68 type t = string 69 (** The type for unescaped reference tokens. These are plain strings 70 representing object member names or array index strings. *) 71 72 val escape : t -> string 73 (** [escape s] escapes special characters in [s] for use in a JSON Pointer. 74 Specifically, [~] becomes [~0] and [/] becomes [~1]. *) 75 76 val unescape : string -> t 77 (** [unescape s] unescapes a JSON Pointer reference token. 78 Specifically, [~1] becomes [/] and [~0] becomes [~]. 79 80 @raise Jsont.Error.Error if [s] contains invalid escape sequences 81 (a [~] not followed by [0] or [1]). *) 82end 83 84(** {1 Indices} 85 86 Indices are the individual navigation steps in a JSON Pointer. 87 This library reuses {!Jsont.Path.index} directly - the JSON Pointer 88 specific [-] token is handled separately via phantom types on the 89 pointer type itself. *) 90 91type index = Jsont.Path.index 92(** The type for navigation indices. This is exactly {!Jsont.Path.index}: 93 either [Jsont.Path.Mem (name, meta)] for object member access or 94 [Jsont.Path.Nth (n, meta)] for array index access. *) 95 96val mem : ?meta:Jsont.Meta.t -> string -> index 97(** [mem ?meta s] is [Jsont.Path.Mem (s, meta)]. 98 Convenience constructor for object member access. 99 [meta] defaults to {!Jsont.Meta.none}. *) 100 101val nth : ?meta:Jsont.Meta.t -> int -> index 102(** [nth ?meta n] is [Jsont.Path.Nth (n, meta)]. 103 Convenience constructor for array index access. 104 [meta] defaults to {!Jsont.Meta.none}. *) 105 106val pp_index : Format.formatter -> index -> unit 107(** [pp_index] formats an index in JSON Pointer string notation. *) 108 109val equal_index : index -> index -> bool 110(** [equal_index i1 i2] is [true] iff [i1] and [i2] are the same index. *) 111 112val compare_index : index -> index -> int 113(** [compare_index i1 i2] is a total order on indices. *) 114 115(** {1 Pointers} 116 117 JSON Pointers use phantom types to distinguish between: 118 {ul 119 {- {!nav} pointers that reference existing elements (safe for all operations)} 120 {- {!append} pointers that end with [-] (only valid for {!add} and {!set})}} 121 122 This ensures at compile time that you cannot accidentally try to 123 retrieve a nonexistent "end of array" position. *) 124 125type 'a t 126(** The type for JSON Pointers. The phantom type ['a] indicates whether 127 the pointer can be used for navigation ([nav]) or only for append 128 operations ([append]). *) 129 130type nav 131(** Phantom type for pointers that reference existing elements. 132 These can be used with all operations including {!get} and {!find}. *) 133 134type append 135(** Phantom type for pointers ending with [-] (the "after last element" 136 position). These can only be used with {!add} and {!set}. *) 137 138(** {2 Existential wrapper} 139 140 The {!type:any} type wraps a pointer of unknown phantom type, allowing 141 ergonomic use with mutation operations like {!set} and {!add} without 142 needing to pattern match on the pointer kind. *) 143 144type any = Any : _ t -> any 145(** Existential wrapper for pointers. Use this when you don't need to 146 distinguish between navigation and append pointers at the type level, 147 such as when using {!set} or {!add} which accept either kind. *) 148 149val root : nav t 150(** [root] is the empty pointer that references the whole document. 151 In string form this is [""]. *) 152 153val is_root : _ t -> bool 154(** [is_root p] is [true] iff [p] is the {!root} pointer. *) 155 156val make : index list -> nav t 157(** [make indices] creates a navigation pointer from a list of indices. 158 The list is ordered from root to target (i.e., the first element 159 is the first step from the root). *) 160 161val ( / ) : nav t -> index -> nav t 162(** [p / idx] appends [idx] to pointer [p]. Operator form of {!append_index}. *) 163 164val append_index : nav t -> index -> nav t 165(** [append_index p idx] appends [idx] to the end of pointer [p]. *) 166 167val at_end : nav t -> append t 168(** [at_end p] creates an append pointer by adding [-] to [p]. 169 The resulting pointer refers to the position after the last element 170 of the array at [p]. Only valid for use with {!add} and {!set}. *) 171 172val concat : nav t -> nav t -> nav t 173(** [concat p1 p2] appends all indices of [p2] to [p1]. *) 174 175val parent : nav t -> nav t option 176(** [parent p] returns the parent pointer of [p], or [None] if [p] 177 is the {!root}. *) 178 179val last : nav t -> index option 180(** [last p] returns the last index of [p], or [None] if [p] is 181 the {!root}. *) 182 183val indices : _ t -> index list 184(** [indices p] returns the indices of [p] from root to target. 185 Note: for append pointers, this returns the indices of the path 186 portion; the [-] (append position) is not represented as an index. *) 187 188(** {2:coercion Coercion and inspection} *) 189 190val any : _ t -> any 191(** [any p] wraps a typed pointer in the existential {!type:any} type. 192 Use this when you have a [nav t] or [append t] but need an {!type:any} 193 for use with functions like {!set} or {!add}. *) 194 195val is_nav : any -> bool 196(** [is_nav p] is [true] if [p] is a navigation pointer (not an append 197 pointer ending with [-]). *) 198 199val to_nav : any -> nav t option 200(** [to_nav p] returns [Some nav_p] if [p] is a navigation pointer, 201 or [None] if it's an append pointer. *) 202 203val to_nav_exn : any -> nav t 204(** [to_nav_exn p] returns the navigation pointer if [p] is one. 205 @raise Jsont.Error.Error if [p] is an append pointer. *) 206 207(** {2:parsing Parsing} *) 208 209val of_string : string -> any 210(** [of_string s] parses a JSON Pointer from its string representation. 211 212 Returns an {!type:any} pointer that can be used directly with mutation 213 operations like {!set} and {!add}. For retrieval operations like 214 {!get}, use {!of_string_nav} instead. 215 216 The string must be either empty (representing the root) or start 217 with [/]. Each segment between [/] characters is unescaped as a 218 reference token. 219 220 @raise Jsont.Error.Error if [s] has invalid syntax: 221 - Non-empty string not starting with [/] 222 - Invalid escape sequence ([~] not followed by [0] or [1]) 223 - [-] appears in non-final position *) 224 225val of_string_kind : string -> [ `Nav of nav t | `Append of append t ] 226(** [of_string_kind s] parses a JSON Pointer and returns a tagged variant 227 indicating whether it's a navigation or append pointer. 228 229 Use this when you need to handle navigation and append pointers 230 differently, or when you need a typed pointer for operations that 231 require a specific kind. 232 233 @raise Jsont.Error.Error if [s] has invalid syntax. *) 234 235val of_string_nav : string -> nav t 236(** [of_string_nav s] parses a JSON Pointer that must not contain [-]. 237 238 Use this when you need a {!nav} pointer for retrieval operations 239 like {!get} or {!find}. 240 241 @raise Jsont.Error.Error if [s] has invalid syntax or contains [-]. *) 242 243val of_string_result : string -> (any, string) result 244(** [of_string_result s] is like {!of_string} but returns a result 245 instead of raising. *) 246 247val of_uri_fragment : string -> any 248(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form. 249 250 This is like {!of_string} but first percent-decodes the string 251 according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}. 252 The leading [#] should {b not} be included in [s]. 253 254 @raise Jsont.Error.Error on invalid syntax or invalid percent-encoding. *) 255 256val of_uri_fragment_nav : string -> nav t 257(** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires 258 the pointer to not contain [-]. 259 260 @raise Jsont.Error.Error if invalid or contains [-]. *) 261 262val of_uri_fragment_result : string -> (any, string) result 263(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns 264 a result instead of raising. *) 265 266(** {2:serializing Serializing} *) 267 268val to_string : _ t -> string 269(** [to_string p] serializes [p] to its JSON Pointer string representation. 270 271 Returns [""] for the root pointer, otherwise [/] followed by 272 escaped reference tokens joined by [/]. Append pointers include 273 the trailing [/-]. *) 274 275val to_uri_fragment : _ t -> string 276(** [to_uri_fragment p] serializes [p] to URI fragment form. 277 278 This is like {!to_string} but additionally percent-encodes 279 characters that are not allowed in URI fragments per RFC 3986. 280 The leading [#] is {b not} included in the result. *) 281 282val pp : Format.formatter -> _ t -> unit 283(** [pp] formats a pointer using {!to_string}. *) 284 285val pp_verbose : Format.formatter -> _ t -> unit 286(** [pp_verbose] formats a pointer showing its index structure. 287 For example, [/foo/0] is formatted as [[Mem "foo"; Nth 0]]. 288 Append pointers show [/-] at the end. 289 Useful for debugging and understanding pointer structure. *) 290 291(** {2:comparison Comparison} *) 292 293val equal : _ t -> _ t -> bool 294(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices 295 and the same append status. *) 296 297val compare : _ t -> _ t -> int 298(** [compare p1 p2] is a total order on pointers, comparing indices 299 lexicographically. Append pointers sort after nav pointers with 300 the same prefix. *) 301 302(** {2:jsont_path Conversion with Jsont.Path} *) 303 304val of_path : Jsont.Path.t -> nav t 305(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. 306 Always returns a {!nav} pointer since {!Jsont.Path} has no [-] concept. *) 307 308val to_path : nav t -> Jsont.Path.t 309(** [to_path p] converts a navigation pointer to a {!Jsont.Path.t}. *) 310 311(** {1 Evaluation} 312 313 These functions evaluate a JSON Pointer against a {!type:Jsont.json} value 314 to retrieve the referenced value. They only accept {!nav} pointers 315 since {!append} pointers refer to nonexistent positions. *) 316 317val get : nav t -> Jsont.json -> Jsont.json 318(** [get p json] retrieves the value at pointer [p] in [json]. 319 320 @raise Jsont.Error.Error if: 321 - The pointer references a nonexistent object member 322 - The pointer references an out-of-bounds array index 323 - An index type doesn't match the JSON value (e.g., [Nth] 324 on an object) *) 325 326val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 327(** [get_result p json] is like {!get} but returns a result. *) 328 329val find : nav t -> Jsont.json -> Jsont.json option 330(** [find p json] is like {!get} but returns [None] instead of 331 raising when the pointer doesn't resolve to a value. *) 332 333(** {1 Mutation} 334 335 These functions modify a {!type:Jsont.json} value at a location specified 336 by a JSON Pointer. They are designed to support 337 {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch} 338 operations. 339 340 All mutation functions return a new JSON value with the modification 341 applied; they do not mutate the input. 342 343 Functions that support the [-] token ({!set}, {!add}, {!move}, {!copy}) 344 accept {!type:any} pointers, making them easy to use with {!of_string}. 345 Functions that require an existing element ({!remove}, {!replace}) 346 only accept {!nav} pointers. *) 347 348val set : any -> Jsont.json -> value:Jsont.json -> Jsont.json 349(** [set p json ~value] replaces the value at pointer [p] with [value]. 350 351 For {!append} pointers, appends [value] to the end of the array. 352 353 This accepts {!type:any} pointers directly from {!of_string}: 354 {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]} 355 356 @raise Jsont.Error.Error if the pointer doesn't resolve to an existing 357 location (except for {!append} pointers on arrays). *) 358 359val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json 360(** [add p json ~value] adds [value] at the location specified by [p]. 361 362 The behavior depends on the target: 363 {ul 364 {- For objects: If the member exists, it is replaced. If it doesn't 365 exist, a new member is added.} 366 {- For arrays with [Nth]: Inserts [value] {e before} the 367 specified index, shifting subsequent elements. The index must be 368 valid (0 to length inclusive).} 369 {- For {!append} pointers: Appends [value] to the array.}} 370 371 @raise Jsont.Error.Error if: 372 - The parent of the target location doesn't exist 373 - An array index is out of bounds (except for {!append} pointers) 374 - The parent is not an object or array *) 375 376val remove : nav t -> Jsont.json -> Jsont.json 377(** [remove p json] removes the value at pointer [p]. 378 379 For objects, removes the member. For arrays, removes the element 380 and shifts subsequent elements. 381 382 @raise Jsont.Error.Error if: 383 - [p] is the root (cannot remove the root) 384 - The pointer doesn't resolve to an existing value *) 385 386val replace : nav t -> Jsont.json -> value:Jsont.json -> Jsont.json 387(** [replace p json ~value] replaces the value at pointer [p] with [value]. 388 389 Unlike {!add}, this requires the target to exist. 390 391 @raise Jsont.Error.Error if the pointer doesn't resolve to an existing value. *) 392 393val move : from:nav t -> path:any -> Jsont.json -> Jsont.json 394(** [move ~from ~path json] moves the value from [from] to [path]. 395 396 This is equivalent to {!remove} at [from] followed by {!add} 397 at [path] with the removed value. 398 399 @raise Jsont.Error.Error if: 400 - [from] doesn't resolve to a value 401 - [path] is a proper prefix of [from] (would create a cycle) *) 402 403val copy : from:nav t -> path:any -> Jsont.json -> Jsont.json 404(** [copy ~from ~path json] copies the value from [from] to [path]. 405 406 This is equivalent to {!get} at [from] followed by {!add} 407 at [path] with the retrieved value. 408 409 @raise Jsont.Error.Error if [from] doesn't resolve to a value. *) 410 411val test : nav t -> Jsont.json -> expected:Jsont.json -> bool 412(** [test p json ~expected] tests if the value at [p] equals [expected]. 413 414 Returns [true] if the values are equal according to {!Jsont.Json.equal}, 415 [false] otherwise. Also returns [false] (rather than raising) if the 416 pointer doesn't resolve. 417 418 Note: This implements the semantics of the JSON Patch "test" operation. *) 419 420(** {1 Jsont Integration} 421 422 These types and functions integrate JSON Pointers with the {!Jsont} 423 codec system. *) 424 425val jsont : any Jsont.t 426(** [jsont] is a {!Jsont.t} codec for JSON Pointers. 427 428 On decode, parses a JSON string as a JSON Pointer using {!of_string}. 429 On encode, serializes a pointer to a JSON string using {!to_string}. *) 430 431val jsont_kind : [ `Nav of nav t | `Append of append t ] Jsont.t 432(** [jsont_kind] is a {!Jsont.t} codec for JSON Pointers that preserves 433 the pointer kind in the type. 434 435 On decode, parses using {!of_string_kind}. 436 On encode, serializes using {!to_string}. *) 437 438val jsont_nav : nav t Jsont.t 439(** [jsont_nav] is a {!Jsont.t} codec for navigation JSON Pointers. 440 441 On decode, parses using {!of_string_nav} (fails on [-]). 442 On encode, serializes using {!to_string}. *) 443 444val jsont_uri_fragment : any Jsont.t 445(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding. 446 447 On decode, parses using {!of_uri_fragment}. 448 On encode, serializes using {!to_uri_fragment}. *) 449 450(** {2:query Query combinators} 451 452 These combinators integrate with jsont's query system, allowing 453 JSON Pointers to be used with jsont codecs for typed access. *) 454 455val path : ?absent:'a -> nav t -> 'a Jsont.t -> 'a Jsont.t 456(** [path p t] decodes the value at pointer [p] using codec [t]. 457 458 If [absent] is provided and the pointer doesn't resolve, returns 459 [absent] instead of raising. 460 461 This is similar to {!Jsont.path} but uses JSON Pointer syntax. *) 462 463val set_path : ?allow_absent:bool -> 'a Jsont.t -> any -> 'a -> Jsont.json Jsont.t 464(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t]. 465 466 If [allow_absent] is [true] (default [false]), creates missing 467 intermediate structure as needed. 468 469 This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *) 470 471val update_path : ?absent:'a -> nav t -> 'a Jsont.t -> Jsont.json Jsont.t 472(** [update_path p t] recodes the value at pointer [p] with codec [t]. 473 474 This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *) 475 476val delete_path : ?allow_absent:bool -> nav t -> Jsont.json Jsont.t 477(** [delete_path p] removes the value at pointer [p]. 478 479 If [allow_absent] is [true] (default [false]), does nothing if 480 the pointer doesn't resolve instead of raising. *) 481 482(** {1:jmap JMAP Extended Pointers} 483 484 {{:https://www.rfc-editor.org/rfc/rfc8620#section-3.7}RFC 8620 Section 3.7} 485 extends JSON Pointer with a wildcard token [*] for mapping through arrays. 486 This is used in JMAP result references. 487 488 The wildcard semantics are: 489 {ul 490 {- When the current value is an array and the token is [*], apply the rest 491 of the pointer to each element, collecting results into a new array.} 492 {- If a mapped result is itself an array, its contents are flattened into 493 the output (i.e., array of arrays becomes a single array).}} 494 495 Example: Given [{"list": \[{"id": "a"}, {"id": "b"}\]}], the extended 496 pointer [/list/*/id] evaluates to [["a", "b"]]. 497 498 {b Note}: These extended pointers are {e not} valid RFC 6901 JSON Pointers. 499 They should only be used for JMAP result reference resolution. *) 500 501module Jmap : sig 502 (** JMAP extended JSON Pointer with wildcard support. *) 503 504 type t 505 (** The type for JMAP extended pointers. Unlike standard pointers, these 506 may contain [*] tokens for array mapping. *) 507 508 val of_string : string -> t 509 (** [of_string s] parses a JMAP extended pointer. 510 511 The syntax is the same as RFC 6901 JSON Pointer, except [*] is allowed 512 as a reference token for array mapping. 513 514 @raise Jsont.Error.Error if [s] has invalid syntax. *) 515 516 val of_string_result : string -> (t, string) result 517 (** [of_string_result s] is like {!of_string} but returns a result. *) 518 519 val to_string : t -> string 520 (** [to_string p] serializes [p] to string form. *) 521 522 val pp : Format.formatter -> t -> unit 523 (** [pp] formats a pointer using {!to_string}. *) 524 525 val eval : t -> Jsont.json -> Jsont.json 526 (** [eval p json] evaluates the extended pointer [p] against [json]. 527 528 For [*] tokens on arrays, maps through all elements and collects results. 529 Results that are arrays are flattened into the output. 530 531 @raise Jsont.Error.Error if: 532 - A standard token doesn't resolve (member not found, index out of bounds) 533 - [*] is used on a non-array value 534 - [-] appears in the pointer (not supported in JMAP extended pointers) *) 535 536 val eval_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result 537 (** [eval_result p json] is like {!eval} but returns a result. *) 538 539 val find : t -> Jsont.json -> Jsont.json option 540 (** [find p json] is like {!eval} but returns [None] on errors. *) 541 542 val jsont : t Jsont.t 543 (** [jsont] is a {!Jsont.t} codec for JMAP extended pointers. *) 544 545 (** {2:combinators Query combinators} 546 547 These combinators integrate JMAP extended pointers with jsont codecs, 548 enabling typed extraction from JSON using pointer paths. *) 549 550 val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t 551 (** [path p codec] extracts the value at pointer [p] and decodes it with [codec]. 552 553 If [absent] is provided and the pointer doesn't resolve, returns [absent]. 554 Otherwise raises on pointer resolution failure. 555 556 Example: Extract all thread IDs from an Email/get response: 557 {[ 558 let thread_ids = 559 Jmap.path 560 (Jmap.of_string "/list/*/threadId") 561 (Jsont.list Jsont.string) 562 ]} 563 564 @raise Jsont.Error.Error if the pointer fails to resolve (and no [absent]) 565 or if decoding with [codec] fails. *) 566 567 val path_list : t -> 'a Jsont.t -> 'a list Jsont.t 568 (** [path_list p codec] extracts the array at pointer [p] and decodes each 569 element with [codec]. 570 571 This is a convenience for the common JMAP pattern where wildcards produce 572 arrays that need element-wise decoding: 573 {[ 574 (* These are equivalent: *) 575 Jmap.path_list (Jmap.of_string "/list/*/id") Jsont.string 576 Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string) 577 ]} 578 579 @raise Jsont.Error.Error if pointer resolution fails, the result is not an array, 580 or any element fails to decode. *) 581end