+1
.gitignore
+1
.gitignore
···
···
1
+
_build
+18
dune-project
+18
dune-project
···
···
1
+
(lang dune 3.17)
2
+
(name jsont-pointer)
3
+
(version 0.1.0)
4
+
5
+
(generate_opam_files true)
6
+
7
+
(source (github avsm/jsont-pointer))
8
+
(license ISC)
9
+
(authors "Anil Madhavapeddy")
10
+
(maintainers "anil@recoil.org")
11
+
12
+
(package
13
+
(name jsont-pointer)
14
+
(synopsis "RFC 6901 JSON Pointer implementation for jsont")
15
+
(description "This library provides RFC 6901 JSON Pointer parsing, serialization, and evaluation compatible with jsont codecs. It also provides mutation operations suitable for implementing RFC 6902 JSON Patch.")
16
+
(depends
17
+
(ocaml (>= 4.14.0))
18
+
(jsont (>= 0.2.0))))
+32
jsont-pointer.opam
+32
jsont-pointer.opam
···
···
1
+
# This file is generated by dune, edit dune-project instead
2
+
opam-version: "2.0"
3
+
version: "0.1.0"
4
+
synopsis: "RFC 6901 JSON Pointer implementation for jsont"
5
+
description:
6
+
"This library provides RFC 6901 JSON Pointer parsing, serialization, and evaluation compatible with jsont codecs. It also provides mutation operations suitable for implementing RFC 6902 JSON Patch."
7
+
maintainer: ["anil@recoil.org"]
8
+
authors: ["Anil Madhavapeddy"]
9
+
license: "ISC"
10
+
homepage: "https://github.com/avsm/jsont-pointer"
11
+
bug-reports: "https://github.com/avsm/jsont-pointer/issues"
12
+
depends: [
13
+
"dune" {>= "3.17"}
14
+
"ocaml" {>= "4.14.0"}
15
+
"jsont" {>= "0.2.0"}
16
+
"odoc" {with-doc}
17
+
]
18
+
build: [
19
+
["dune" "subst"] {dev}
20
+
[
21
+
"dune"
22
+
"build"
23
+
"-p"
24
+
name
25
+
"-j"
26
+
jobs
27
+
"@install"
28
+
"@runtest" {with-test}
29
+
"@doc" {with-doc}
30
+
]
31
+
]
32
+
dev-repo: "git+https://github.com/avsm/jsont-pointer.git"
+451
spec/rfc6901.txt
+451
spec/rfc6901.txt
···
···
1
+
2
+
3
+
4
+
5
+
6
+
7
+
Internet Engineering Task Force (IETF) P. Bryan, Ed.
8
+
Request for Comments: 6901 Salesforce.com
9
+
Category: Standards Track K. Zyp
10
+
ISSN: 2070-1721 SitePen (USA)
11
+
M. Nottingham, Ed.
12
+
Akamai
13
+
April 2013
14
+
15
+
16
+
JavaScript Object Notation (JSON) Pointer
17
+
18
+
Abstract
19
+
20
+
JSON Pointer defines a string syntax for identifying a specific value
21
+
within a JavaScript Object Notation (JSON) document.
22
+
23
+
Status of This Memo
24
+
25
+
This is an Internet Standards Track document.
26
+
27
+
This document is a product of the Internet Engineering Task Force
28
+
(IETF). It represents the consensus of the IETF community. It has
29
+
received public review and has been approved for publication by the
30
+
Internet Engineering Steering Group (IESG). Further information on
31
+
Internet Standards is available in Section 2 of RFC 5741.
32
+
33
+
Information about the current status of this document, any errata,
34
+
and how to provide feedback on it may be obtained at
35
+
http://www.rfc-editor.org/info/rfc6901.
36
+
37
+
Copyright Notice
38
+
39
+
Copyright (c) 2013 IETF Trust and the persons identified as the
40
+
document authors. All rights reserved.
41
+
42
+
This document is subject to BCP 78 and the IETF Trust's Legal
43
+
Provisions Relating to IETF Documents
44
+
(http://trustee.ietf.org/license-info) in effect on the date of
45
+
publication of this document. Please review these documents
46
+
carefully, as they describe your rights and restrictions with respect
47
+
to this document. Code Components extracted from this document must
48
+
include Simplified BSD License text as described in Section 4.e of
49
+
the Trust Legal Provisions and are provided without warranty as
50
+
described in the Simplified BSD License.
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
Bryan, et al. Standards Track [Page 1]
59
+
60
+
RFC 6901 JSON Pointer April 2013
61
+
62
+
63
+
Table of Contents
64
+
65
+
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
66
+
2. Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . 2
67
+
3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
68
+
4. Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . 3
69
+
5. JSON String Representation . . . . . . . . . . . . . . . . . . 4
70
+
6. URI Fragment Identifier Representation . . . . . . . . . . . . 5
71
+
7. Error Handling . . . . . . . . . . . . . . . . . . . . . . . . 6
72
+
8. Security Considerations . . . . . . . . . . . . . . . . . . . . 6
73
+
9. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 7
74
+
10. References . . . . . . . . . . . . . . . . . . . . . . . . . . 7
75
+
10.1. Normative References . . . . . . . . . . . . . . . . . . . 7
76
+
10.2. Informative References . . . . . . . . . . . . . . . . . . 7
77
+
78
+
1. Introduction
79
+
80
+
This specification defines JSON Pointer, a string syntax for
81
+
identifying a specific value within a JavaScript Object Notation
82
+
(JSON) document [RFC4627]. JSON Pointer is intended to be easily
83
+
expressed in JSON string values as well as Uniform Resource
84
+
Identifier (URI) [RFC3986] fragment identifiers.
85
+
86
+
2. Conventions
87
+
88
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
89
+
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
90
+
document are to be interpreted as described in [RFC2119].
91
+
92
+
This specification expresses normative syntax rules using Augmented
93
+
Backus-Naur Form (ABNF) [RFC5234] notation.
94
+
95
+
3. Syntax
96
+
97
+
A JSON Pointer is a Unicode string (see [RFC4627], Section 3)
98
+
containing a sequence of zero or more reference tokens, each prefixed
99
+
by a '/' (%x2F) character.
100
+
101
+
Because the characters '~' (%x7E) and '/' (%x2F) have special
102
+
meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/'
103
+
needs to be encoded as '~1' when these characters appear in a
104
+
reference token.
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
Bryan, et al. Standards Track [Page 2]
115
+
116
+
RFC 6901 JSON Pointer April 2013
117
+
118
+
119
+
The ABNF syntax of a JSON Pointer is:
120
+
121
+
json-pointer = *( "/" reference-token )
122
+
reference-token = *( unescaped / escaped )
123
+
unescaped = %x00-2E / %x30-7D / %x7F-10FFFF
124
+
; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
125
+
escaped = "~" ( "0" / "1" )
126
+
; representing '~' and '/', respectively
127
+
128
+
It is an error condition if a JSON Pointer value does not conform to
129
+
this syntax (see Section 7).
130
+
131
+
Note that JSON Pointers are specified in characters, not as bytes.
132
+
133
+
4. Evaluation
134
+
135
+
Evaluation of a JSON Pointer begins with a reference to the root
136
+
value of a JSON document and completes with a reference to some value
137
+
within the document. Each reference token in the JSON Pointer is
138
+
evaluated sequentially.
139
+
140
+
Evaluation of each reference token begins by decoding any escaped
141
+
character sequence. This is performed by first transforming any
142
+
occurrence of the sequence '~1' to '/', and then transforming any
143
+
occurrence of the sequence '~0' to '~'. By performing the
144
+
substitutions in this order, an implementation avoids the error of
145
+
turning '~01' first into '~1' and then into '/', which would be
146
+
incorrect (the string '~01' correctly becomes '~1' after
147
+
transformation).
148
+
149
+
The reference token then modifies which value is referenced according
150
+
to the following scheme:
151
+
152
+
o If the currently referenced value is a JSON object, the new
153
+
referenced value is the object member with the name identified by
154
+
the reference token. The member name is equal to the token if it
155
+
has the same number of Unicode characters as the token and their
156
+
code points are byte-by-byte equal. No Unicode character
157
+
normalization is performed. If a referenced member name is not
158
+
unique in an object, the member that is referenced is undefined,
159
+
and evaluation fails (see below).
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
Bryan, et al. Standards Track [Page 3]
171
+
172
+
RFC 6901 JSON Pointer April 2013
173
+
174
+
175
+
o If the currently referenced value is a JSON array, the reference
176
+
token MUST contain either:
177
+
178
+
* characters comprised of digits (see ABNF below; note that
179
+
leading zeros are not allowed) that represent an unsigned
180
+
base-10 integer value, making the new referenced value the
181
+
array element with the zero-based index identified by the
182
+
token, or
183
+
184
+
* exactly the single character "-", making the new referenced
185
+
value the (nonexistent) member after the last array element.
186
+
187
+
The ABNF syntax for array indices is:
188
+
189
+
array-index = %x30 / ( %x31-39 *(%x30-39) )
190
+
; "0", or digits without a leading "0"
191
+
192
+
Implementations will evaluate each reference token against the
193
+
document's contents and will raise an error condition if it fails to
194
+
resolve a concrete value for any of the JSON pointer's reference
195
+
tokens. For example, if an array is referenced with a non-numeric
196
+
token, an error condition will be raised. See Section 7 for details.
197
+
198
+
Note that the use of the "-" character to index an array will always
199
+
result in such an error condition because by definition it refers to
200
+
a nonexistent array element. Thus, applications of JSON Pointer need
201
+
to specify how that character is to be handled, if it is to be
202
+
useful.
203
+
204
+
Any error condition for which a specific action is not defined by the
205
+
JSON Pointer application results in termination of evaluation.
206
+
207
+
5. JSON String Representation
208
+
209
+
A JSON Pointer can be represented in a JSON string value. Per
210
+
[RFC4627], Section 2.5, all instances of quotation mark '"' (%x22),
211
+
reverse solidus '\' (%x5C), and control (%x00-1F) characters MUST be
212
+
escaped.
213
+
214
+
Note that before processing a JSON string as a JSON Pointer,
215
+
backslash escape sequences must be unescaped.
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+
Bryan, et al. Standards Track [Page 4]
227
+
228
+
RFC 6901 JSON Pointer April 2013
229
+
230
+
231
+
For example, given the JSON document
232
+
233
+
{
234
+
"foo": ["bar", "baz"],
235
+
"": 0,
236
+
"a/b": 1,
237
+
"c%d": 2,
238
+
"e^f": 3,
239
+
"g|h": 4,
240
+
"i\\j": 5,
241
+
"k\"l": 6,
242
+
" ": 7,
243
+
"m~n": 8
244
+
}
245
+
246
+
The following JSON strings evaluate to the accompanying values:
247
+
248
+
"" // the whole document
249
+
"/foo" ["bar", "baz"]
250
+
"/foo/0" "bar"
251
+
"/" 0
252
+
"/a~1b" 1
253
+
"/c%d" 2
254
+
"/e^f" 3
255
+
"/g|h" 4
256
+
"/i\\j" 5
257
+
"/k\"l" 6
258
+
"/ " 7
259
+
"/m~0n" 8
260
+
261
+
6. URI Fragment Identifier Representation
262
+
263
+
A JSON Pointer can be represented in a URI fragment identifier by
264
+
encoding it into octets using UTF-8 [RFC3629], while percent-encoding
265
+
those characters not allowed by the fragment rule in [RFC3986].
266
+
267
+
Note that a given media type needs to specify JSON Pointer as its
268
+
fragment identifier syntax explicitly (usually, in its registration
269
+
[RFC6838]). That is, just because a document is JSON does not imply
270
+
that JSON Pointer can be used as its fragment identifier syntax. In
271
+
particular, the fragment identifier syntax for application/json is
272
+
not JSON Pointer.
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
Bryan, et al. Standards Track [Page 5]
283
+
284
+
RFC 6901 JSON Pointer April 2013
285
+
286
+
287
+
Given the same example document as above, the following URI fragment
288
+
identifiers evaluate to the accompanying values:
289
+
290
+
# // the whole document
291
+
#/foo ["bar", "baz"]
292
+
#/foo/0 "bar"
293
+
#/ 0
294
+
#/a~1b 1
295
+
#/c%25d 2
296
+
#/e%5Ef 3
297
+
#/g%7Ch 4
298
+
#/i%5Cj 5
299
+
#/k%22l 6
300
+
#/%20 7
301
+
#/m~0n 8
302
+
303
+
7. Error Handling
304
+
305
+
In the event of an error condition, evaluation of the JSON Pointer
306
+
fails to complete.
307
+
308
+
Error conditions include, but are not limited to:
309
+
310
+
o Invalid pointer syntax
311
+
312
+
o A pointer that references a nonexistent value
313
+
314
+
This specification does not define how errors are handled. An
315
+
application of JSON Pointer SHOULD specify the impact and handling of
316
+
each type of error.
317
+
318
+
For example, some applications might stop pointer processing upon an
319
+
error, while others may attempt to recover from missing values by
320
+
inserting default ones.
321
+
322
+
8. Security Considerations
323
+
324
+
A given JSON Pointer is not guaranteed to reference an actual JSON
325
+
value. Therefore, applications using JSON Pointer should anticipate
326
+
this situation by defining how a pointer that does not resolve ought
327
+
to be handled.
328
+
329
+
Note that JSON pointers can contain the NUL (Unicode U+0000)
330
+
character. Care is needed not to misinterpret this character in
331
+
programming languages that use NUL to mark the end of a string.
332
+
333
+
334
+
335
+
336
+
337
+
338
+
Bryan, et al. Standards Track [Page 6]
339
+
340
+
RFC 6901 JSON Pointer April 2013
341
+
342
+
343
+
9. Acknowledgements
344
+
345
+
The following individuals contributed ideas, feedback, and wording to
346
+
this specification:
347
+
348
+
Mike Acar, Carsten Bormann, Tim Bray, Jacob Davies, Martin J.
349
+
Duerst, Bjoern Hoehrmann, James H. Manger, Drew Perttula, and
350
+
Julian Reschke.
351
+
352
+
10. References
353
+
354
+
10.1. Normative References
355
+
356
+
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
357
+
Requirement Levels", BCP 14, RFC 2119, March 1997.
358
+
359
+
[RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO
360
+
10646", STD 63, RFC 3629, November 2003.
361
+
362
+
[RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
363
+
Resource Identifier (URI): Generic Syntax", STD 66,
364
+
RFC 3986, January 2005.
365
+
366
+
[RFC4627] Crockford, D., "The application/json Media Type for
367
+
JavaScript Object Notation (JSON)", RFC 4627, July 2006.
368
+
369
+
[RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax
370
+
Specifications: ABNF", STD 68, RFC 5234, January 2008.
371
+
372
+
10.2. Informative References
373
+
374
+
[RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type
375
+
Specifications and Registration Procedures", BCP 13,
376
+
RFC 6838, January 2013.
377
+
378
+
379
+
380
+
381
+
382
+
383
+
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+
392
+
393
+
394
+
Bryan, et al. Standards Track [Page 7]
395
+
396
+
RFC 6901 JSON Pointer April 2013
397
+
398
+
399
+
Authors' Addresses
400
+
401
+
Paul C. Bryan (editor)
402
+
Salesforce.com
403
+
404
+
Phone: +1 604 783 1481
405
+
EMail: pbryan@anode.ca
406
+
407
+
408
+
Kris Zyp
409
+
SitePen (USA)
410
+
411
+
Phone: +1 650 968 8787
412
+
EMail: kris@sitepen.com
413
+
414
+
415
+
Mark Nottingham (editor)
416
+
Akamai
417
+
418
+
EMail: mnot@mnot.net
419
+
420
+
421
+
422
+
423
+
424
+
425
+
426
+
427
+
428
+
429
+
430
+
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
+
439
+
440
+
441
+
442
+
443
+
444
+
445
+
446
+
447
+
448
+
449
+
450
+
Bryan, et al. Standards Track [Page 8]
451
+
+4
src/dune
+4
src/dune
+736
src/jsont_pointer.ml
+736
src/jsont_pointer.ml
···
···
1
+
(*---------------------------------------------------------------------------
2
+
Copyright (c) 2024 The jsont programmers. All rights reserved.
3
+
SPDX-License-Identifier: ISC
4
+
---------------------------------------------------------------------------*)
5
+
6
+
(* Token escaping/unescaping per RFC 6901 Section 3-4 *)
7
+
module 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
55
+
end
56
+
57
+
(* Index type - represents how a token is interpreted in context *)
58
+
module Index = struct
59
+
type t =
60
+
| Mem of string
61
+
| Nth of int
62
+
| End
63
+
64
+
let pp ppf = function
65
+
| Mem s -> Format.fprintf ppf "/%s" (Token.escape s)
66
+
| Nth n -> Format.fprintf ppf "/%d" n
67
+
| End -> Format.fprintf ppf "/-"
68
+
69
+
let equal i1 i2 = match i1, i2 with
70
+
| Mem s1, Mem s2 -> String.equal s1 s2
71
+
| Nth n1, Nth n2 -> Int.equal n1 n2
72
+
| End, End -> true
73
+
| _ -> false
74
+
75
+
let compare i1 i2 = match i1, i2 with
76
+
| Mem s1, Mem s2 -> String.compare s1 s2
77
+
| Mem _, _ -> -1
78
+
| _, Mem _ -> 1
79
+
| Nth n1, Nth n2 -> Int.compare n1 n2
80
+
| Nth _, End -> -1
81
+
| End, Nth _ -> 1
82
+
| End, End -> 0
83
+
84
+
let of_path_index (idx : Jsont.Path.index) : t =
85
+
match idx with
86
+
| Jsont.Path.Mem (s, _meta) -> Mem s
87
+
| Jsont.Path.Nth (n, _meta) -> Nth n
88
+
89
+
let to_path_index (idx : t) : Jsont.Path.index option =
90
+
match idx with
91
+
| Mem s -> Some (Jsont.Path.Mem (s, Jsont.Meta.none))
92
+
| Nth n -> Some (Jsont.Path.Nth (n, Jsont.Meta.none))
93
+
| End -> None
94
+
end
95
+
96
+
(* Internal representation: raw unescaped tokens.
97
+
Per RFC 6901, interpretation as member name vs array index
98
+
depends on the JSON value type at evaluation time. *)
99
+
module Segment = struct
100
+
type t =
101
+
| Token of string (* Unescaped reference token *)
102
+
| End (* The "-" token for end-of-array *)
103
+
104
+
let of_escaped_string s =
105
+
if s = "-" then End
106
+
else Token (Token.unescape s)
107
+
108
+
let to_escaped_string = function
109
+
| Token s -> Token.escape s
110
+
| End -> "-"
111
+
112
+
(* Convert to Index for a given JSON value type *)
113
+
let to_index seg ~for_array =
114
+
match seg with
115
+
| End -> Index.End
116
+
| Token s ->
117
+
if for_array then
118
+
match Token.is_valid_array_index s with
119
+
| Some n -> Index.Nth n
120
+
| None -> Index.Mem s (* Invalid index becomes member for error msg *)
121
+
else
122
+
Index.Mem s
123
+
124
+
(* Convert from Index *)
125
+
let of_index = function
126
+
| Index.End -> End
127
+
| Index.Mem s -> Token s
128
+
| Index.Nth n -> Token (string_of_int n)
129
+
end
130
+
131
+
(* Pointer type - list of segments *)
132
+
type t = Segment.t list
133
+
134
+
let root = []
135
+
136
+
let is_root p = p = []
137
+
138
+
(* Convert indices to segments *)
139
+
let make indices = List.map Segment.of_index indices
140
+
141
+
(* Convert segments to indices, assuming array context for numeric tokens *)
142
+
let indices p = List.map (fun seg -> Segment.to_index seg ~for_array:true) p
143
+
144
+
let append p idx = p @ [Segment.of_index idx]
145
+
146
+
let concat p1 p2 = p1 @ p2
147
+
148
+
let parent p = match List.rev p with
149
+
| [] -> None
150
+
| _ :: rest -> Some (List.rev rest)
151
+
152
+
let last p = match List.rev p with
153
+
| [] -> None
154
+
| seg :: _ -> Some (Segment.to_index seg ~for_array:true)
155
+
156
+
(* Parsing *)
157
+
158
+
let of_string s =
159
+
if s = "" then root
160
+
else if s.[0] <> '/' then
161
+
Jsont.Error.msgf Jsont.Meta.none
162
+
"Invalid JSON Pointer: must be empty or start with '/': %s" s
163
+
else
164
+
let rest = String.sub s 1 (String.length s - 1) in
165
+
let tokens = String.split_on_char '/' rest in
166
+
List.map Segment.of_escaped_string tokens
167
+
168
+
let of_string_result s =
169
+
try Ok (of_string s)
170
+
with Jsont.Error (_, _, _) as e ->
171
+
Error (Jsont.Error.to_string (match e with Jsont.Error e -> e | _ -> assert false))
172
+
173
+
(* URI fragment percent-decoding *)
174
+
let hex_value c =
175
+
if c >= '0' && c <= '9' then Char.code c - Char.code '0'
176
+
else if c >= 'A' && c <= 'F' then Char.code c - Char.code 'A' + 10
177
+
else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10
178
+
else -1
179
+
180
+
let percent_decode s =
181
+
let len = String.length s in
182
+
let b = Buffer.create len in
183
+
let rec loop i =
184
+
if i >= len then Buffer.contents b
185
+
else match s.[i] with
186
+
| '%' when i + 2 < len ->
187
+
let h1 = hex_value s.[i + 1] in
188
+
let h2 = hex_value s.[i + 2] in
189
+
if h1 >= 0 && h2 >= 0 then begin
190
+
Buffer.add_char b (Char.chr ((h1 lsl 4) lor h2));
191
+
loop (i + 3)
192
+
end else
193
+
Jsont.Error.msgf Jsont.Meta.none
194
+
"Invalid percent-encoding at position %d" i
195
+
| '%' ->
196
+
Jsont.Error.msgf Jsont.Meta.none
197
+
"Incomplete percent-encoding at position %d" i
198
+
| c -> Buffer.add_char b c; loop (i + 1)
199
+
in
200
+
loop 0
201
+
202
+
let of_uri_fragment s =
203
+
of_string (percent_decode s)
204
+
205
+
let of_uri_fragment_result s =
206
+
try Ok (of_uri_fragment s)
207
+
with Jsont.Error (_, _, _) as e ->
208
+
Error (Jsont.Error.to_string (match e with Jsont.Error e -> e | _ -> assert false))
209
+
210
+
(* Serialization *)
211
+
212
+
let to_string p =
213
+
if p = [] then ""
214
+
else
215
+
let b = Buffer.create 64 in
216
+
List.iter (fun seg ->
217
+
Buffer.add_char b '/';
218
+
Buffer.add_string b (Segment.to_escaped_string seg)
219
+
) p;
220
+
Buffer.contents b
221
+
222
+
(* URI fragment percent-encoding *)
223
+
let needs_percent_encoding c =
224
+
(* RFC 3986 fragment: unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" *)
225
+
(* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" *)
226
+
(* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *)
227
+
not (
228
+
(c >= 'A' && c <= 'Z') ||
229
+
(c >= 'a' && c <= 'z') ||
230
+
(c >= '0' && c <= '9') ||
231
+
c = '-' || c = '.' || c = '_' || c = '~' ||
232
+
c = '!' || c = '$' || c = '&' || c = '\'' ||
233
+
c = '(' || c = ')' || c = '*' || c = '+' ||
234
+
c = ',' || c = ';' || c = '=' ||
235
+
c = ':' || c = '@' || c = '/' || c = '?'
236
+
)
237
+
238
+
let hex_char n =
239
+
if n < 10 then Char.chr (Char.code '0' + n)
240
+
else Char.chr (Char.code 'A' + n - 10)
241
+
242
+
let percent_encode s =
243
+
let b = Buffer.create (String.length s * 3) in
244
+
String.iter (fun c ->
245
+
if needs_percent_encoding c then begin
246
+
let code = Char.code c in
247
+
Buffer.add_char b '%';
248
+
Buffer.add_char b (hex_char (code lsr 4));
249
+
Buffer.add_char b (hex_char (code land 0xF))
250
+
end else
251
+
Buffer.add_char b c
252
+
) s;
253
+
Buffer.contents b
254
+
255
+
let to_uri_fragment p =
256
+
percent_encode (to_string p)
257
+
258
+
let pp ppf p =
259
+
Format.pp_print_string ppf (to_string p)
260
+
261
+
(* Comparison *)
262
+
263
+
let segment_equal s1 s2 = match s1, s2 with
264
+
| Segment.Token t1, Segment.Token t2 -> String.equal t1 t2
265
+
| Segment.End, Segment.End -> true
266
+
| _ -> false
267
+
268
+
let segment_compare s1 s2 = match s1, s2 with
269
+
| Segment.Token t1, Segment.Token t2 -> String.compare t1 t2
270
+
| Segment.Token _, Segment.End -> -1
271
+
| Segment.End, Segment.Token _ -> 1
272
+
| Segment.End, Segment.End -> 0
273
+
274
+
let equal p1 p2 =
275
+
List.length p1 = List.length p2 &&
276
+
List.for_all2 segment_equal p1 p2
277
+
278
+
let compare p1 p2 =
279
+
let rec loop l1 l2 = match l1, l2 with
280
+
| [], [] -> 0
281
+
| [], _ -> -1
282
+
| _, [] -> 1
283
+
| h1 :: t1, h2 :: t2 ->
284
+
let c = segment_compare h1 h2 in
285
+
if c <> 0 then c else loop t1 t2
286
+
in
287
+
loop p1 p2
288
+
289
+
(* Path conversion *)
290
+
291
+
let segment_of_path_index (idx : Jsont.Path.index) : Segment.t =
292
+
match idx with
293
+
| Jsont.Path.Mem (s, _meta) -> Segment.Token s
294
+
| Jsont.Path.Nth (n, _meta) -> Segment.Token (string_of_int n)
295
+
296
+
let of_path (p : Jsont.Path.t) : t =
297
+
List.rev_map segment_of_path_index (Jsont.Path.rev_indices p)
298
+
299
+
let to_path p =
300
+
let rec convert acc = function
301
+
| [] -> Some acc
302
+
| Segment.End :: _ -> None
303
+
| Segment.Token s :: rest ->
304
+
(* For path conversion, we need to decide if it's a member or index.
305
+
We use array context for numeric tokens since Jsont.Path distinguishes. *)
306
+
let acc' = match Token.is_valid_array_index s with
307
+
| Some n -> Jsont.Path.nth ~meta:Jsont.Meta.none n acc
308
+
| None -> Jsont.Path.mem ~meta:Jsont.Meta.none s acc
309
+
in
310
+
convert acc' rest
311
+
in
312
+
convert Jsont.Path.root p
313
+
314
+
let to_path_exn p =
315
+
match to_path p with
316
+
| Some path -> path
317
+
| None ->
318
+
Jsont.Error.msgf Jsont.Meta.none
319
+
"Cannot convert JSON Pointer with '-' index to Jsont.Path"
320
+
321
+
(* Evaluation helpers *)
322
+
323
+
let json_sort_string (j : Jsont.json) =
324
+
match j with
325
+
| Null _ -> "null"
326
+
| Bool _ -> "boolean"
327
+
| Number _ -> "number"
328
+
| String _ -> "string"
329
+
| Array _ -> "array"
330
+
| Object _ -> "object"
331
+
332
+
let get_member name (obj : Jsont.object') =
333
+
List.find_opt (fun ((n, _), _) -> String.equal n name) obj
334
+
335
+
let get_nth n (arr : Jsont.json list) =
336
+
if n < 0 || n >= List.length arr then None
337
+
else Some (List.nth arr n)
338
+
339
+
(* Evaluation *)
340
+
341
+
let rec eval_get p json =
342
+
match p with
343
+
| [] -> json
344
+
| Segment.End :: _ ->
345
+
Jsont.Error.msgf (Jsont.Json.meta json)
346
+
"JSON Pointer: '-' (end marker) refers to nonexistent array element"
347
+
| Segment.Token token :: rest ->
348
+
(match json with
349
+
| Jsont.Object (members, _) ->
350
+
(* For objects, token is always a member name *)
351
+
(match get_member token members with
352
+
| Some (_, value) -> eval_get rest value
353
+
| None ->
354
+
Jsont.Error.msgf (Jsont.Json.meta json)
355
+
"JSON Pointer: member '%s' not found" token)
356
+
| Jsont.Array (elements, _) ->
357
+
(* For arrays, token must be a valid array index *)
358
+
(match Token.is_valid_array_index token with
359
+
| Some n ->
360
+
(match get_nth n elements with
361
+
| Some value -> eval_get rest value
362
+
| None ->
363
+
Jsont.Error.msgf (Jsont.Json.meta json)
364
+
"JSON Pointer: index %d out of bounds (array has %d elements)"
365
+
n (List.length elements))
366
+
| None ->
367
+
Jsont.Error.msgf (Jsont.Json.meta json)
368
+
"JSON Pointer: invalid array index '%s'" token)
369
+
| _ ->
370
+
Jsont.Error.msgf (Jsont.Json.meta json)
371
+
"JSON Pointer: cannot index into %s with '%s'"
372
+
(json_sort_string json) token)
373
+
374
+
let get p json = eval_get p json
375
+
376
+
let get_result p json =
377
+
try Ok (get p json)
378
+
with Jsont.Error e -> Error e
379
+
380
+
let find p json =
381
+
try Some (get p json)
382
+
with Jsont.Error _ -> None
383
+
384
+
(* Mutation helpers *)
385
+
386
+
let set_member name value (obj : Jsont.object') : Jsont.object' =
387
+
let found = ref false in
388
+
let result = List.map (fun ((n, m), v) ->
389
+
if String.equal n name then begin
390
+
found := true;
391
+
((n, m), value)
392
+
end else
393
+
((n, m), v)
394
+
) obj in
395
+
if !found then result
396
+
else obj @ [((name, Jsont.Meta.none), value)]
397
+
398
+
let remove_member name (obj : Jsont.object') : Jsont.object' =
399
+
List.filter (fun ((n, _), _) -> not (String.equal n name)) obj
400
+
401
+
let insert_at n value lst =
402
+
let rec loop i acc = function
403
+
| [] when i = n -> List.rev (value :: acc)
404
+
| [] -> List.rev acc (* shouldn't happen if n is valid *)
405
+
| h :: t when i = n -> List.rev_append (value :: acc) (h :: t)
406
+
| h :: t -> loop (i + 1) (h :: acc) t
407
+
in
408
+
loop 0 [] lst
409
+
410
+
let remove_at n lst =
411
+
let rec loop i acc = function
412
+
| [] -> List.rev acc
413
+
| _ :: t when i = n -> List.rev_append acc t
414
+
| h :: t -> loop (i + 1) (h :: acc) t
415
+
in
416
+
loop 0 [] lst
417
+
418
+
let replace_at n value lst =
419
+
List.mapi (fun i v -> if i = n then value else v) lst
420
+
421
+
(* Mutation: set *)
422
+
423
+
let rec eval_set p value json =
424
+
match p with
425
+
| [] -> value
426
+
| [Segment.End] ->
427
+
(match json with
428
+
| Jsont.Array (elements, meta) ->
429
+
Jsont.Array (elements @ [value], meta)
430
+
| _ ->
431
+
Jsont.Error.msgf (Jsont.Json.meta json)
432
+
"JSON Pointer: '-' can only be used on arrays, got %s"
433
+
(json_sort_string json))
434
+
| Segment.End :: _ ->
435
+
Jsont.Error.msgf (Jsont.Json.meta json)
436
+
"JSON Pointer: '-' (end marker) refers to nonexistent array element"
437
+
| [Segment.Token token] ->
438
+
(match json with
439
+
| Jsont.Object (members, meta) ->
440
+
if Option.is_some (get_member token members) then
441
+
Jsont.Object (set_member token value members, meta)
442
+
else
443
+
Jsont.Error.msgf (Jsont.Json.meta json)
444
+
"JSON Pointer: member '%s' not found for set" token
445
+
| Jsont.Array (elements, meta) ->
446
+
(match Token.is_valid_array_index token with
447
+
| Some n when n >= 0 && n < List.length elements ->
448
+
Jsont.Array (replace_at n value elements, meta)
449
+
| Some n ->
450
+
Jsont.Error.msgf (Jsont.Json.meta json)
451
+
"JSON Pointer: index %d out of bounds for set" n
452
+
| None ->
453
+
Jsont.Error.msgf (Jsont.Json.meta json)
454
+
"JSON Pointer: invalid array index '%s'" token)
455
+
| _ ->
456
+
Jsont.Error.msgf (Jsont.Json.meta json)
457
+
"JSON Pointer: cannot set in %s" (json_sort_string json))
458
+
| Segment.Token token :: rest ->
459
+
(match json with
460
+
| Jsont.Object (members, meta) ->
461
+
(match get_member token members with
462
+
| Some (_, child) ->
463
+
Jsont.Object (set_member token (eval_set rest value child) members, meta)
464
+
| None ->
465
+
Jsont.Error.msgf (Jsont.Json.meta json)
466
+
"JSON Pointer: member '%s' not found" token)
467
+
| Jsont.Array (elements, meta) ->
468
+
(match Token.is_valid_array_index token with
469
+
| Some n ->
470
+
(match get_nth n elements with
471
+
| Some child ->
472
+
Jsont.Array (replace_at n (eval_set rest value child) elements, meta)
473
+
| None ->
474
+
Jsont.Error.msgf (Jsont.Json.meta json)
475
+
"JSON Pointer: index %d out of bounds" n)
476
+
| None ->
477
+
Jsont.Error.msgf (Jsont.Json.meta json)
478
+
"JSON Pointer: invalid array index '%s'" token)
479
+
| _ ->
480
+
Jsont.Error.msgf (Jsont.Json.meta json)
481
+
"JSON Pointer: cannot navigate through %s" (json_sort_string json))
482
+
483
+
let set p json ~value = eval_set p value json
484
+
485
+
(* Mutation: add (RFC 6902 semantics) *)
486
+
487
+
let rec eval_add p value json =
488
+
match p with
489
+
| [] -> value
490
+
| [Segment.End] ->
491
+
(match json with
492
+
| Jsont.Array (elements, meta) ->
493
+
Jsont.Array (elements @ [value], meta)
494
+
| _ ->
495
+
Jsont.Error.msgf (Jsont.Json.meta json)
496
+
"JSON Pointer: '-' can only be used on arrays, got %s"
497
+
(json_sort_string json))
498
+
| Segment.End :: _ ->
499
+
Jsont.Error.msgf (Jsont.Json.meta json)
500
+
"JSON Pointer: '-' in non-final position"
501
+
| [Segment.Token token] ->
502
+
(match json with
503
+
| Jsont.Object (members, meta) ->
504
+
(* For objects, add/replace member *)
505
+
Jsont.Object (set_member token value members, meta)
506
+
| Jsont.Array (elements, meta) ->
507
+
(* For arrays, insert at index *)
508
+
(match Token.is_valid_array_index token with
509
+
| Some n ->
510
+
let len = List.length elements in
511
+
if n >= 0 && n <= len then
512
+
Jsont.Array (insert_at n value elements, meta)
513
+
else
514
+
Jsont.Error.msgf (Jsont.Json.meta json)
515
+
"JSON Pointer: index %d out of bounds for add (array has %d elements)"
516
+
n len
517
+
| None ->
518
+
Jsont.Error.msgf (Jsont.Json.meta json)
519
+
"JSON Pointer: invalid array index '%s'" token)
520
+
| _ ->
521
+
Jsont.Error.msgf (Jsont.Json.meta json)
522
+
"JSON Pointer: cannot add to %s" (json_sort_string json))
523
+
| Segment.Token token :: rest ->
524
+
(match json with
525
+
| Jsont.Object (members, meta) ->
526
+
(match get_member token members with
527
+
| Some (_, child) ->
528
+
Jsont.Object (set_member token (eval_add rest value child) members, meta)
529
+
| None ->
530
+
Jsont.Error.msgf (Jsont.Json.meta json)
531
+
"JSON Pointer: member '%s' not found" token)
532
+
| Jsont.Array (elements, meta) ->
533
+
(match Token.is_valid_array_index token with
534
+
| Some n ->
535
+
(match get_nth n elements with
536
+
| Some child ->
537
+
Jsont.Array (replace_at n (eval_add rest value child) elements, meta)
538
+
| None ->
539
+
Jsont.Error.msgf (Jsont.Json.meta json)
540
+
"JSON Pointer: index %d out of bounds" n)
541
+
| None ->
542
+
Jsont.Error.msgf (Jsont.Json.meta json)
543
+
"JSON Pointer: invalid array index '%s'" token)
544
+
| _ ->
545
+
Jsont.Error.msgf (Jsont.Json.meta json)
546
+
"JSON Pointer: cannot navigate through %s" (json_sort_string json))
547
+
548
+
let add p json ~value = eval_add p value json
549
+
550
+
(* Mutation: remove *)
551
+
552
+
let rec eval_remove p json =
553
+
match p with
554
+
| [] ->
555
+
Jsont.Error.msgf Jsont.Meta.none
556
+
"JSON Pointer: cannot remove root document"
557
+
| [Segment.End] ->
558
+
Jsont.Error.msgf (Jsont.Json.meta json)
559
+
"JSON Pointer: '-' refers to nonexistent element"
560
+
| Segment.End :: _ ->
561
+
Jsont.Error.msgf (Jsont.Json.meta json)
562
+
"JSON Pointer: '-' in non-final position"
563
+
| [Segment.Token token] ->
564
+
(match json with
565
+
| Jsont.Object (members, meta) ->
566
+
if Option.is_some (get_member token members) then
567
+
Jsont.Object (remove_member token members, meta)
568
+
else
569
+
Jsont.Error.msgf (Jsont.Json.meta json)
570
+
"JSON Pointer: member '%s' not found for remove" token
571
+
| Jsont.Array (elements, meta) ->
572
+
(match Token.is_valid_array_index token with
573
+
| Some n when n >= 0 && n < List.length elements ->
574
+
Jsont.Array (remove_at n elements, meta)
575
+
| Some n ->
576
+
Jsont.Error.msgf (Jsont.Json.meta json)
577
+
"JSON Pointer: index %d out of bounds for remove" n
578
+
| None ->
579
+
Jsont.Error.msgf (Jsont.Json.meta json)
580
+
"JSON Pointer: invalid array index '%s'" token)
581
+
| _ ->
582
+
Jsont.Error.msgf (Jsont.Json.meta json)
583
+
"JSON Pointer: cannot remove from %s" (json_sort_string json))
584
+
| Segment.Token token :: rest ->
585
+
(match json with
586
+
| Jsont.Object (members, meta) ->
587
+
(match get_member token members with
588
+
| Some (_, child) ->
589
+
Jsont.Object (set_member token (eval_remove rest child) members, meta)
590
+
| None ->
591
+
Jsont.Error.msgf (Jsont.Json.meta json)
592
+
"JSON Pointer: member '%s' not found" token)
593
+
| Jsont.Array (elements, meta) ->
594
+
(match Token.is_valid_array_index token with
595
+
| Some n ->
596
+
(match get_nth n elements with
597
+
| Some child ->
598
+
Jsont.Array (replace_at n (eval_remove rest child) elements, meta)
599
+
| None ->
600
+
Jsont.Error.msgf (Jsont.Json.meta json)
601
+
"JSON Pointer: index %d out of bounds" n)
602
+
| None ->
603
+
Jsont.Error.msgf (Jsont.Json.meta json)
604
+
"JSON Pointer: invalid array index '%s'" token)
605
+
| _ ->
606
+
Jsont.Error.msgf (Jsont.Json.meta json)
607
+
"JSON Pointer: cannot navigate through %s" (json_sort_string json))
608
+
609
+
let remove p json = eval_remove p json
610
+
611
+
(* Mutation: replace *)
612
+
613
+
let replace p json ~value =
614
+
(* Replace requires the target to exist, unlike add *)
615
+
let _ = get p json in (* Will raise if not found *)
616
+
eval_set p value json
617
+
618
+
(* Mutation: move *)
619
+
620
+
let is_prefix_of p1 p2 =
621
+
let rec loop l1 l2 = match l1, l2 with
622
+
| [], _ -> true
623
+
| _, [] -> false
624
+
| h1 :: t1, h2 :: t2 ->
625
+
segment_equal h1 h2 && loop t1 t2
626
+
in
627
+
loop p1 p2
628
+
629
+
let move ~from ~path json =
630
+
(* Check for cycle: path cannot be a proper prefix of from *)
631
+
if is_prefix_of path from && not (equal path from) then
632
+
Jsont.Error.msgf Jsont.Meta.none
633
+
"JSON Pointer: move would create cycle (path is prefix of from)";
634
+
let value = get from json in
635
+
let json' = remove from json in
636
+
add path json' ~value
637
+
638
+
(* Mutation: copy *)
639
+
640
+
let copy ~from ~path json =
641
+
let value = get from json in
642
+
add path json ~value
643
+
644
+
(* Mutation: test *)
645
+
646
+
let test p json ~expected =
647
+
match find p json with
648
+
| None -> false
649
+
| Some value -> Jsont.Json.equal value expected
650
+
651
+
(* Jsont codec *)
652
+
653
+
let jsont : t Jsont.t =
654
+
let dec _meta s = of_string s in
655
+
let enc p = to_string p in
656
+
Jsont.Base.string (Jsont.Base.map
657
+
~kind:"JSON Pointer"
658
+
~doc:"RFC 6901 JSON Pointer"
659
+
~dec ~enc ())
660
+
661
+
let jsont_uri_fragment : t Jsont.t =
662
+
let dec _meta s = of_uri_fragment s in
663
+
let enc p = to_uri_fragment p in
664
+
Jsont.Base.string (Jsont.Base.map
665
+
~kind:"JSON Pointer (URI fragment)"
666
+
~doc:"RFC 6901 JSON Pointer in URI fragment encoding"
667
+
~dec ~enc ())
668
+
669
+
(* Query combinators *)
670
+
671
+
let path ?absent p t =
672
+
let dec json =
673
+
match find p json with
674
+
| Some value ->
675
+
(match Jsont.Json.decode' t value with
676
+
| Ok v -> v
677
+
| Error e -> raise (Jsont.Error e))
678
+
| None ->
679
+
match absent with
680
+
| Some v -> v
681
+
| None ->
682
+
Jsont.Error.msgf Jsont.Meta.none
683
+
"JSON Pointer %s: path not found" (to_string p)
684
+
in
685
+
Jsont.map Jsont.json ~dec ~enc:(fun _ ->
686
+
Jsont.Error.msgf Jsont.Meta.none "path: encode not supported")
687
+
688
+
let set_path ?(allow_absent = false) t p v =
689
+
let encoded = match Jsont.Json.encode' t v with
690
+
| Ok json -> json
691
+
| Error e -> raise (Jsont.Error e)
692
+
in
693
+
let dec json =
694
+
if allow_absent then
695
+
add p json ~value:encoded
696
+
else
697
+
set p json ~value:encoded
698
+
in
699
+
Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
700
+
701
+
let update_path ?absent p t =
702
+
let dec json =
703
+
let value = match find p json with
704
+
| Some v -> v
705
+
| None ->
706
+
match absent with
707
+
| Some v ->
708
+
(match Jsont.Json.encode' t v with
709
+
| Ok j -> j
710
+
| Error e -> raise (Jsont.Error e))
711
+
| None ->
712
+
Jsont.Error.msgf Jsont.Meta.none
713
+
"JSON Pointer %s: path not found" (to_string p)
714
+
in
715
+
let decoded = match Jsont.Json.decode' t value with
716
+
| Ok v -> v
717
+
| Error e -> raise (Jsont.Error e)
718
+
in
719
+
let re_encoded = match Jsont.Json.encode' t decoded with
720
+
| Ok j -> j
721
+
| Error e -> raise (Jsont.Error e)
722
+
in
723
+
set p json ~value:re_encoded
724
+
in
725
+
Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
726
+
727
+
let delete_path ?(allow_absent = false) p =
728
+
let dec json =
729
+
if allow_absent then
730
+
match find p json with
731
+
| Some _ -> remove p json
732
+
| None -> json
733
+
else
734
+
remove p json
735
+
in
736
+
Jsont.map Jsont.json ~dec ~enc:(fun j -> j)
+368
src/jsont_pointer.mli
+368
src/jsont_pointer.mli
···
···
1
+
(*---------------------------------------------------------------------------
2
+
Copyright (c) 2024 The jsont programmers. 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 [/]. *)
43
+
module 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]). *)
59
+
end
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 [-]. *)
66
+
module 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
+
val pp : Format.formatter -> t -> unit
84
+
(** [pp] formats an index in JSON Pointer string notation. *)
85
+
86
+
val equal : t -> t -> bool
87
+
(** [equal i1 i2] is [true] iff [i1] and [i2] are the same index. *)
88
+
89
+
val compare : t -> t -> int
90
+
(** [compare i1 i2] is a total order on indices. *)
91
+
92
+
(** {2:jsont_conv Conversion with Jsont.Path} *)
93
+
94
+
val of_path_index : Jsont.Path.index -> t
95
+
(** [of_path_index idx] converts a {!Jsont.Path.index} to an index. *)
96
+
97
+
val to_path_index : t -> Jsont.Path.index option
98
+
(** [to_path_index idx] converts to a {!Jsont.Path.index}.
99
+
Returns [None] for {!End} since it has no equivalent in
100
+
{!Jsont.Path}. *)
101
+
end
102
+
103
+
(** {1 Pointers} *)
104
+
105
+
type t
106
+
(** The type for JSON Pointers. A pointer is a sequence of {!Index.t}
107
+
values representing a path from the root of a JSON document to
108
+
a specific value. *)
109
+
110
+
val root : t
111
+
(** [root] is the empty pointer that references the whole document.
112
+
In string form this is [""]. *)
113
+
114
+
val is_root : t -> bool
115
+
(** [is_root p] is [true] iff [p] is the {!root} pointer. *)
116
+
117
+
val make : Index.t list -> t
118
+
(** [make indices] creates a pointer from a list of indices.
119
+
The list is ordered from root to target (i.e., the first element
120
+
is the first step from the root). *)
121
+
122
+
val indices : t -> Index.t list
123
+
(** [indices p] returns the indices of [p] from root to target. *)
124
+
125
+
val append : t -> Index.t -> t
126
+
(** [append p idx] appends [idx] to the end of pointer [p]. *)
127
+
128
+
val concat : t -> t -> t
129
+
(** [concat p1 p2] appends all indices of [p2] to [p1]. *)
130
+
131
+
val parent : t -> t option
132
+
(** [parent p] returns the parent pointer of [p], or [None] if [p]
133
+
is the {!root}. *)
134
+
135
+
val last : t -> Index.t option
136
+
(** [last p] returns the last index of [p], or [None] if [p] is
137
+
the {!root}. *)
138
+
139
+
(** {2:parsing Parsing} *)
140
+
141
+
val of_string : string -> t
142
+
(** [of_string s] parses a JSON Pointer from its string representation.
143
+
144
+
The string must be either empty (representing the root) or start
145
+
with [/]. Each segment between [/] characters is unescaped as a
146
+
reference token. Segments that are valid non-negative integers
147
+
without leading zeros become {!Index.Nth} indices; the string [-]
148
+
becomes {!Index.End}; all others become {!Index.Mem}.
149
+
150
+
@raise Jsont.Error if [s] has invalid syntax:
151
+
- Non-empty string not starting with [/]
152
+
- Invalid escape sequence ([~] not followed by [0] or [1])
153
+
- Array index with leading zeros
154
+
- Array index that overflows [int] *)
155
+
156
+
val of_string_result : string -> (t, string) result
157
+
(** [of_string_result s] is like {!of_string} but returns a result
158
+
instead of raising. *)
159
+
160
+
val of_uri_fragment : string -> t
161
+
(** [of_uri_fragment s] parses a JSON Pointer from URI fragment form.
162
+
163
+
This is like {!of_string} but first percent-decodes the string
164
+
according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}.
165
+
The leading [#] should {b not} be included in [s].
166
+
167
+
@raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
168
+
169
+
val of_uri_fragment_result : string -> (t, string) result
170
+
(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
171
+
a result instead of raising. *)
172
+
173
+
(** {2:serializing Serializing} *)
174
+
175
+
val to_string : t -> string
176
+
(** [to_string p] serializes [p] to its JSON Pointer string representation.
177
+
178
+
Returns [""] for the root pointer, otherwise [/] followed by
179
+
escaped reference tokens joined by [/]. *)
180
+
181
+
val to_uri_fragment : t -> string
182
+
(** [to_uri_fragment p] serializes [p] to URI fragment form.
183
+
184
+
This is like {!to_string} but additionally percent-encodes
185
+
characters that are not allowed in URI fragments per RFC 3986.
186
+
The leading [#] is {b not} included in the result. *)
187
+
188
+
val pp : Format.formatter -> t -> unit
189
+
(** [pp] formats a pointer using {!to_string}. *)
190
+
191
+
(** {2:comparison Comparison} *)
192
+
193
+
val equal : t -> t -> bool
194
+
(** [equal p1 p2] is [true] iff [p1] and [p2] have the same indices. *)
195
+
196
+
val compare : t -> t -> int
197
+
(** [compare p1 p2] is a total order on pointers, comparing indices
198
+
lexicographically. *)
199
+
200
+
(** {2:jsont_path Conversion with Jsont.Path} *)
201
+
202
+
val of_path : Jsont.Path.t -> t
203
+
(** [of_path p] converts a {!Jsont.Path.t} to a JSON Pointer. *)
204
+
205
+
val to_path : t -> Jsont.Path.t option
206
+
(** [to_path p] converts to a {!Jsont.Path.t}.
207
+
Returns [None] if [p] contains an {!Index.End} index. *)
208
+
209
+
val to_path_exn : t -> Jsont.Path.t
210
+
(** [to_path_exn p] is like {!to_path} but raises {!Jsont.Error}
211
+
if conversion fails. *)
212
+
213
+
(** {1 Evaluation}
214
+
215
+
These functions evaluate a JSON Pointer against a {!Jsont.json} value
216
+
to retrieve the referenced value. *)
217
+
218
+
val get : t -> Jsont.json -> Jsont.json
219
+
(** [get p json] retrieves the value at pointer [p] in [json].
220
+
221
+
@raise Jsont.Error if:
222
+
- The pointer references a nonexistent object member
223
+
- The pointer references an out-of-bounds array index
224
+
- The pointer contains {!Index.End} (since [-] always refers
225
+
to a nonexistent element)
226
+
- An index type doesn't match the JSON value (e.g., {!Index.Nth}
227
+
on an object) *)
228
+
229
+
val get_result : t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
230
+
(** [get_result p json] is like {!get} but returns a result. *)
231
+
232
+
val find : t -> Jsont.json -> Jsont.json option
233
+
(** [find p json] is like {!get} but returns [None] instead of
234
+
raising when the pointer doesn't resolve to a value. *)
235
+
236
+
(** {1 Mutation}
237
+
238
+
These functions modify a {!Jsont.json} value at a location specified
239
+
by a JSON Pointer. They are designed to support
240
+
{{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch}
241
+
operations.
242
+
243
+
All mutation functions return a new JSON value with the modification
244
+
applied; they do not mutate the input. *)
245
+
246
+
val set : t -> Jsont.json -> value:Jsont.json -> Jsont.json
247
+
(** [set p json ~value] replaces the value at pointer [p] with [value].
248
+
249
+
For {!Index.End} on arrays, appends [value] to the end of the array.
250
+
251
+
@raise Jsont.Error if the pointer doesn't resolve to an existing
252
+
location (except for {!Index.End} on arrays). *)
253
+
254
+
val add : t -> Jsont.json -> value:Jsont.json -> Jsont.json
255
+
(** [add p json ~value] adds [value] at the location specified by [p].
256
+
257
+
The behavior depends on the target:
258
+
{ul
259
+
{- For objects: If the member exists, it is replaced. If it doesn't
260
+
exist, a new member is added.}
261
+
{- For arrays with {!Index.Nth}: Inserts [value] {e before} the
262
+
specified index, shifting subsequent elements. The index must be
263
+
valid (0 to length inclusive).}
264
+
{- For arrays with {!Index.End}: Appends [value] to the array.}}
265
+
266
+
@raise Jsont.Error if:
267
+
- The parent of the target location doesn't exist
268
+
- An array index is out of bounds (except for {!Index.End})
269
+
- The parent is not an object or array *)
270
+
271
+
val remove : t -> Jsont.json -> Jsont.json
272
+
(** [remove p json] removes the value at pointer [p].
273
+
274
+
For objects, removes the member. For arrays, removes the element
275
+
and shifts subsequent elements.
276
+
277
+
@raise Jsont.Error if:
278
+
- [p] is the root (cannot remove the root)
279
+
- The pointer doesn't resolve to an existing value
280
+
- The pointer contains {!Index.End} *)
281
+
282
+
val replace : t -> Jsont.json -> value:Jsont.json -> Jsont.json
283
+
(** [replace p json ~value] replaces the value at pointer [p] with [value].
284
+
285
+
Unlike {!add}, this requires the target to exist.
286
+
287
+
@raise Jsont.Error if:
288
+
- The pointer doesn't resolve to an existing value
289
+
- The pointer contains {!Index.End} *)
290
+
291
+
val move : from:t -> path:t -> Jsont.json -> Jsont.json
292
+
(** [move ~from ~path json] moves the value from [from] to [path].
293
+
294
+
This is equivalent to {!remove} at [from] followed by {!add}
295
+
at [path] with the removed value.
296
+
297
+
@raise Jsont.Error if:
298
+
- [from] doesn't resolve to a value
299
+
- [path] is a proper prefix of [from] (would create a cycle)
300
+
- Either pointer contains {!Index.End} *)
301
+
302
+
val copy : from:t -> path:t -> Jsont.json -> Jsont.json
303
+
(** [copy ~from ~path json] copies the value from [from] to [path].
304
+
305
+
This is equivalent to {!get} at [from] followed by {!add}
306
+
at [path] with the retrieved value.
307
+
308
+
@raise Jsont.Error if:
309
+
- [from] doesn't resolve to a value
310
+
- Either pointer contains {!Index.End} *)
311
+
312
+
val test : t -> Jsont.json -> expected:Jsont.json -> bool
313
+
(** [test p json ~expected] tests if the value at [p] equals [expected].
314
+
315
+
Returns [true] if the values are equal according to {!Jsont.Json.equal},
316
+
[false] otherwise. Also returns [false] (rather than raising) if the
317
+
pointer doesn't resolve.
318
+
319
+
Note: This implements the semantics of the JSON Patch "test" operation. *)
320
+
321
+
(** {1 Jsont Integration}
322
+
323
+
These types and functions integrate JSON Pointers with the {!Jsont}
324
+
codec system. *)
325
+
326
+
val jsont : t Jsont.t
327
+
(** [jsont] is a {!Jsont.t} codec for JSON Pointers.
328
+
329
+
On decode, parses a JSON string as a JSON Pointer using {!of_string}.
330
+
On encode, serializes a pointer to a JSON string using {!to_string}. *)
331
+
332
+
val jsont_uri_fragment : t Jsont.t
333
+
(** [jsont_uri_fragment] is like {!jsont} but uses URI fragment encoding.
334
+
335
+
On decode, parses using {!of_uri_fragment}.
336
+
On encode, serializes using {!to_uri_fragment}. *)
337
+
338
+
(** {2:query Query combinators}
339
+
340
+
These combinators integrate with jsont's query system, allowing
341
+
JSON Pointers to be used with jsont codecs for typed access. *)
342
+
343
+
val path : ?absent:'a -> t -> 'a Jsont.t -> 'a Jsont.t
344
+
(** [path p t] decodes the value at pointer [p] using codec [t].
345
+
346
+
If [absent] is provided and the pointer doesn't resolve, returns
347
+
[absent] instead of raising.
348
+
349
+
This is similar to {!Jsont.path} but uses JSON Pointer syntax. *)
350
+
351
+
val set_path : ?allow_absent:bool -> 'a Jsont.t -> t -> 'a -> Jsont.json Jsont.t
352
+
(** [set_path t p v] sets the value at pointer [p] to [v] encoded with [t].
353
+
354
+
If [allow_absent] is [true] (default [false]), creates missing
355
+
intermediate structure as needed.
356
+
357
+
This is similar to {!Jsont.set_path} but uses JSON Pointer syntax. *)
358
+
359
+
val update_path : ?absent:'a -> t -> 'a Jsont.t -> Jsont.json Jsont.t
360
+
(** [update_path p t] recodes the value at pointer [p] with codec [t].
361
+
362
+
This is similar to {!Jsont.update_path} but uses JSON Pointer syntax. *)
363
+
364
+
val delete_path : ?allow_absent:bool -> t -> Jsont.json Jsont.t
365
+
(** [delete_path p] removes the value at pointer [p].
366
+
367
+
If [allow_absent] is [true] (default [false]), does nothing if
368
+
the pointer doesn't resolve instead of raising. *)
+151
test/comprehensive.t
+151
test/comprehensive.t
···
···
1
+
Comprehensive JSON Pointer Tests (from json-pointer-js test suite)
2
+
3
+
Additional parsing tests:
4
+
$ ./test_pointer.exe parse "/foo/bar"
5
+
OK: [Mem:foo, Mem:bar]
6
+
$ ./test_pointer.exe parse "/foo/-"
7
+
OK: [Mem:foo, End]
8
+
$ ./test_pointer.exe parse "/foo/1"
9
+
OK: [Mem:foo, Nth:1]
10
+
$ ./test_pointer.exe parse "/foo/~0"
11
+
OK: [Mem:foo, Mem:~]
12
+
$ ./test_pointer.exe parse "/foo/~1"
13
+
OK: [Mem:foo, Mem:/]
14
+
$ ./test_pointer.exe parse "/foo/~1~0"
15
+
OK: [Mem:foo, Mem:/~]
16
+
$ ./test_pointer.exe parse "/foo/~0~1"
17
+
OK: [Mem:foo, Mem:~/]
18
+
$ ./test_pointer.exe parse "/foo/~01"
19
+
OK: [Mem:foo, Mem:~1]
20
+
$ ./test_pointer.exe parse "/foo/~10"
21
+
OK: [Mem:foo, Mem:/0]
22
+
23
+
Parse error tests:
24
+
$ ./test_pointer.exe parse "foo/bar"
25
+
ERROR: Invalid JSON Pointer: must be empty or start with '/': foo/bar
26
+
$ ./test_pointer.exe parse "~foo"
27
+
ERROR: Invalid JSON Pointer: must be empty or start with '/': ~foo
28
+
$ ./test_pointer.exe parse "/~a"
29
+
ERROR: Invalid JSON Pointer: invalid escape sequence ~a
30
+
$ ./test_pointer.exe parse "~1/foo"
31
+
ERROR: Invalid JSON Pointer: must be empty or start with '/': ~1/foo
32
+
33
+
Get value tests:
34
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/a~1b"
35
+
OK: 1
36
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/c%d"
37
+
OK: 2
38
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/e^f"
39
+
OK: 3
40
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/g|h"
41
+
OK: 4
42
+
$ ./test_pointer.exe eval data/rfc6901_example.json '/i\j'
43
+
OK: 5
44
+
$ ./test_pointer.exe eval data/rfc6901_example.json '/k"l'
45
+
OK: 6
46
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/ "
47
+
OK: 7
48
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/m~0n"
49
+
OK: 8
50
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/"
51
+
OK: 0
52
+
53
+
Set tests (using add for replace/add semantics):
54
+
$ ./test_pointer.exe add '{"bar":"foo"}' '/bar' '"baz"'
55
+
{"bar":"baz"}
56
+
$ ./test_pointer.exe add '{"bar":"foo"}' '/foo' '"baz"'
57
+
{"bar":"foo","foo":"baz"}
58
+
$ ./test_pointer.exe add '["foo"]' '/0' '"bar"'
59
+
["bar","foo"]
60
+
$ ./test_pointer.exe add '["foo"]' '/-' '"bar"'
61
+
["foo","bar"]
62
+
$ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/0' '"baz"'
63
+
{"foo":["baz","bar"]}
64
+
65
+
Replace tests using special characters:
66
+
$ ./test_pointer.exe replace '{"a/b":"bar"}' '/a~1b' '"baz"'
67
+
{"a/b":"baz"}
68
+
$ ./test_pointer.exe replace '{"c%d":"bar"}' '/c%d' '"baz"'
69
+
{"c%d":"baz"}
70
+
$ ./test_pointer.exe replace '{"e^f":"bar"}' '/e^f' '"baz"'
71
+
{"e^f":"baz"}
72
+
$ ./test_pointer.exe replace '{"g|h":"bar"}' '/g|h' '"baz"'
73
+
{"g|h":"baz"}
74
+
$ ./test_pointer.exe replace '{" ":"bar"}' '/ ' '"baz"'
75
+
{" ":"baz"}
76
+
$ ./test_pointer.exe replace '{"m~n":"bar"}' '/m~0n' '"baz"'
77
+
{"m~n":"baz"}
78
+
$ ./test_pointer.exe replace '{"":"bar"}' '/' '"baz"'
79
+
{"":"baz"}
80
+
81
+
Remove tests:
82
+
$ ./test_pointer.exe remove '{"foo":"bar","baz":"qux"}' '/foo'
83
+
{"baz":"qux"}
84
+
$ ./test_pointer.exe remove '["foo","baz"]' '/1'
85
+
["foo"]
86
+
$ ./test_pointer.exe remove '["foo","baz"]' '/0'
87
+
["baz"]
88
+
$ ./test_pointer.exe remove '{"foo":["bar"]}' '/foo/0'
89
+
{"foo":[]}
90
+
91
+
Copy tests:
92
+
$ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/baz'
93
+
{"foo":"bar","baz":"bar"}
94
+
95
+
Test operation:
96
+
$ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"bar"'
97
+
true
98
+
$ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"baz"'
99
+
false
100
+
$ ./test_pointer.exe test '{"foo":["bar","baz"]}' '/foo' '["bar","baz"]'
101
+
true
102
+
$ ./test_pointer.exe test '{"foo":"bar"}' '/baz' '"qux"'
103
+
false
104
+
105
+
Equality tests (pointers that should be equal after roundtrip):
106
+
$ ./test_pointer.exe roundtrip "/foo/~0"
107
+
OK: /foo/~0
108
+
$ ./test_pointer.exe roundtrip "/foo/~1"
109
+
OK: /foo/~1
110
+
111
+
Has/exists tests (from json-pointer-js):
112
+
$ ./test_pointer.exe has '{"foo":"bar"}' '/foo'
113
+
true
114
+
$ ./test_pointer.exe has '{"foo":"bar"}' '/bar'
115
+
false
116
+
$ ./test_pointer.exe has '{"foo":{"bar":"baz"}}' '/foo/bar'
117
+
true
118
+
$ ./test_pointer.exe has '{"foo":{"bar":"baz"}}' '/foo/qux'
119
+
false
120
+
$ ./test_pointer.exe has '["foo","bar"]' '/0'
121
+
true
122
+
$ ./test_pointer.exe has '["foo","bar"]' '/1'
123
+
true
124
+
$ ./test_pointer.exe has '["foo","bar"]' '/2'
125
+
false
126
+
$ ./test_pointer.exe has '{"foo":["bar"]}' '/foo/0'
127
+
true
128
+
$ ./test_pointer.exe has '{"foo":["bar"]}' '/foo/1'
129
+
false
130
+
$ ./test_pointer.exe has '{}' ''
131
+
true
132
+
$ ./test_pointer.exe has '[]' ''
133
+
true
134
+
$ ./test_pointer.exe has '{"":0}' '/'
135
+
true
136
+
$ ./test_pointer.exe has '{"a/b":1}' '/a~1b'
137
+
true
138
+
$ ./test_pointer.exe has '{"m~n":8}' '/m~0n'
139
+
true
140
+
141
+
Has with null values:
142
+
$ ./test_pointer.exe has '{"foo":null}' '/foo'
143
+
true
144
+
$ ./test_pointer.exe has 'null' ''
145
+
true
146
+
147
+
Has with '-' (end marker - should be false for get, points to nonexistent):
148
+
$ ./test_pointer.exe has '["foo"]' '/-'
149
+
false
150
+
$ ./test_pointer.exe has '[]' '/-'
151
+
false
+25
test/data/escape.txt
+25
test/data/escape.txt
···
···
1
+
# Token escape/unescape tests
2
+
# Format: unescaped -> escaped
3
+
4
+
# Basic cases
5
+
foo -> foo
6
+
bar -> bar
7
+
8
+
# Tilde escaping
9
+
~ -> ~0
10
+
~0 -> ~00
11
+
~1 -> ~01
12
+
13
+
# Slash escaping
14
+
/ -> ~1
15
+
a/b -> a~1b
16
+
/foo/bar -> ~1foo~1bar
17
+
18
+
# Combined
19
+
~/ -> ~0~1
20
+
/~ -> ~1~0
21
+
a~b/c -> a~0b~1c
22
+
23
+
# No escaping needed
24
+
hello world -> hello world
25
+
special!@#$%^&*() -> special!@#$%^&*()
+18
test/data/eval_errors.txt
+18
test/data/eval_errors.txt
···
···
1
+
# Evaluation error tests
2
+
# Format: pointer -> error type
3
+
4
+
# Nonexistent members
5
+
/nonexistent -> member not found
6
+
/foo/nonexistent -> type mismatch (array, not object)
7
+
8
+
# Out of bounds array indices
9
+
/foo/2 -> index out of bounds
10
+
/foo/99 -> index out of bounds
11
+
12
+
# Type mismatches
13
+
/foo/0/bar -> type mismatch (string, not object)
14
+
/ /0 -> type mismatch (number, not array)
15
+
16
+
# End marker always errors on get
17
+
/foo/- -> end marker not allowed
18
+
/- -> end marker not allowed
+19
test/data/eval_rfc6901.txt
+19
test/data/eval_rfc6901.txt
···
···
1
+
# Evaluation tests against RFC 6901 example document
2
+
# Format: pointer -> expected JSON value
3
+
4
+
# Root - whole document
5
+
-> {"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8}
6
+
7
+
# RFC 6901 Section 5 examples
8
+
/foo -> ["bar","baz"]
9
+
/foo/0 -> "bar"
10
+
/foo/1 -> "baz"
11
+
/ -> 0
12
+
/a~1b -> 1
13
+
/c%d -> 2
14
+
/e^f -> 3
15
+
/g|h -> 4
16
+
/i\j -> 5
17
+
/k"l -> 6
18
+
/ -> 7
19
+
/m~0n -> 8
+96
test/data/mutations.json
+96
test/data/mutations.json
···
···
1
+
{
2
+
"add_tests": [
3
+
{
4
+
"doc": {"foo": "bar"},
5
+
"pointer": "/baz",
6
+
"value": "qux",
7
+
"expected": {"foo": "bar", "baz": "qux"}
8
+
},
9
+
{
10
+
"doc": {"foo": ["bar", "baz"]},
11
+
"pointer": "/foo/1",
12
+
"value": "qux",
13
+
"expected": {"foo": ["bar", "qux", "baz"]}
14
+
},
15
+
{
16
+
"doc": {"foo": ["bar"]},
17
+
"pointer": "/foo/-",
18
+
"value": "qux",
19
+
"expected": {"foo": ["bar", "qux"]}
20
+
},
21
+
{
22
+
"doc": {"foo": ["bar", "baz"]},
23
+
"pointer": "/foo/0",
24
+
"value": "qux",
25
+
"expected": {"foo": ["qux", "bar", "baz"]}
26
+
}
27
+
],
28
+
"remove_tests": [
29
+
{
30
+
"doc": {"foo": "bar", "baz": "qux"},
31
+
"pointer": "/baz",
32
+
"expected": {"foo": "bar"}
33
+
},
34
+
{
35
+
"doc": {"foo": ["bar", "qux", "baz"]},
36
+
"pointer": "/foo/1",
37
+
"expected": {"foo": ["bar", "baz"]}
38
+
}
39
+
],
40
+
"replace_tests": [
41
+
{
42
+
"doc": {"foo": "bar"},
43
+
"pointer": "/foo",
44
+
"value": "baz",
45
+
"expected": {"foo": "baz"}
46
+
},
47
+
{
48
+
"doc": {"foo": ["bar", "baz"]},
49
+
"pointer": "/foo/0",
50
+
"value": "qux",
51
+
"expected": {"foo": ["qux", "baz"]}
52
+
}
53
+
],
54
+
"move_tests": [
55
+
{
56
+
"doc": {"foo": {"bar": "baz"}, "qux": {"corge": "grault"}},
57
+
"from": "/foo/bar",
58
+
"path": "/qux/thud",
59
+
"expected": {"foo": {}, "qux": {"corge": "grault", "thud": "baz"}}
60
+
},
61
+
{
62
+
"doc": {"foo": ["all", "grass", "cows", "eat"]},
63
+
"from": "/foo/1",
64
+
"path": "/foo/3",
65
+
"expected": {"foo": ["all", "cows", "eat", "grass"]}
66
+
}
67
+
],
68
+
"copy_tests": [
69
+
{
70
+
"doc": {"foo": {"bar": "baz"}},
71
+
"from": "/foo/bar",
72
+
"path": "/foo/qux",
73
+
"expected": {"foo": {"bar": "baz", "qux": "baz"}}
74
+
}
75
+
],
76
+
"test_tests": [
77
+
{
78
+
"doc": {"foo": "bar"},
79
+
"pointer": "/foo",
80
+
"value": "bar",
81
+
"expected": true
82
+
},
83
+
{
84
+
"doc": {"foo": "bar"},
85
+
"pointer": "/foo",
86
+
"value": "baz",
87
+
"expected": false
88
+
},
89
+
{
90
+
"doc": {"foo": ["bar", "baz"]},
91
+
"pointer": "/foo",
92
+
"value": ["bar", "baz"],
93
+
"expected": true
94
+
}
95
+
]
96
+
}
+11
test/data/nested.json
+11
test/data/nested.json
+24
test/data/parse_invalid.txt
+24
test/data/parse_invalid.txt
···
···
1
+
# Invalid JSON Pointer parsing tests
2
+
# Format: pointer -> error description
3
+
4
+
# Must start with / if non-empty
5
+
foo -> must start with /
6
+
a/b -> must start with /
7
+
0 -> must start with /
8
+
- -> must start with /
9
+
10
+
# Invalid escape sequences
11
+
/~ -> incomplete escape
12
+
/~2 -> invalid escape ~2
13
+
/~a -> invalid escape ~a
14
+
/foo~bar -> invalid escape ~b
15
+
/~~ -> invalid escape ~~
16
+
17
+
# Leading zeros in array indices (RFC 6901 Section 4)
18
+
/00 -> leading zero
19
+
/01 -> leading zero
20
+
/007 -> leading zero
21
+
22
+
# Negative indices not allowed
23
+
/-1 -> not a valid index
24
+
/-42 -> not a valid index
+53
test/data/parse_valid.txt
+53
test/data/parse_valid.txt
···
···
1
+
# Valid JSON Pointer parsing tests
2
+
# Format: pointer -> indices (Mem:name, Nth:n, End)
3
+
4
+
# Root pointer (empty string)
5
+
-> []
6
+
7
+
# RFC 6901 Section 5 examples
8
+
/foo -> [Mem:foo]
9
+
/foo/0 -> [Mem:foo, Nth:0]
10
+
/ -> [Mem:]
11
+
/a~1b -> [Mem:a/b]
12
+
/c%d -> [Mem:c%d]
13
+
/e^f -> [Mem:e^f]
14
+
/g|h -> [Mem:g|h]
15
+
/i\j -> [Mem:i\j]
16
+
/k"l -> [Mem:k"l]
17
+
/ -> [Mem: ]
18
+
/m~0n -> [Mem:m~n]
19
+
20
+
# Array indices
21
+
/0 -> [Nth:0]
22
+
/1 -> [Nth:1]
23
+
/10 -> [Nth:10]
24
+
/123 -> [Nth:123]
25
+
/999999 -> [Nth:999999]
26
+
27
+
# End-of-array marker
28
+
/- -> [End]
29
+
/foo/- -> [Mem:foo, End]
30
+
/arr/0/- -> [Mem:arr, Nth:0, End]
31
+
32
+
# Multiple levels
33
+
/a/b/c -> [Mem:a, Mem:b, Mem:c]
34
+
/0/1/2 -> [Nth:0, Nth:1, Nth:2]
35
+
/foo/0/bar/1 -> [Mem:foo, Nth:0, Mem:bar, Nth:1]
36
+
37
+
# Escape sequences
38
+
/~0 -> [Mem:~]
39
+
/~1 -> [Mem:/]
40
+
/~0~1 -> [Mem:~/]
41
+
/~1~0 -> [Mem:/~]
42
+
/~01 -> [Mem:~1]
43
+
/a~0b~1c -> [Mem:a~b/c]
44
+
45
+
# Empty member names
46
+
// -> [Mem:, Mem:]
47
+
/// -> [Mem:, Mem:, Mem:]
48
+
/foo//bar -> [Mem:foo, Mem:, Mem:bar]
49
+
50
+
# Special characters (not needing escaping)
51
+
/hello world -> [Mem:hello world]
52
+
/with spaces -> [Mem:with spaces]
53
+
/unicode-\u00e9 -> [Mem:unicode-\u00e9]
+12
test/data/rfc6901_example.json
+12
test/data/rfc6901_example.json
+5
test/data/unicode.json
+5
test/data/unicode.json
+21
test/data/uri_fragment.txt
+21
test/data/uri_fragment.txt
···
···
1
+
# URI Fragment encoding/decoding tests
2
+
# Format: uri_fragment <-> pointer_string
3
+
4
+
# RFC 6901 Section 6 examples (without leading #)
5
+
<->
6
+
/foo <-> /foo
7
+
/foo/0 <-> /foo/0
8
+
/ <-> /
9
+
/a~1b <-> /a~1b
10
+
/c%25d <-> /c%d
11
+
/e%5Ef <-> /e^f
12
+
/g%7Ch <-> /g|h
13
+
/i%5Cj <-> /i\j
14
+
/k%22l <-> /k"l
15
+
/%20 <-> /
16
+
/m~0n <-> /m~0n
17
+
18
+
# Additional percent-encoding cases
19
+
/hello%20world <-> /hello world
20
+
/%2F <-> //
21
+
/%7E <-> /~
+13
test/dune
+13
test/dune
···
···
1
+
(executable
2
+
(name test_pointer)
3
+
(libraries jsont jsont_pointer yojson))
4
+
5
+
(cram
6
+
(deps test_pointer.exe
7
+
data/rfc6901_example.json
8
+
data/mutations.json
9
+
data/nested.json
10
+
data/nulls.json
11
+
data/booleans.json
12
+
data/unicode.json
13
+
data/numeric_keys.json))
+53
test/escape.t
+53
test/escape.t
···
···
1
+
Token Escaping Tests
2
+
3
+
Escape tilde:
4
+
$ ./test_pointer.exe escape "~"
5
+
~0
6
+
$ ./test_pointer.exe escape "~0"
7
+
~00
8
+
$ ./test_pointer.exe escape "~1"
9
+
~01
10
+
11
+
Escape slash:
12
+
$ ./test_pointer.exe escape "/"
13
+
~1
14
+
$ ./test_pointer.exe escape "a/b"
15
+
a~1b
16
+
$ ./test_pointer.exe escape "/foo/bar"
17
+
~1foo~1bar
18
+
19
+
Combined:
20
+
$ ./test_pointer.exe escape "~/"
21
+
~0~1
22
+
$ ./test_pointer.exe escape "/~"
23
+
~1~0
24
+
$ ./test_pointer.exe escape "a~b/c"
25
+
a~0b~1c
26
+
27
+
No escaping needed:
28
+
$ ./test_pointer.exe escape "foo"
29
+
foo
30
+
$ ./test_pointer.exe escape "hello world"
31
+
hello world
32
+
33
+
Unescape:
34
+
$ ./test_pointer.exe unescape "~0"
35
+
OK: ~
36
+
$ ./test_pointer.exe unescape "~1"
37
+
OK: /
38
+
$ ./test_pointer.exe unescape "~0~1"
39
+
OK: ~/
40
+
$ ./test_pointer.exe unescape "~1~0"
41
+
OK: /~
42
+
$ ./test_pointer.exe unescape "a~0b~1c"
43
+
OK: a~b/c
44
+
$ ./test_pointer.exe unescape "foo"
45
+
OK: foo
46
+
47
+
Unescape errors:
48
+
$ ./test_pointer.exe unescape "~"
49
+
ERROR: Invalid JSON Pointer: incomplete escape sequence at end
50
+
$ ./test_pointer.exe unescape "~2"
51
+
ERROR: Invalid JSON Pointer: invalid escape sequence ~2
52
+
$ ./test_pointer.exe unescape "foo~"
53
+
ERROR: Invalid JSON Pointer: incomplete escape sequence at end
+134
test/eval.t
+134
test/eval.t
···
···
1
+
JSON Pointer Evaluation Tests (RFC 6901 Section 5)
2
+
3
+
Using the RFC 6901 example document:
4
+
$ cat data/rfc6901_example.json
5
+
{
6
+
"foo": ["bar", "baz"],
7
+
"": 0,
8
+
"a/b": 1,
9
+
"c%d": 2,
10
+
"e^f": 3,
11
+
"g|h": 4,
12
+
"i\\j": 5,
13
+
"k\"l": 6,
14
+
" ": 7,
15
+
"m~n": 8
16
+
}
17
+
18
+
Root pointer returns whole document:
19
+
$ ./test_pointer.exe eval data/rfc6901_example.json ""
20
+
OK: {"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8}
21
+
22
+
RFC 6901 examples:
23
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo"
24
+
OK: ["bar","baz"]
25
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0"
26
+
OK: "bar"
27
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/1"
28
+
OK: "baz"
29
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/"
30
+
OK: 0
31
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/a~1b"
32
+
OK: 1
33
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/c%d"
34
+
OK: 2
35
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/e^f"
36
+
OK: 3
37
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/g|h"
38
+
OK: 4
39
+
$ ./test_pointer.exe eval data/rfc6901_example.json '/i\j'
40
+
OK: 5
41
+
$ ./test_pointer.exe eval data/rfc6901_example.json '/k"l'
42
+
OK: 6
43
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/ "
44
+
OK: 7
45
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/m~0n"
46
+
OK: 8
47
+
48
+
Error: nonexistent member:
49
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/nonexistent"
50
+
ERROR: JSON Pointer: member 'nonexistent' not found
51
+
52
+
Error: index out of bounds:
53
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/2"
54
+
ERROR: JSON Pointer: index 2 out of bounds (array has 2 elements)
55
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/99"
56
+
ERROR: JSON Pointer: index 99 out of bounds (array has 2 elements)
57
+
58
+
Error: invalid array index (not a valid integer):
59
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/bar"
60
+
ERROR: JSON Pointer: invalid array index 'bar'
61
+
62
+
Error: end marker not allowed in get:
63
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-"
64
+
ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element
65
+
66
+
Error: navigating through primitive (string):
67
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0"
68
+
ERROR: JSON Pointer: cannot index into string with '0'
69
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/bar"
70
+
ERROR: JSON Pointer: cannot index into string with 'bar'
71
+
72
+
Nested evaluation with deep nesting:
73
+
$ cat data/nested.json
74
+
{
75
+
"a": {
76
+
"b": {
77
+
"c": {
78
+
"d": "deep value"
79
+
}
80
+
}
81
+
},
82
+
"arr": [[1, 2], [3, 4]],
83
+
"mixed": { "list": [{"x": 1}, {"y": 2}] }
84
+
}
85
+
$ ./test_pointer.exe eval data/nested.json "/a/b/c/d"
86
+
OK: "deep value"
87
+
$ ./test_pointer.exe eval data/nested.json "/arr/0/1"
88
+
OK: 2
89
+
$ ./test_pointer.exe eval data/nested.json "/arr/1/0"
90
+
OK: 3
91
+
$ ./test_pointer.exe eval data/nested.json "/mixed/list/0/x"
92
+
OK: 1
93
+
$ ./test_pointer.exe eval data/nested.json "/mixed/list/1/y"
94
+
OK: 2
95
+
96
+
Null value handling:
97
+
$ cat data/nulls.json
98
+
{
99
+
"null": null,
100
+
"nested": { "null": null }
101
+
}
102
+
$ ./test_pointer.exe eval data/nulls.json "/null"
103
+
OK: null
104
+
$ ./test_pointer.exe eval data/nulls.json "/nested/null"
105
+
OK: null
106
+
107
+
Boolean values:
108
+
$ cat data/booleans.json
109
+
{
110
+
"true": true,
111
+
"false": false
112
+
}
113
+
$ ./test_pointer.exe eval data/booleans.json "/true"
114
+
OK: true
115
+
$ ./test_pointer.exe eval data/booleans.json "/false"
116
+
OK: false
117
+
118
+
Empty key access:
119
+
$ ./test_pointer.exe eval data/rfc6901_example.json "/"
120
+
OK: 0
121
+
122
+
Unicode member access:
123
+
$ cat data/unicode.json
124
+
{
125
+
"café": "coffee",
126
+
"日本語": "japanese",
127
+
"🎉": "party"
128
+
}
129
+
$ ./test_pointer.exe eval data/unicode.json "/café"
130
+
OK: "coffee"
131
+
$ ./test_pointer.exe eval data/unicode.json "/日本語"
132
+
OK: "japanese"
133
+
$ ./test_pointer.exe eval data/unicode.json "/🎉"
134
+
OK: "party"
+167
test/mutations.t
+167
test/mutations.t
···
···
1
+
JSON Pointer Mutation Tests (RFC 6902 JSON Patch operations)
2
+
3
+
Add to object:
4
+
$ ./test_pointer.exe add '{"foo":"bar"}' '/baz' '"qux"'
5
+
{"foo":"bar","baz":"qux"}
6
+
7
+
Add to array (insert before):
8
+
$ ./test_pointer.exe add '{"foo":["bar","baz"]}' '/foo/1' '"qux"'
9
+
{"foo":["bar","qux","baz"]}
10
+
11
+
Add to array at beginning:
12
+
$ ./test_pointer.exe add '{"foo":["bar","baz"]}' '/foo/0' '"qux"'
13
+
{"foo":["qux","bar","baz"]}
14
+
15
+
Add to array at end using -:
16
+
$ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/-' '"qux"'
17
+
{"foo":["bar","qux"]}
18
+
19
+
Add replaces existing member:
20
+
$ ./test_pointer.exe add '{"foo":"bar"}' '/foo' '"baz"'
21
+
{"foo":"baz"}
22
+
23
+
Remove from object:
24
+
$ ./test_pointer.exe remove '{"foo":"bar","baz":"qux"}' '/baz'
25
+
{"foo":"bar"}
26
+
27
+
Remove from array:
28
+
$ ./test_pointer.exe remove '{"foo":["bar","qux","baz"]}' '/foo/1'
29
+
{"foo":["bar","baz"]}
30
+
31
+
Remove first element:
32
+
$ ./test_pointer.exe remove '{"foo":["bar","baz"]}' '/foo/0'
33
+
{"foo":["baz"]}
34
+
35
+
Replace in object:
36
+
$ ./test_pointer.exe replace '{"foo":"bar"}' '/foo' '"baz"'
37
+
{"foo":"baz"}
38
+
39
+
Replace in array:
40
+
$ ./test_pointer.exe replace '{"foo":["bar","baz"]}' '/foo/0' '"qux"'
41
+
{"foo":["qux","baz"]}
42
+
43
+
Move within object:
44
+
$ ./test_pointer.exe move '{"foo":{"bar":"baz"},"qux":{}}' '/foo/bar' '/qux/thud'
45
+
{"foo":{},"qux":{"thud":"baz"}}
46
+
47
+
Move within array:
48
+
$ ./test_pointer.exe move '{"foo":["a","b","c","d"]}' '/foo/1' '/foo/3'
49
+
{"foo":["a","c","d","b"]}
50
+
51
+
Copy within object:
52
+
$ ./test_pointer.exe copy '{"foo":{"bar":"baz"}}' '/foo/bar' '/foo/qux'
53
+
{"foo":{"bar":"baz","qux":"baz"}}
54
+
55
+
Copy to new location:
56
+
$ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/baz'
57
+
{"foo":"bar","baz":"bar"}
58
+
59
+
Test equality (true):
60
+
$ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"bar"'
61
+
true
62
+
63
+
Test equality (false):
64
+
$ ./test_pointer.exe test '{"foo":"bar"}' '/foo' '"baz"'
65
+
false
66
+
67
+
Test array equality:
68
+
$ ./test_pointer.exe test '{"foo":["bar","baz"]}' '/foo' '["bar","baz"]'
69
+
true
70
+
71
+
Test nonexistent path returns false:
72
+
$ ./test_pointer.exe test '{"foo":"bar"}' '/baz' '"qux"'
73
+
false
74
+
75
+
Error: remove root:
76
+
$ ./test_pointer.exe remove '{"foo":"bar"}' ''
77
+
ERROR: JSON Pointer: cannot remove root document
78
+
79
+
Error: remove nonexistent:
80
+
$ ./test_pointer.exe remove '{"foo":"bar"}' '/baz'
81
+
ERROR: JSON Pointer: member 'baz' not found for remove
82
+
83
+
Error: replace nonexistent:
84
+
$ ./test_pointer.exe replace '{"foo":"bar"}' '/baz' '"qux"'
85
+
ERROR: JSON Pointer: member 'baz' not found
86
+
87
+
Error: add to out of bounds index:
88
+
$ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/5' '"qux"'
89
+
ERROR: JSON Pointer: index 5 out of bounds for add (array has 1 elements)
90
+
91
+
Add nested path (parent must exist):
92
+
$ ./test_pointer.exe add '{"foo":{}}' '/foo/bar' '"baz"'
93
+
{"foo":{"bar":"baz"}}
94
+
95
+
Add to root-level array:
96
+
$ ./test_pointer.exe add '["a","b"]' '/1' '"x"'
97
+
["a","x","b"]
98
+
$ ./test_pointer.exe add '["a","b"]' '/-' '"c"'
99
+
["a","b","c"]
100
+
$ ./test_pointer.exe add '["a","b"]' '/0' '"x"'
101
+
["x","a","b"]
102
+
103
+
Replace root document:
104
+
$ ./test_pointer.exe replace '{"foo":"bar"}' '' '"new root"'
105
+
"new root"
106
+
$ ./test_pointer.exe replace '["a","b"]' '' '{"replaced":true}'
107
+
{"replaced":true}
108
+
109
+
Move from array to object:
110
+
$ ./test_pointer.exe move '{"arr":["x","y"],"obj":{}}' '/arr/0' '/obj/item'
111
+
{"arr":["y"],"obj":{"item":"x"}}
112
+
113
+
Copy array element:
114
+
$ ./test_pointer.exe copy '{"arr":["x","y"]}' '/arr/0' '/arr/-'
115
+
{"arr":["x","y","x"]}
116
+
117
+
Test with numeric string member names (RFC 6901: context determines interpretation):
118
+
$ ./test_pointer.exe add '{"0":"zero","1":"one"}' '/2' '"two"'
119
+
{"0":"zero","1":"one","2":"two"}
120
+
$ ./test_pointer.exe eval data/numeric_keys.json "/0"
121
+
OK: "zero"
122
+
123
+
Special characters in member names:
124
+
$ ./test_pointer.exe add '{}' '/a~1b' '"slash"'
125
+
{"a/b":"slash"}
126
+
$ ./test_pointer.exe add '{}' '/m~0n' '"tilde"'
127
+
{"m~n":"tilde"}
128
+
$ ./test_pointer.exe replace '{"a/b":"old"}' '/a~1b' '"new"'
129
+
{"a/b":"new"}
130
+
131
+
Deep nested mutations:
132
+
$ ./test_pointer.exe add '{"a":{"b":{"c":{}}}}' '/a/b/c/d' '"deep"'
133
+
{"a":{"b":{"c":{"d":"deep"}}}}
134
+
$ ./test_pointer.exe remove '{"a":{"b":{"c":{"d":"deep"}}}}' '/a/b/c/d'
135
+
{"a":{"b":{"c":{}}}}
136
+
137
+
Error: remove from nonexistent array index:
138
+
$ ./test_pointer.exe remove '{"foo":["bar"]}' '/foo/5'
139
+
ERROR: JSON Pointer: index 5 out of bounds for remove
140
+
141
+
Error: move to descendant of source (path doesn't exist after removal):
142
+
$ ./test_pointer.exe move '{"a":{"b":"c"}}' '/a' '/a/b'
143
+
ERROR: JSON Pointer: member 'a' not found
144
+
145
+
Copy to same location (no-op essentially):
146
+
$ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/foo'
147
+
{"foo":"bar"}
148
+
149
+
Test value equality with different types:
150
+
$ ./test_pointer.exe test '{"n":1}' '/n' '1'
151
+
true
152
+
$ ./test_pointer.exe test '{"n":1.0}' '/n' '1'
153
+
true
154
+
$ ./test_pointer.exe test '{"n":"1"}' '/n' '1'
155
+
false
156
+
$ ./test_pointer.exe test '{"b":true}' '/b' 'true'
157
+
true
158
+
$ ./test_pointer.exe test '{"b":false}' '/b' 'false'
159
+
true
160
+
$ ./test_pointer.exe test '{"n":null}' '/n' 'null'
161
+
true
162
+
163
+
Test deep equality:
164
+
$ ./test_pointer.exe test '{"obj":{"a":1,"b":2}}' '/obj' '{"a":1,"b":2}'
165
+
true
166
+
$ ./test_pointer.exe test '{"arr":[1,2,3]}' '/arr' '[1,2,3]'
167
+
true
+135
test/parse.t
+135
test/parse.t
···
···
1
+
JSON Pointer Parsing Tests (RFC 6901)
2
+
3
+
Root pointer (empty string):
4
+
$ ./test_pointer.exe parse ""
5
+
OK: []
6
+
7
+
RFC 6901 Section 5 examples:
8
+
$ ./test_pointer.exe parse "/foo"
9
+
OK: [Mem:foo]
10
+
$ ./test_pointer.exe parse "/foo/0"
11
+
OK: [Mem:foo, Nth:0]
12
+
$ ./test_pointer.exe parse "/"
13
+
OK: [Mem:]
14
+
$ ./test_pointer.exe parse "/a~1b"
15
+
OK: [Mem:a/b]
16
+
$ ./test_pointer.exe parse "/c%d"
17
+
OK: [Mem:c%d]
18
+
$ ./test_pointer.exe parse "/e^f"
19
+
OK: [Mem:e^f]
20
+
$ ./test_pointer.exe parse "/g|h"
21
+
OK: [Mem:g|h]
22
+
$ ./test_pointer.exe parse '/i\j'
23
+
OK: [Mem:i\j]
24
+
$ ./test_pointer.exe parse '/k"l'
25
+
OK: [Mem:k"l]
26
+
$ ./test_pointer.exe parse "/ "
27
+
OK: [Mem: ]
28
+
$ ./test_pointer.exe parse "/m~0n"
29
+
OK: [Mem:m~n]
30
+
31
+
Array indices:
32
+
$ ./test_pointer.exe parse "/0"
33
+
OK: [Nth:0]
34
+
$ ./test_pointer.exe parse "/1"
35
+
OK: [Nth:1]
36
+
$ ./test_pointer.exe parse "/10"
37
+
OK: [Nth:10]
38
+
$ ./test_pointer.exe parse "/123"
39
+
OK: [Nth:123]
40
+
41
+
End-of-array marker:
42
+
$ ./test_pointer.exe parse "/-"
43
+
OK: [End]
44
+
$ ./test_pointer.exe parse "/foo/-"
45
+
OK: [Mem:foo, End]
46
+
47
+
Multiple levels:
48
+
$ ./test_pointer.exe parse "/a/b/c"
49
+
OK: [Mem:a, Mem:b, Mem:c]
50
+
$ ./test_pointer.exe parse "/0/1/2"
51
+
OK: [Nth:0, Nth:1, Nth:2]
52
+
$ ./test_pointer.exe parse "/foo/0/bar/1"
53
+
OK: [Mem:foo, Nth:0, Mem:bar, Nth:1]
54
+
55
+
Escape sequences:
56
+
$ ./test_pointer.exe parse "/~0"
57
+
OK: [Mem:~]
58
+
$ ./test_pointer.exe parse "/~1"
59
+
OK: [Mem:/]
60
+
$ ./test_pointer.exe parse "/~0~1"
61
+
OK: [Mem:~/]
62
+
$ ./test_pointer.exe parse "/~1~0"
63
+
OK: [Mem:/~]
64
+
$ ./test_pointer.exe parse "/~01"
65
+
OK: [Mem:~1]
66
+
67
+
Empty member names:
68
+
$ ./test_pointer.exe parse "//"
69
+
OK: [Mem:, Mem:]
70
+
$ ./test_pointer.exe parse "///"
71
+
OK: [Mem:, Mem:, Mem:]
72
+
73
+
Invalid: must start with /
74
+
$ ./test_pointer.exe parse "foo"
75
+
ERROR: Invalid JSON Pointer: must be empty or start with '/': foo
76
+
$ ./test_pointer.exe parse "a/b"
77
+
ERROR: Invalid JSON Pointer: must be empty or start with '/': a/b
78
+
79
+
Invalid: incomplete escape
80
+
$ ./test_pointer.exe parse "/~"
81
+
ERROR: Invalid JSON Pointer: incomplete escape sequence at end
82
+
$ ./test_pointer.exe parse "/foo~"
83
+
ERROR: Invalid JSON Pointer: incomplete escape sequence at end
84
+
85
+
Invalid: bad escape sequence
86
+
$ ./test_pointer.exe parse "/~2"
87
+
ERROR: Invalid JSON Pointer: invalid escape sequence ~2
88
+
$ ./test_pointer.exe parse "/~a"
89
+
ERROR: Invalid JSON Pointer: invalid escape sequence ~a
90
+
91
+
Leading zeros - valid as tokens, become member names (invalid as array indices at eval time):
92
+
$ ./test_pointer.exe parse "/00"
93
+
OK: [Mem:00]
94
+
$ ./test_pointer.exe parse "/01"
95
+
OK: [Mem:01]
96
+
$ ./test_pointer.exe parse "/007"
97
+
OK: [Mem:007]
98
+
99
+
RFC 6901 Section 4: ~01 decodes to ~1, not / (order matters):
100
+
$ ./test_pointer.exe parse "/~01"
101
+
OK: [Mem:~1]
102
+
$ ./test_pointer.exe parse "/~10"
103
+
OK: [Mem:/0]
104
+
105
+
Unicode characters (RFC 6901 specifies JSON Pointer is Unicode):
106
+
$ ./test_pointer.exe parse "/café"
107
+
OK: [Mem:café]
108
+
$ ./test_pointer.exe parse "/日本語"
109
+
OK: [Mem:日本語]
110
+
$ ./test_pointer.exe parse "/emoji🎉"
111
+
OK: [Mem:emoji🎉]
112
+
113
+
Numeric member names (should be parsed as indices when valid):
114
+
$ ./test_pointer.exe parse "/0"
115
+
OK: [Nth:0]
116
+
$ ./test_pointer.exe parse "/-1"
117
+
OK: [Mem:-1]
118
+
$ ./test_pointer.exe parse "/+1"
119
+
OK: [Mem:+1]
120
+
121
+
Very deep paths:
122
+
$ ./test_pointer.exe parse "/a/b/c/d/e/f/g/h/i/j"
123
+
OK: [Mem:a, Mem:b, Mem:c, Mem:d, Mem:e, Mem:f, Mem:g, Mem:h, Mem:i, Mem:j]
124
+
125
+
Complex escape sequences:
126
+
$ ./test_pointer.exe parse "/~0~0"
127
+
OK: [Mem:~~]
128
+
$ ./test_pointer.exe parse "/~1~1"
129
+
OK: [Mem://]
130
+
$ ./test_pointer.exe parse "/a~0b~1c~0d~1e"
131
+
OK: [Mem:a~b/c~d/e]
132
+
133
+
Invalid: tilde at end of path:
134
+
$ ./test_pointer.exe parse "/foo/bar~"
135
+
ERROR: Invalid JSON Pointer: incomplete escape sequence at end
+234
test/test_pointer.ml
+234
test/test_pointer.ml
···
···
1
+
(* Test runner for jsont_pointer *)
2
+
3
+
let read_file path =
4
+
let ic = open_in path in
5
+
let n = in_channel_length ic in
6
+
let s = really_input_string ic n in
7
+
close_in ic;
8
+
s
9
+
10
+
(* Convert Yojson.Safe.t to Jsont.json *)
11
+
let rec yojson_to_jsont (y : Yojson.Safe.t) : Jsont.json =
12
+
match y with
13
+
| `Null -> Jsont.Json.null ()
14
+
| `Bool b -> Jsont.Json.bool b
15
+
| `Int i -> Jsont.Json.number (float_of_int i)
16
+
| `Float f -> Jsont.Json.number f
17
+
| `String s -> Jsont.Json.string s
18
+
| `List l -> Jsont.Json.list (List.map yojson_to_jsont l)
19
+
| `Assoc pairs ->
20
+
let members = List.map (fun (k, v) ->
21
+
Jsont.Json.mem (Jsont.Json.name k) (yojson_to_jsont v)
22
+
) pairs in
23
+
Jsont.Json.object' members
24
+
| `Intlit s -> Jsont.Json.number (float_of_string s)
25
+
26
+
(* Convert Jsont.json to Yojson.Safe.t for output *)
27
+
let rec jsont_to_yojson (j : Jsont.json) : Yojson.Safe.t =
28
+
match j with
29
+
| Jsont.Null _ -> `Null
30
+
| Jsont.Bool (b, _) -> `Bool b
31
+
| Jsont.Number (f, _) ->
32
+
if Float.is_integer f && Float.abs f < 2e15 then
33
+
`Int (int_of_float f)
34
+
else
35
+
`Float f
36
+
| Jsont.String (s, _) -> `String s
37
+
| Jsont.Array (l, _) -> `List (List.map jsont_to_yojson l)
38
+
| Jsont.Object (members, _) ->
39
+
`Assoc (List.map (fun ((name, _), v) ->
40
+
(name, jsont_to_yojson v)
41
+
) members)
42
+
43
+
let parse_json s =
44
+
yojson_to_jsont (Yojson.Safe.from_string s)
45
+
46
+
let json_to_string json =
47
+
Yojson.Safe.to_string (jsont_to_yojson json)
48
+
49
+
(* Test: parse pointer and print indices *)
50
+
let test_parse pointer_str =
51
+
try
52
+
let p = Jsont_pointer.of_string pointer_str in
53
+
let indices = Jsont_pointer.indices p in
54
+
let index_strs = List.map (fun idx ->
55
+
match idx with
56
+
| Jsont_pointer.Index.Mem s -> Printf.sprintf "Mem:%s" s
57
+
| Jsont_pointer.Index.Nth n -> Printf.sprintf "Nth:%d" n
58
+
| Jsont_pointer.Index.End -> "End"
59
+
) indices in
60
+
Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
61
+
with Jsont.Error e ->
62
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
63
+
64
+
(* Test: roundtrip pointer string *)
65
+
let test_roundtrip pointer_str =
66
+
try
67
+
let p = Jsont_pointer.of_string pointer_str in
68
+
let s = Jsont_pointer.to_string p in
69
+
if s = pointer_str then
70
+
Printf.printf "OK: %s\n" s
71
+
else
72
+
Printf.printf "MISMATCH: input=%s output=%s\n" pointer_str s
73
+
with Jsont.Error e ->
74
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
75
+
76
+
(* Test: evaluate pointer against JSON *)
77
+
let test_eval json_path pointer_str =
78
+
try
79
+
let json = parse_json (read_file json_path) in
80
+
let p = Jsont_pointer.of_string pointer_str in
81
+
let result = Jsont_pointer.get p json in
82
+
Printf.printf "OK: %s\n" (json_to_string result)
83
+
with
84
+
| Jsont.Error e ->
85
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
86
+
| Failure e ->
87
+
Printf.printf "FAIL: %s\n" e
88
+
89
+
(* Test: escape token *)
90
+
let test_escape token =
91
+
let escaped = Jsont_pointer.Token.escape token in
92
+
Printf.printf "%s\n" escaped
93
+
94
+
(* Test: unescape token *)
95
+
let test_unescape token =
96
+
try
97
+
let unescaped = Jsont_pointer.Token.unescape token in
98
+
Printf.printf "OK: %s\n" unescaped
99
+
with Jsont.Error e ->
100
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
101
+
102
+
(* Test: URI fragment roundtrip *)
103
+
let test_uri_fragment pointer_str =
104
+
try
105
+
let p = Jsont_pointer.of_string pointer_str in
106
+
let frag = Jsont_pointer.to_uri_fragment p in
107
+
let p2 = Jsont_pointer.of_uri_fragment frag in
108
+
let s2 = Jsont_pointer.to_string p2 in
109
+
if s2 = pointer_str then
110
+
Printf.printf "OK: %s -> %s\n" pointer_str frag
111
+
else
112
+
Printf.printf "MISMATCH: %s -> %s -> %s\n" pointer_str frag s2
113
+
with Jsont.Error e ->
114
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
115
+
116
+
(* Test: add operation *)
117
+
let test_add json_str pointer_str value_str =
118
+
try
119
+
let json = parse_json json_str in
120
+
let p = Jsont_pointer.of_string pointer_str in
121
+
let value = parse_json value_str in
122
+
let result = Jsont_pointer.add p json ~value in
123
+
Printf.printf "%s\n" (json_to_string result)
124
+
with Jsont.Error e ->
125
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
126
+
127
+
(* Test: remove operation *)
128
+
let test_remove json_str pointer_str =
129
+
try
130
+
let json = parse_json json_str in
131
+
let p = Jsont_pointer.of_string pointer_str in
132
+
let result = Jsont_pointer.remove p json in
133
+
Printf.printf "%s\n" (json_to_string result)
134
+
with Jsont.Error e ->
135
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
136
+
137
+
(* Test: replace operation *)
138
+
let test_replace json_str pointer_str value_str =
139
+
try
140
+
let json = parse_json json_str in
141
+
let p = Jsont_pointer.of_string pointer_str in
142
+
let value = parse_json value_str in
143
+
let result = Jsont_pointer.replace p json ~value in
144
+
Printf.printf "%s\n" (json_to_string result)
145
+
with Jsont.Error e ->
146
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
147
+
148
+
(* Test: move operation *)
149
+
let test_move json_str from_str path_str =
150
+
try
151
+
let json = parse_json json_str in
152
+
let from = Jsont_pointer.of_string from_str in
153
+
let path = Jsont_pointer.of_string path_str in
154
+
let result = Jsont_pointer.move ~from ~path json in
155
+
Printf.printf "%s\n" (json_to_string result)
156
+
with Jsont.Error e ->
157
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
158
+
159
+
(* Test: copy operation *)
160
+
let test_copy json_str from_str path_str =
161
+
try
162
+
let json = parse_json json_str in
163
+
let from = Jsont_pointer.of_string from_str in
164
+
let path = Jsont_pointer.of_string path_str in
165
+
let result = Jsont_pointer.copy ~from ~path json in
166
+
Printf.printf "%s\n" (json_to_string result)
167
+
with Jsont.Error e ->
168
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
169
+
170
+
(* Test: test operation *)
171
+
let test_test json_str pointer_str expected_str =
172
+
try
173
+
let json = parse_json json_str in
174
+
let p = Jsont_pointer.of_string pointer_str in
175
+
let expected = parse_json expected_str in
176
+
let result = Jsont_pointer.test p json ~expected in
177
+
Printf.printf "%b\n" result
178
+
with Jsont.Error e ->
179
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
180
+
181
+
(* Test: has operation (checks if pointer exists) *)
182
+
let test_has json_str pointer_str =
183
+
try
184
+
let json = parse_json json_str in
185
+
let p = Jsont_pointer.of_string pointer_str in
186
+
let result = Jsont_pointer.find p json in
187
+
Printf.printf "%b\n" (Option.is_some result)
188
+
with Jsont.Error e ->
189
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
190
+
191
+
let () =
192
+
match Array.to_list Sys.argv with
193
+
| _ :: "parse" :: pointer :: _ ->
194
+
test_parse pointer
195
+
| _ :: "roundtrip" :: pointer :: _ ->
196
+
test_roundtrip pointer
197
+
| _ :: "eval" :: json_path :: pointer :: _ ->
198
+
test_eval json_path pointer
199
+
| _ :: "escape" :: token :: _ ->
200
+
test_escape token
201
+
| _ :: "unescape" :: token :: _ ->
202
+
test_unescape token
203
+
| _ :: "uri-fragment" :: pointer :: _ ->
204
+
test_uri_fragment pointer
205
+
| _ :: "add" :: json :: pointer :: value :: _ ->
206
+
test_add json pointer value
207
+
| _ :: "remove" :: json :: pointer :: _ ->
208
+
test_remove json pointer
209
+
| _ :: "replace" :: json :: pointer :: value :: _ ->
210
+
test_replace json pointer value
211
+
| _ :: "move" :: json :: from :: path :: _ ->
212
+
test_move json from path
213
+
| _ :: "copy" :: json :: from :: path :: _ ->
214
+
test_copy json from path
215
+
| _ :: "test" :: json :: pointer :: expected :: _ ->
216
+
test_test json pointer expected
217
+
| _ :: "has" :: json :: pointer :: _ ->
218
+
test_has json pointer
219
+
| _ ->
220
+
Printf.printf "Usage:\n";
221
+
Printf.printf " test_pointer parse <pointer>\n";
222
+
Printf.printf " test_pointer roundtrip <pointer>\n";
223
+
Printf.printf " test_pointer eval <json-file> <pointer>\n";
224
+
Printf.printf " test_pointer escape <token>\n";
225
+
Printf.printf " test_pointer unescape <token>\n";
226
+
Printf.printf " test_pointer uri-fragment <pointer>\n";
227
+
Printf.printf " test_pointer add <json> <pointer> <value>\n";
228
+
Printf.printf " test_pointer remove <json> <pointer>\n";
229
+
Printf.printf " test_pointer replace <json> <pointer> <value>\n";
230
+
Printf.printf " test_pointer move <json> <from> <path>\n";
231
+
Printf.printf " test_pointer copy <json> <from> <path>\n";
232
+
Printf.printf " test_pointer test <json> <pointer> <expected>\n";
233
+
Printf.printf " test_pointer has <json> <pointer>\n";
234
+
exit 1
+77
test/uri_fragment.t
+77
test/uri_fragment.t
···
···
1
+
URI Fragment Encoding Tests (RFC 6901 Section 6)
2
+
3
+
Roundtrip through URI fragment encoding:
4
+
$ ./test_pointer.exe uri-fragment ""
5
+
OK: ->
6
+
$ ./test_pointer.exe uri-fragment "/foo"
7
+
OK: /foo -> /foo
8
+
$ ./test_pointer.exe uri-fragment "/foo/0"
9
+
OK: /foo/0 -> /foo/0
10
+
$ ./test_pointer.exe uri-fragment "/"
11
+
OK: / -> /
12
+
$ ./test_pointer.exe uri-fragment "/a~1b"
13
+
OK: /a~1b -> /a~1b
14
+
$ ./test_pointer.exe uri-fragment "/m~0n"
15
+
OK: /m~0n -> /m~0n
16
+
17
+
Characters requiring percent-encoding:
18
+
$ ./test_pointer.exe uri-fragment "/c%d"
19
+
OK: /c%d -> /c%25d
20
+
$ ./test_pointer.exe uri-fragment "/e^f"
21
+
OK: /e^f -> /e%5Ef
22
+
$ ./test_pointer.exe uri-fragment "/g|h"
23
+
OK: /g|h -> /g%7Ch
24
+
$ ./test_pointer.exe uri-fragment '/i\j'
25
+
OK: /i\j -> /i%5Cj
26
+
$ ./test_pointer.exe uri-fragment '/k"l'
27
+
OK: /k"l -> /k%22l
28
+
$ ./test_pointer.exe uri-fragment "/ "
29
+
OK: / -> /%20
30
+
31
+
Roundtrip tests:
32
+
$ ./test_pointer.exe roundtrip ""
33
+
OK:
34
+
$ ./test_pointer.exe roundtrip "/foo"
35
+
OK: /foo
36
+
$ ./test_pointer.exe roundtrip "/foo/0"
37
+
OK: /foo/0
38
+
$ ./test_pointer.exe roundtrip "/"
39
+
OK: /
40
+
$ ./test_pointer.exe roundtrip "/a~1b"
41
+
OK: /a~1b
42
+
$ ./test_pointer.exe roundtrip "/m~0n"
43
+
OK: /m~0n
44
+
$ ./test_pointer.exe roundtrip "/-"
45
+
OK: /-
46
+
$ ./test_pointer.exe roundtrip "/a/b/c"
47
+
OK: /a/b/c
48
+
49
+
RFC 6901 Section 6 examples (URI fragment encoding):
50
+
Note: These test the full RFC 6901 Section 6 examples
51
+
$ ./test_pointer.exe uri-fragment "/c%d"
52
+
OK: /c%d -> /c%25d
53
+
$ ./test_pointer.exe uri-fragment "/e^f"
54
+
OK: /e^f -> /e%5Ef
55
+
$ ./test_pointer.exe uri-fragment "/g|h"
56
+
OK: /g|h -> /g%7Ch
57
+
$ ./test_pointer.exe uri-fragment '/i\j'
58
+
OK: /i\j -> /i%5Cj
59
+
$ ./test_pointer.exe uri-fragment '/k"l'
60
+
OK: /k"l -> /k%22l
61
+
$ ./test_pointer.exe uri-fragment "/ "
62
+
OK: / -> /%20
63
+
64
+
Unicode in URI fragments:
65
+
$ ./test_pointer.exe uri-fragment "/café"
66
+
OK: /café -> /caf%C3%A9
67
+
$ ./test_pointer.exe uri-fragment "/日本語"
68
+
OK: /日本語 -> /%E6%97%A5%E6%9C%AC%E8%AA%9E
69
+
70
+
Combined escapes (tilde escape + URI encode):
71
+
Note: %25 in result is the URI encoding of %, and 100% is treated as member name
72
+
$ ./test_pointer.exe uri-fragment "/100%"
73
+
OK: /100% -> /100%25
74
+
75
+
Multiple special chars:
76
+
$ ./test_pointer.exe uri-fragment "/a b^c|d"
77
+
OK: /a b^c|d -> /a%20b%5Ec%7Cd