RFC6901 JSON Pointer implementation in OCaml using jsont
at main 28 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(* Token escaping/unescaping per RFC 6901 Section 3-4 *) 7module Token = struct 8 type t = string 9 10 let escape s = 11 let b = Buffer.create (String.length s) in 12 String.iter (function 13 | '~' -> Buffer.add_string b "~0" 14 | '/' -> Buffer.add_string b "~1" 15 | c -> Buffer.add_char b c 16 ) s; 17 Buffer.contents b 18 19 let unescape s = 20 let len = String.length s in 21 let b = Buffer.create len in 22 let rec loop i = 23 if i >= len then Buffer.contents b 24 else match s.[i] with 25 | '~' when i + 1 >= len -> 26 Jsont.Error.msgf Jsont.Meta.none 27 "Invalid JSON Pointer: incomplete escape sequence at end" 28 | '~' -> 29 (match s.[i + 1] with 30 | '0' -> Buffer.add_char b '~'; loop (i + 2) 31 | '1' -> Buffer.add_char b '/'; loop (i + 2) 32 | c -> 33 Jsont.Error.msgf Jsont.Meta.none 34 "Invalid JSON Pointer: invalid escape sequence ~%c" c) 35 | c -> Buffer.add_char b c; loop (i + 1) 36 in 37 loop 0 38 39 (* Check if a token is a valid array index per RFC 6901 ABNF: 40 array-index = %x30 / ( %x31-39 *(%x30-39) ) 41 i.e., "0" or a non-zero digit followed by any digits *) 42 let is_valid_array_index s = 43 let len = String.length s in 44 let is_digit c = c >= '0' && c <= '9' in 45 if len = 0 then None 46 else if len = 1 && s.[0] = '0' then Some 0 47 else if s.[0] >= '1' && s.[0] <= '9' then 48 let rec all_digits i = 49 if i >= len then true 50 else if is_digit s.[i] then all_digits (i + 1) 51 else false 52 in 53 if all_digits 1 then int_of_string_opt s else None 54 else None 55end 56 57(* Index type - directly reuses Jsont.Path.index *) 58type index = Jsont.Path.index 59 60(* Convenience constructors *) 61let mem ?(meta = Jsont.Meta.none) s : index = Jsont.Path.Mem (s, meta) 62let nth ?(meta = Jsont.Meta.none) n : index = Jsont.Path.Nth (n, meta) 63 64let pp_index ppf = function 65 | Jsont.Path.Mem (s, _) -> Format.fprintf ppf "/%s" (Token.escape s) 66 | Jsont.Path.Nth (n, _) -> Format.fprintf ppf "/%d" n 67 68let equal_index i1 i2 = match i1, i2 with 69 | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.equal s1 s2 70 | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.equal n1 n2 71 | _ -> false 72 73let compare_index i1 i2 = match i1, i2 with 74 | Jsont.Path.Mem (s1, _), Jsont.Path.Mem (s2, _) -> String.compare s1 s2 75 | Jsont.Path.Mem _, Jsont.Path.Nth _ -> -1 76 | Jsont.Path.Nth _, Jsont.Path.Mem _ -> 1 77 | Jsont.Path.Nth (n1, _), Jsont.Path.Nth (n2, _) -> Int.compare n1 n2 78 79(* Internal representation: raw unescaped tokens *) 80module Segment = struct 81 type t = string (* Unescaped reference token *) 82 83 let of_escaped_string s = Token.unescape s 84 85 let to_escaped_string s = Token.escape s 86 87 let of_index = function 88 | Jsont.Path.Mem (s, _) -> s 89 | Jsont.Path.Nth (n, _) -> string_of_int n 90 91 let to_index s : index = 92 match Token.is_valid_array_index s with 93 | Some n -> nth n 94 | None -> mem s 95end 96 97(* Phantom types *) 98type nav 99type append 100 101(* Pointer type with phantom type parameter *) 102type _ t = { 103 segments : Segment.t list; 104 is_append : bool; (* true if ends with "-" *) 105} 106 107(* Existential wrapper *) 108type any = Any : _ t -> any 109 110let root = { segments = []; is_append = false } 111 112let is_root p = p.segments = [] && not p.is_append 113 114let make indices = 115 { segments = List.map Segment.of_index indices; is_append = false } 116 117let ( / ) p idx = 118 { segments = p.segments @ [Segment.of_index idx]; is_append = false } 119 120let append_index = ( / ) 121 122let at_end p = 123 { segments = p.segments; is_append = true } 124 125let concat p1 p2 = 126 { segments = p1.segments @ p2.segments; is_append = false } 127 128let parent p = 129 match List.rev p.segments with 130 | [] -> None 131 | _ :: rest -> Some { segments = List.rev rest; is_append = false } 132 133let last p = 134 match List.rev p.segments with 135 | [] -> None 136 | seg :: _ -> Some (Segment.to_index seg) 137 138let indices (type a) (p : a t) = List.map Segment.to_index p.segments 139 140(* Coercion and inspection *) 141 142let any (type a) (p : a t) : any = Any p 143 144let is_nav (Any p) = not p.is_append 145 146let to_nav (Any p) = 147 if p.is_append then None 148 else Some { segments = p.segments; is_append = false } 149 150let to_nav_exn (Any p) = 151 if p.is_append then 152 Jsont.Error.msgf Jsont.Meta.none 153 "JSON Pointer: cannot convert append pointer to nav pointer" 154 else 155 { segments = p.segments; is_append = false } 156 157(* Parsing *) 158 159let parse_segments s = 160 if s = "" then [] 161 else if s.[0] <> '/' then 162 Jsont.Error.msgf Jsont.Meta.none 163 "Invalid JSON Pointer: must be empty or start with '/': %s" s 164 else 165 let rest = String.sub s 1 (String.length s - 1) in 166 let tokens = String.split_on_char '/' rest in 167 List.map Segment.of_escaped_string tokens 168 169let of_string_kind s : [ `Nav of nav t | `Append of append t ] = 170 let segments = parse_segments s in 171 (* Check if ends with "-" *) 172 match List.rev segments with 173 | "-" :: rest -> 174 (* Validate that "-" only appears at the end *) 175 if List.exists (( = ) "-") rest then 176 Jsont.Error.msgf Jsont.Meta.none 177 "Invalid JSON Pointer: '-' can only appear at the end"; 178 `Append { segments = List.rev rest; is_append = true } 179 | _ -> 180 (* Validate no "-" anywhere *) 181 if List.exists (( = ) "-") segments then 182 Jsont.Error.msgf Jsont.Meta.none 183 "Invalid JSON Pointer: '-' can only appear at the end"; 184 `Nav { segments; is_append = false } 185 186let of_string s : any = 187 match of_string_kind s with 188 | `Nav p -> Any p 189 | `Append p -> Any p 190 191let of_string_nav s : nav t = 192 match of_string_kind s with 193 | `Nav p -> p 194 | `Append _ -> 195 Jsont.Error.msgf Jsont.Meta.none 196 "Invalid JSON Pointer: '-' not allowed in navigation pointer" 197 198let of_string_result s = 199 try Ok (of_string s) 200 with Jsont.Error e -> Error (Jsont.Error.to_string e) 201 202(* URI fragment percent-decoding *) 203let hex_value c = 204 if c >= '0' && c <= '9' then Char.code c - Char.code '0' 205 else if c >= 'A' && c <= 'F' then Char.code c - Char.code 'A' + 10 206 else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10 207 else -1 208 209let percent_decode s = 210 let len = String.length s in 211 let b = Buffer.create len in 212 let rec loop i = 213 if i >= len then Buffer.contents b 214 else match s.[i] with 215 | '%' when i + 2 < len -> 216 let h1 = hex_value s.[i + 1] in 217 let h2 = hex_value s.[i + 2] in 218 if h1 >= 0 && h2 >= 0 then begin 219 Buffer.add_char b (Char.chr ((h1 lsl 4) lor h2)); 220 loop (i + 3) 221 end else 222 Jsont.Error.msgf Jsont.Meta.none 223 "Invalid percent-encoding at position %d" i 224 | '%' -> 225 Jsont.Error.msgf Jsont.Meta.none 226 "Incomplete percent-encoding at position %d" i 227 | c -> Buffer.add_char b c; loop (i + 1) 228 in 229 loop 0 230 231let of_uri_fragment s : any = of_string (percent_decode s) 232 233let of_uri_fragment_nav s = of_string_nav (percent_decode s) 234 235let of_uri_fragment_result s : (any, string) result = 236 try Ok (of_uri_fragment s) 237 with Jsont.Error e -> Error (Jsont.Error.to_string e) 238 239(* Serialization *) 240 241let to_string (type a) (p : a t) = 242 let base = 243 if p.segments = [] then "" 244 else 245 let b = Buffer.create 64 in 246 List.iter (fun seg -> 247 Buffer.add_char b '/'; 248 Buffer.add_string b (Segment.to_escaped_string seg) 249 ) p.segments; 250 Buffer.contents b 251 in 252 if p.is_append then base ^ "/-" else base 253 254(* URI fragment percent-encoding *) 255let needs_percent_encoding c = 256 not ( 257 (c >= 'A' && c <= 'Z') || 258 (c >= 'a' && c <= 'z') || 259 (c >= '0' && c <= '9') || 260 c = '-' || c = '.' || c = '_' || c = '~' || 261 c = '!' || c = '$' || c = '&' || c = '\'' || 262 c = '(' || c = ')' || c = '*' || c = '+' || 263 c = ',' || c = ';' || c = '=' || 264 c = ':' || c = '@' || c = '/' || c = '?' 265 ) 266 267let hex_char n = 268 if n < 10 then Char.chr (Char.code '0' + n) 269 else Char.chr (Char.code 'A' + n - 10) 270 271let percent_encode s = 272 let b = Buffer.create (String.length s * 3) in 273 String.iter (fun c -> 274 if needs_percent_encoding c then begin 275 let code = Char.code c in 276 Buffer.add_char b '%'; 277 Buffer.add_char b (hex_char (code lsr 4)); 278 Buffer.add_char b (hex_char (code land 0xF)) 279 end else 280 Buffer.add_char b c 281 ) s; 282 Buffer.contents b 283 284let to_uri_fragment p = percent_encode (to_string p) 285 286let pp ppf p = Format.pp_print_string ppf (to_string p) 287 288let pp_verbose (type a) ppf (p : a t) = 289 let pp_idx ppf seg = 290 match Token.is_valid_array_index seg with 291 | Some n -> Format.fprintf ppf "Nth %d" n 292 | None -> Format.fprintf ppf {|Mem "%s"|} seg 293 in 294 Format.fprintf ppf "[%a]%s" 295 (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp_idx) 296 p.segments 297 (if p.is_append then " /-" else "") 298 299(* Comparison *) 300 301let equal (type a b) (p1 : a t) (p2 : b t) = 302 List.equal String.equal p1.segments p2.segments && 303 p1.is_append = p2.is_append 304 305let compare (type a b) (p1 : a t) (p2 : b t) = 306 match List.compare String.compare p1.segments p2.segments with 307 | 0 -> Bool.compare p1.is_append p2.is_append 308 | n -> n 309 310(* Path conversion *) 311 312let of_path (p : Jsont.Path.t) : nav t = 313 let segments = List.rev_map Segment.of_index (Jsont.Path.rev_indices p) in 314 { segments; is_append = false } 315 316let to_path (p : nav t) : Jsont.Path.t = 317 List.fold_left (fun acc seg -> 318 match Token.is_valid_array_index seg with 319 | Some n -> Jsont.Path.nth n acc 320 | None -> Jsont.Path.mem seg acc 321 ) Jsont.Path.root p.segments 322 323(* Evaluation helpers *) 324 325let json_sort_string (j : Jsont.json) = 326 match j with 327 | Null _ -> "null" 328 | Bool _ -> "boolean" 329 | Number _ -> "number" 330 | String _ -> "string" 331 | Array _ -> "array" 332 | Object _ -> "object" 333 334let get_member name (obj : Jsont.object') = 335 List.find_opt (fun ((n, _), _) -> String.equal n name) obj 336 337let get_nth n (arr : Jsont.json list) = 338 if n < 0 || n >= List.length arr then None 339 else Some (List.nth arr n) 340 341(* Evaluation - only for nav pointers *) 342 343let rec eval_get segments json = 344 match segments with 345 | [] -> json 346 | token :: rest -> 347 (match json with 348 | Jsont.Object (members, _) -> 349 (match get_member token members with 350 | Some (_, value) -> eval_get rest value 351 | None -> 352 Jsont.Error.msgf (Jsont.Json.meta json) 353 "JSON Pointer: member '%s' not found" token) 354 | Jsont.Array (elements, _) -> 355 (match Token.is_valid_array_index token with 356 | Some n -> 357 (match get_nth n elements with 358 | Some value -> eval_get rest value 359 | None -> 360 Jsont.Error.msgf (Jsont.Json.meta json) 361 "JSON Pointer: index %d out of bounds (array has %d elements)" 362 n (List.length elements)) 363 | None -> 364 Jsont.Error.msgf (Jsont.Json.meta json) 365 "JSON Pointer: invalid array index '%s'" token) 366 | _ -> 367 Jsont.Error.msgf (Jsont.Json.meta json) 368 "JSON Pointer: cannot index into %s with '%s'" 369 (json_sort_string json) token) 370 371let get (p : nav t) json = eval_get p.segments json 372 373let get_result p json = 374 try Ok (get p json) 375 with Jsont.Error e -> Error e 376 377let find p json = 378 try Some (get p json) 379 with Jsont.Error _ -> None 380 381(* Mutation helpers *) 382 383let set_member name value (obj : Jsont.object') : Jsont.object' = 384 let rec loop found acc = function 385 | [] -> 386 if found then List.rev acc 387 else List.rev_append acc [((name, Jsont.Meta.none), value)] 388 | ((n, m), _) :: rest when String.equal n name -> 389 loop true (((n, m), value) :: acc) rest 390 | mem :: rest -> 391 loop found (mem :: acc) rest 392 in 393 loop false [] obj 394 395let remove_member name (obj : Jsont.object') : Jsont.object' = 396 List.filter (fun ((n, _), _) -> not (String.equal n name)) obj 397 398let insert_at n value lst = 399 let rec loop i acc = function 400 | rest when i = n -> List.rev_append acc (value :: rest) 401 | [] -> List.rev acc 402 | h :: t -> loop (i + 1) (h :: acc) t 403 in 404 loop 0 [] lst 405 406let remove_at n lst = 407 List.filteri (fun i _ -> i <> n) lst 408 409let replace_at n value lst = 410 List.mapi (fun i v -> if i = n then value else v) lst 411 412(* Common navigation for mutation operations *) 413 414let navigate_to_child token json ~on_object ~on_array ~on_other = 415 match json with 416 | Jsont.Object (members, meta) -> on_object members meta 417 | Jsont.Array (elements, meta) -> 418 (match Token.is_valid_array_index token with 419 | Some n -> on_array elements meta n 420 | None -> 421 Jsont.Error.msgf (Jsont.Json.meta json) 422 "JSON Pointer: invalid array index '%s'" token) 423 | _ -> on_other () 424 425let error_member_not_found json token = 426 Jsont.Error.msgf (Jsont.Json.meta json) "JSON Pointer: member '%s' not found" token 427 428let error_index_out_of_bounds json n = 429 Jsont.Error.msgf (Jsont.Json.meta json) "JSON Pointer: index %d out of bounds" n 430 431let error_cannot_navigate json = 432 Jsont.Error.msgf (Jsont.Json.meta json) 433 "JSON Pointer: cannot navigate through %s" (json_sort_string json) 434 435(* Mutation: set - works with any pointer type *) 436 437let rec eval_set_segments segments is_append value json = 438 match segments, is_append with 439 | [], false -> value 440 | [], true -> 441 (* Append to array *) 442 (match json with 443 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta) 444 | _ -> 445 Jsont.Error.msgf (Jsont.Json.meta json) 446 "JSON Pointer: '-' can only be used on arrays, got %s" 447 (json_sort_string json)) 448 | [token], false -> 449 navigate_to_child token json 450 ~on_object:(fun members meta -> 451 if Option.is_some (get_member token members) then 452 Jsont.Object (set_member token value members, meta) 453 else 454 Jsont.Error.msgf (Jsont.Json.meta json) 455 "JSON Pointer: member '%s' not found for set" token) 456 ~on_array:(fun elements meta n -> 457 if n < List.length elements then 458 Jsont.Array (replace_at n value elements, meta) 459 else 460 Jsont.Error.msgf (Jsont.Json.meta json) 461 "JSON Pointer: index %d out of bounds for set" n) 462 ~on_other:(fun () -> 463 Jsont.Error.msgf (Jsont.Json.meta json) 464 "JSON Pointer: cannot set in %s" (json_sort_string json)) 465 | [token], true -> 466 (* Navigate to token, then append *) 467 navigate_to_child token json 468 ~on_object:(fun members meta -> 469 match get_member token members with 470 | Some (_, child) -> 471 let child' = eval_set_segments [] true value child in 472 Jsont.Object (set_member token child' members, meta) 473 | None -> error_member_not_found json token) 474 ~on_array:(fun elements meta n -> 475 match get_nth n elements with 476 | Some child -> 477 let child' = eval_set_segments [] true value child in 478 Jsont.Array (replace_at n child' elements, meta) 479 | None -> error_index_out_of_bounds json n) 480 ~on_other:(fun () -> error_cannot_navigate json) 481 | token :: rest, _ -> 482 navigate_to_child token json 483 ~on_object:(fun members meta -> 484 match get_member token members with 485 | Some (_, child) -> 486 Jsont.Object (set_member token (eval_set_segments rest is_append value child) members, meta) 487 | None -> error_member_not_found json token) 488 ~on_array:(fun elements meta n -> 489 match get_nth n elements with 490 | Some child -> 491 Jsont.Array (replace_at n (eval_set_segments rest is_append value child) elements, meta) 492 | None -> error_index_out_of_bounds json n) 493 ~on_other:(fun () -> error_cannot_navigate json) 494 495let set (Any p) json ~value = 496 eval_set_segments p.segments p.is_append value json 497 498(* Mutation: add (RFC 6902 semantics) - works with any pointer type *) 499 500let rec eval_add_segments segments is_append value json = 501 match segments, is_append with 502 | [], false -> value 503 | [], true -> 504 (* Append to array *) 505 (match json with 506 | Jsont.Array (elements, meta) -> Jsont.Array (elements @ [value], meta) 507 | _ -> 508 Jsont.Error.msgf (Jsont.Json.meta json) 509 "JSON Pointer: '-' can only be used on arrays, got %s" 510 (json_sort_string json)) 511 | [token], false -> 512 navigate_to_child token json 513 ~on_object:(fun members meta -> 514 Jsont.Object (set_member token value members, meta)) 515 ~on_array:(fun elements meta n -> 516 let len = List.length elements in 517 if n <= len then 518 Jsont.Array (insert_at n value elements, meta) 519 else 520 Jsont.Error.msgf (Jsont.Json.meta json) 521 "JSON Pointer: index %d out of bounds for add (array has %d elements)" 522 n len) 523 ~on_other:(fun () -> 524 Jsont.Error.msgf (Jsont.Json.meta json) 525 "JSON Pointer: cannot add to %s" (json_sort_string json)) 526 | [token], true -> 527 (* Navigate to token, then append *) 528 navigate_to_child token json 529 ~on_object:(fun members meta -> 530 match get_member token members with 531 | Some (_, child) -> 532 let child' = eval_add_segments [] true value child in 533 Jsont.Object (set_member token child' members, meta) 534 | None -> error_member_not_found json token) 535 ~on_array:(fun elements meta n -> 536 match get_nth n elements with 537 | Some child -> 538 let child' = eval_add_segments [] true value child in 539 Jsont.Array (replace_at n child' elements, meta) 540 | None -> error_index_out_of_bounds json n) 541 ~on_other:(fun () -> error_cannot_navigate json) 542 | token :: rest, _ -> 543 navigate_to_child token json 544 ~on_object:(fun members meta -> 545 match get_member token members with 546 | Some (_, child) -> 547 Jsont.Object (set_member token (eval_add_segments rest is_append value child) members, meta) 548 | None -> error_member_not_found json token) 549 ~on_array:(fun elements meta n -> 550 match get_nth n elements with 551 | Some child -> 552 Jsont.Array (replace_at n (eval_add_segments rest is_append value child) elements, meta) 553 | None -> error_index_out_of_bounds json n) 554 ~on_other:(fun () -> error_cannot_navigate json) 555 556let add (Any p) json ~value = 557 eval_add_segments p.segments p.is_append value json 558 559(* Mutation: remove - only for nav pointers *) 560 561let rec eval_remove_segments segments json = 562 match segments with 563 | [] -> 564 Jsont.Error.msgf Jsont.Meta.none "JSON Pointer: cannot remove root document" 565 | [token] -> 566 navigate_to_child token json 567 ~on_object:(fun members meta -> 568 if Option.is_some (get_member token members) then 569 Jsont.Object (remove_member token members, meta) 570 else 571 Jsont.Error.msgf (Jsont.Json.meta json) 572 "JSON Pointer: member '%s' not found for remove" token) 573 ~on_array:(fun elements meta n -> 574 if n < List.length elements then 575 Jsont.Array (remove_at n elements, meta) 576 else 577 Jsont.Error.msgf (Jsont.Json.meta json) 578 "JSON Pointer: index %d out of bounds for remove" n) 579 ~on_other:(fun () -> 580 Jsont.Error.msgf (Jsont.Json.meta json) 581 "JSON Pointer: cannot remove from %s" (json_sort_string json)) 582 | token :: rest -> 583 navigate_to_child token json 584 ~on_object:(fun members meta -> 585 match get_member token members with 586 | Some (_, child) -> 587 Jsont.Object (set_member token (eval_remove_segments rest child) members, meta) 588 | None -> error_member_not_found json token) 589 ~on_array:(fun elements meta n -> 590 match get_nth n elements with 591 | Some child -> 592 Jsont.Array (replace_at n (eval_remove_segments rest child) elements, meta) 593 | None -> error_index_out_of_bounds json n) 594 ~on_other:(fun () -> error_cannot_navigate json) 595 596let remove (p : nav t) json = eval_remove_segments p.segments json 597 598(* Mutation: replace - only for nav pointers *) 599 600let replace (p : nav t) json ~value = 601 let _ = get p json in (* Will raise if not found *) 602 eval_set_segments p.segments false value json 603 604(* Mutation: move *) 605 606let move ~(from : nav t) ~(path : any) json = 607 let (Any p) = path in 608 (* Check for cycle: path cannot be a proper prefix of from *) 609 let from_segs = from.segments in 610 let path_segs = p.segments in 611 let rec is_prefix p1 p2 = match p1, p2 with 612 | [], _ -> true 613 | _, [] -> false 614 | h1 :: t1, h2 :: t2 -> String.equal h1 h2 && is_prefix t1 t2 615 in 616 if is_prefix path_segs from_segs && 617 not (List.equal String.equal path_segs from_segs && p.is_append = false) then 618 Jsont.Error.msgf Jsont.Meta.none 619 "JSON Pointer: move would create cycle (path is prefix of from)"; 620 let value = get from json in 621 let json' = remove from json in 622 add path json' ~value 623 624(* Mutation: copy *) 625 626let copy ~(from : nav t) ~(path : any) json = 627 let value = get from json in 628 add path json ~value 629 630(* Mutation: test *) 631 632let test (p : nav t) json ~expected = 633 Option.fold ~none:false ~some:(Jsont.Json.equal expected) (find p json) 634 635(* Jsont codec *) 636 637let jsont : any Jsont.t = 638 let dec _meta s = of_string s in 639 let enc (Any p) = to_string p in 640 Jsont.Base.string (Jsont.Base.map 641 ~kind:"JSON Pointer" 642 ~doc:"RFC 6901 JSON Pointer" 643 ~dec ~enc ()) 644 645let jsont_kind : [ `Nav of nav t | `Append of append t ] Jsont.t = 646 let dec _meta s = of_string_kind s in 647 let enc = function 648 | `Nav p -> to_string p 649 | `Append p -> to_string p 650 in 651 Jsont.Base.string (Jsont.Base.map 652 ~kind:"JSON Pointer (kind)" 653 ~doc:"RFC 6901 JSON Pointer with kind tag" 654 ~dec ~enc ()) 655 656let jsont_nav : nav t Jsont.t = 657 let dec _meta s = of_string_nav s in 658 let enc p = to_string p in 659 Jsont.Base.string (Jsont.Base.map 660 ~kind:"JSON Pointer (nav)" 661 ~doc:"RFC 6901 JSON Pointer (navigation only)" 662 ~dec ~enc ()) 663 664let jsont_uri_fragment : any Jsont.t = 665 let dec _meta s = of_uri_fragment s in 666 let enc (Any p) = to_uri_fragment p in 667 Jsont.Base.string (Jsont.Base.map 668 ~kind:"JSON Pointer (URI fragment)" 669 ~doc:"RFC 6901 JSON Pointer in URI fragment encoding" 670 ~dec ~enc ()) 671 672(* Query combinators *) 673 674let path ?absent (p : nav t) t = 675 let dec json = 676 match find p json with 677 | Some value -> 678 (match Jsont.Json.decode' t value with 679 | Ok v -> v 680 | Error e -> raise (Jsont.Error e)) 681 | None -> 682 match absent with 683 | Some v -> v 684 | None -> 685 Jsont.Error.msgf Jsont.Meta.none 686 "JSON Pointer %s: path not found" (to_string p) 687 in 688 Jsont.map Jsont.json ~dec ~enc:(fun _ -> 689 Jsont.Error.msgf Jsont.Meta.none "path: encode not supported") 690 691let set_path ?(allow_absent = false) t (p : any) v = 692 let encoded = match Jsont.Json.encode' t v with 693 | Ok json -> json 694 | Error e -> raise (Jsont.Error e) 695 in 696 let dec json = 697 if allow_absent then 698 add p json ~value:encoded 699 else 700 set p json ~value:encoded 701 in 702 Jsont.map Jsont.json ~dec ~enc:(fun j -> j) 703 704let update_path ?absent (p : nav t) t = 705 let dec json = 706 let value = match find p json with 707 | Some v -> v 708 | None -> 709 match absent with 710 | Some v -> 711 (match Jsont.Json.encode' t v with 712 | Ok j -> j 713 | Error e -> raise (Jsont.Error e)) 714 | None -> 715 Jsont.Error.msgf Jsont.Meta.none 716 "JSON Pointer %s: path not found" (to_string p) 717 in 718 let decoded = match Jsont.Json.decode' t value with 719 | Ok v -> v 720 | Error e -> raise (Jsont.Error e) 721 in 722 let re_encoded = match Jsont.Json.encode' t decoded with 723 | Ok j -> j 724 | Error e -> raise (Jsont.Error e) 725 in 726 set (Any p) json ~value:re_encoded 727 in 728 Jsont.map Jsont.json ~dec ~enc:(fun j -> j) 729 730let delete_path ?(allow_absent = false) (p : nav t) = 731 let dec json = 732 if allow_absent then 733 match find p json with 734 | Some _ -> remove p json 735 | None -> json 736 else 737 remove p json 738 in 739 Jsont.map Jsont.json ~dec ~enc:(fun j -> j) 740 741(* JMAP Extended Pointers - RFC 8620 Section 3.7 *) 742module Jmap = struct 743 (* Extended segment type: regular tokens or wildcard *) 744 type segment = 745 | Token of string (* Unescaped reference token *) 746 | Wildcard (* The * token for array mapping *) 747 748 type t = segment list 749 750 let parse_segments s = 751 if s = "" then [] 752 else if s.[0] <> '/' then 753 Jsont.Error.msgf Jsont.Meta.none 754 "Invalid JMAP Pointer: must be empty or start with '/': %s" s 755 else 756 let rest = String.sub s 1 (String.length s - 1) in 757 let tokens = String.split_on_char '/' rest in 758 List.map (fun tok -> 759 if tok = "*" then Wildcard 760 else if tok = "-" then 761 Jsont.Error.msgf Jsont.Meta.none 762 "Invalid JMAP Pointer: '-' not supported in result reference paths" 763 else Token (Token.unescape tok) 764 ) tokens 765 766 let of_string s = parse_segments s 767 768 let of_string_result s = 769 try Ok (of_string s) 770 with Jsont.Error e -> Error (Jsont.Error.to_string e) 771 772 let segment_to_string = function 773 | Token s -> Token.escape s 774 | Wildcard -> "*" 775 776 let to_string p = 777 if p = [] then "" 778 else 779 let b = Buffer.create 64 in 780 List.iter (fun seg -> 781 Buffer.add_char b '/'; 782 Buffer.add_string b (segment_to_string seg) 783 ) p; 784 Buffer.contents b 785 786 let pp ppf p = Format.pp_print_string ppf (to_string p) 787 788 (* Evaluation with wildcard support *) 789 let rec eval_segments segments json = 790 match segments with 791 | [] -> json 792 | Wildcard :: rest -> 793 (* Wildcard: map through array, flatten results *) 794 (match json with 795 | Jsont.Array (elements, meta) -> 796 let results = List.map (eval_segments rest) elements in 797 (* Flatten: if a result is an array, inline its contents *) 798 let flattened = List.concat_map (function 799 | Jsont.Array (elems, _) -> elems 800 | other -> [other] 801 ) results in 802 Jsont.Array (flattened, meta) 803 | _ -> 804 Jsont.Error.msgf (Jsont.Json.meta json) 805 "JMAP Pointer: '*' can only be used on arrays, got %s" 806 (json_sort_string json)) 807 | Token token :: rest -> 808 (* Standard token: navigate into object or array *) 809 (match json with 810 | Jsont.Object (members, _) -> 811 (match get_member token members with 812 | Some (_, value) -> eval_segments rest value 813 | None -> 814 Jsont.Error.msgf (Jsont.Json.meta json) 815 "JMAP Pointer: member '%s' not found" token) 816 | Jsont.Array (elements, _) -> 817 (match Token.is_valid_array_index token with 818 | Some n -> 819 (match get_nth n elements with 820 | Some value -> eval_segments rest value 821 | None -> 822 Jsont.Error.msgf (Jsont.Json.meta json) 823 "JMAP Pointer: index %d out of bounds (array has %d elements)" 824 n (List.length elements)) 825 | None -> 826 Jsont.Error.msgf (Jsont.Json.meta json) 827 "JMAP Pointer: invalid array index '%s'" token) 828 | _ -> 829 Jsont.Error.msgf (Jsont.Json.meta json) 830 "JMAP Pointer: cannot index into %s with '%s'" 831 (json_sort_string json) token) 832 833 let eval p json = eval_segments p json 834 835 let eval_result p json = 836 try Ok (eval p json) 837 with Jsont.Error e -> Error e 838 839 let find p json = 840 try Some (eval p json) 841 with Jsont.Error _ -> None 842 843 let jsont : t Jsont.t = 844 let dec _meta s = of_string s in 845 let enc p = to_string p in 846 Jsont.Base.string (Jsont.Base.map 847 ~kind:"JMAP Pointer" 848 ~doc:"RFC 8620 JMAP extended JSON Pointer" 849 ~dec ~enc ()) 850 851 (* Query combinators *) 852 853 let path ?absent p codec = 854 let dec json = 855 match find p json with 856 | Some extracted -> 857 (match Jsont.Json.decode' codec extracted with 858 | Ok v -> v 859 | Error e -> raise (Jsont.Error e)) 860 | None -> 861 match absent with 862 | Some v -> v 863 | None -> 864 Jsont.Error.msgf Jsont.Meta.none 865 "JMAP Pointer %s: path not found" (to_string p) 866 in 867 Jsont.map Jsont.json ~dec ~enc:(fun _ -> 868 Jsont.Error.msgf Jsont.Meta.none "Jmap.path: encode not supported") 869 870 let path_list p elem_codec = 871 path p (Jsont.list elem_codec) 872end