RFC6901 JSON Pointer implementation in OCaml using jsont
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