Skip to content

Commit 5941e27

Browse files
committed
docs(README): provide full documentation
1 parent 62c7781 commit 5941e27

File tree

5 files changed

+220
-6
lines changed

5 files changed

+220
-6
lines changed

README.md

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
[![try on RunKit](https://img.shields.io/badge/try%20on-RunKit-brightgreen.svg?style=flat)](https://npm.runkit.com/@swaggerexpert/json-pointer)
88
[![Tidelift](https://tidelift.com/badges/package/npm/@swaggerexpert%2Fjson-pointer)](https://tidelift.com/subscription/pkg/npm-.swaggerexpert-json-pointer?utm_source=npm-swaggerexpert-json-pointer&utm_medium=referral&utm_campaign=readme)
99

10-
`@swaggerexpert/json-pointer` is a **parser**, **validator**, **escaper**, **evaluator**, **compiler** and **representer** for [RFC 6901 JavaScript Object Notation (JSON) Pointer](https://datatracker.ietf.org/doc/html/rfc6901).
10+
`@swaggerexpert/json-pointer` is a **parser**, **validator**, **escaper**, **evaluator**, **compiler** and **representer** for [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) JavaScript Object Notation (JSON) Pointer.
11+
Development of this library led to the creation of new [Errata 8314](https://www.rfc-editor.org/errata/eid8314) reported under RFC 6901, providing clarification about JSON String Representation.
1112

1213
<table>
1314
<tr>
@@ -33,6 +34,7 @@
3334
- [Evaluation](#evaluation)
3435
- [Compilation](#compilation)
3536
- [Representation](#representation)
37+
- [Errors](#errors)
3638
- [Grammar](#grammar)
3739
- [More about JSON Pointer](#more-about-json-pointer)
3840
- [License](#license)
@@ -211,6 +213,98 @@ unescape('~0foo'); // => '~foo'
211213
unescape('~1foo'); // => '/foo'
212214
```
213215

216+
#### Evaluation
217+
218+
[comment]: <> (SPDX-FileCopyrightText: Copyright &#40;c&#41; 2013 IETF Trust and the persons identified as the document authors. All rights reserved.)
219+
[comment]: <> (SPDX-License-Identifier: BSD-2-Clause)
220+
221+
Evaluation of a JSON Pointer begins with a reference to the root
222+
value of a JSON document and completes with a reference to some value
223+
within the document. Each reference token in the JSON Pointer is
224+
evaluated sequentially.
225+
226+
```js
227+
import { evaluate } from '@swaggerexpert/json-pointer';
228+
229+
const value = {
230+
"foo": ["bar", "baz"],
231+
"": 0,
232+
"a/b": 1,
233+
"c%d": 2,
234+
"e^f": 3,
235+
"g|h": 4,
236+
"i\\j": 5,
237+
"k\"l": 6,
238+
" ": 7,
239+
"m~n": 8
240+
};
241+
242+
evaluate(value, ''); // => identical to value
243+
evaluate(value, '/foo'); // => ["bar", "baz"]
244+
evaluate(value, '/foo/0'); // => "bar"
245+
evaluate(value, '/'); // => 0
246+
evaluate(value, '/a~1b'); // => 1
247+
evaluate(value, '/c%d'); // => 2
248+
evaluate(value, '/e^f'); // => 3
249+
evaluate(value, '/g|h'); // => 4
250+
evaluate(value, '/i\\j'); // => 5
251+
evaluate(value, '/k"l'); // => 6
252+
evaluate(value, '/ '); // => 7
253+
evaluate(value, '/m~0n'); // => 8
254+
255+
// neither object nor array
256+
evaluate(null, '/foo'); // => throws JSONPointerTypeError
257+
// arrays
258+
evaluate(value, '/foo/2'); // => throws JSONPointerIndexError
259+
evaluate(value, '/foo/-'); // => throws JSONPointerIndexError
260+
evaluate(value, '/foo/a'); // => throws JSONPointerIndexError
261+
// objects
262+
evaluate(value, '/bar'); // => throws JSONPointerKeyError
263+
```
264+
265+
**Strict Arrays**
266+
267+
[comment]: <> (SPDX-FileCopyrightText: Copyright &#40;c&#41; 2013 IETF Trust and the persons identified as the document authors. All rights reserved.)
268+
[comment]: <> (SPDX-License-Identifier: BSD-2-Clause)
269+
270+
By default, the evaluation is **strict**, meaning error condition will be raised if it fails to
271+
resolve a concrete value for any of the JSON pointer's reference tokens. For example, if an array
272+
is referenced with a non-numeric token, an error condition will be raised.
273+
274+
Note that the use of the `"-"` character to index an array will always
275+
result in such an error condition because by definition it refers to
276+
a nonexistent array element.
277+
278+
This spec compliant strict behavior can be disabled by setting the `strictArrays` option to `false`.
279+
280+
```js
281+
evaluate(value, '/foo/2', { strictArrays: false }); // => undefined
282+
evaluate(value, '/foo/-', { strictArrays: false }); // => undefined
283+
evaluate(value, '/foo/a', { strictArrays: false }); // => undefined
284+
```
285+
286+
**Strict Objects**
287+
288+
[comment]: <> (SPDX-FileCopyrightText: Copyright &#40;c&#41; 2013 IETF Trust and the persons identified as the document authors. All rights reserved.)
289+
[comment]: <> (SPDX-License-Identifier: BSD-2-Clause)
290+
291+
By default, the evaluation is **strict**, meaning error condition will be raised if it fails to
292+
resolve a concrete value for any of the JSON pointer's reference tokens.
293+
For example, if a token references a key that is not present in an object, an error condition will be raised.
294+
295+
This spec compliant strict behavior can be disabled by setting the `strictObjects` option to `false`.
296+
297+
```js
298+
evaluate(value, '/bar', { strictObjects: false }); // => undefined
299+
```
300+
301+
`strictObjects` options has no effect in cases where evaluation of previous
302+
reference token failed to resolve a concrete value.
303+
304+
```js
305+
evaluate(value, '/bar/baz', { strictObjects: false }); // => throw JSONPointerTypeError
306+
```
307+
214308
#### Compilation
215309

216310
Compilation is the process of transforming a list of reference tokens into a JSON Pointer.
@@ -222,6 +316,60 @@ import { compile } from '@swaggerexpert/json-pointer';
222316
compile(['~foo', 'bar']); // => '/~0foo/bar'
223317
```
224318

319+
#### Representation
320+
321+
**JSON String**
322+
323+
[comment]: <> (SPDX-FileCopyrightText: Copyright &#40;c&#41; 2013 IETF Trust and the persons identified as the document authors. All rights reserved.)
324+
[comment]: <> (SPDX-License-Identifier: BSD-2-Clause)
325+
326+
A JSON Pointer can be represented in a JSON string value. Per
327+
[RFC4627, Section 2.5](https://datatracker.ietf.org/doc/html/rfc4627#section-2.5), all instances of quotation mark `'"'` (%x22),
328+
reverse solidus `'\'` (%x5C), and control (%x00-1F) characters MUST be
329+
escaped.
330+
331+
```js
332+
import { JSONString } from '@swaggerexpert/json-pointer';
333+
334+
JSONString.to('/foo"bar'); // => '"/foo\\"bar"'
335+
JSONString.from('"/foo\\"bar"'); // => '/foo"bar'
336+
```
337+
338+
**URI Fragment Identifier**
339+
340+
[comment]: <> (SPDX-FileCopyrightText: Copyright &#40;c&#41; 2013 IETF Trust and the persons identified as the document authors. All rights reserved.)
341+
[comment]: <> (SPDX-License-Identifier: BSD-2-Clause)
342+
343+
A JSON Pointer can be represented in a URI fragment identifier by
344+
encoding it into octets using UTF-8 [RFC3629](https://datatracker.ietf.org/doc/html/rfc3629), while percent-encoding
345+
those characters not allowed by the fragment rule in [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986).
346+
347+
```js
348+
import { URIFragmentIdentifier } from '@swaggerexpert/json-pointer';
349+
350+
URIFragmentIdentifier.to('/foo"bar'); // => '#/foo%22bar'
351+
URIFragmentIdentifier.from('#/foo%22bar'); // => '/foo"bar'
352+
```
353+
354+
#### Errors
355+
356+
`@swaggerexpert/json-pointer` provides a structured error class hierarchy,
357+
enabling precise error handling across JSON Pointer operations, including evaluation, parsing, and manipulation.
358+
359+
```js
360+
import {
361+
JSONPointerError,
362+
JSONPointerParseError,
363+
JSONPointerCompileError,
364+
JSONPointerEvaluateError,
365+
JSONPointerTypeError,
366+
JSONPointerKeyError,
367+
JSONPointerIndexError
368+
} from '@swaggerexpert/json-pointer';
369+
```
370+
371+
**JSONPointerError** is the base class for all JSON Pointer errors.
372+
225373
#### Grammar
226374

227375
New grammar instance can be created in following way:

src/evaluate.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import JSONPointerTypeError from './errors/JSONPointerTypeError.js';
66
import JSONPointerIndexError from './errors/JSONPointerIndexError.js';
77
import JSONPointerKeyError from './errors/JSONPointerKeyError.js';
88

9-
const evaluate = (value, jsonPointer, { strictArrays = true, evaluator = null } = {}) => {
9+
const evaluate = (
10+
value,
11+
jsonPointer,
12+
{ strictArrays = true, strictObjects = true, evaluator = null } = {},
13+
) => {
1014
const parseOptions = typeof evaluator === 'function' ? { evaluator } : undefined;
1115
const { result, computed: referenceTokens } = parse(jsonPointer, parseOptions);
1216

@@ -30,7 +34,7 @@ const evaluate = (value, jsonPointer, { strictArrays = true, evaluator = null }
3034
}
3135
}
3236

33-
if (!testArrayIndex(referenceToken)) {
37+
if (!testArrayIndex(referenceToken) && strictArrays) {
3438
throw new JSONPointerIndexError(
3539
`Invalid array index: '${referenceToken}' (MUST be "0", or digits without a leading "0")`,
3640
);
@@ -43,7 +47,7 @@ const evaluate = (value, jsonPointer, { strictArrays = true, evaluator = null }
4347
return current[index];
4448
}
4549

46-
if (!Object.prototype.hasOwnProperty.call(current, referenceToken)) {
50+
if (!Object.prototype.hasOwnProperty.call(current, referenceToken) && strictObjects) {
4751
throw new JSONPointerKeyError(referenceToken);
4852
}
4953

src/representation/uri-fragment-identifier.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
export const to = (jsonPointer) => {
2-
return `#${encodeURI(jsonPointer)}`;
2+
const encodedFragment = [...jsonPointer]
3+
.map((char) =>
4+
/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/.test(char) ? char : encodeURIComponent(char),
5+
)
6+
.join('');
7+
8+
return `#${encodedFragment}`;
39
};
410

511
export const from = (fragment) => {
612
const rfc3986Fragment = fragment.startsWith('#') ? fragment.slice(1) : fragment;
713

814
try {
9-
return decodeURI(rfc3986Fragment);
15+
return decodeURIComponent(rfc3986Fragment);
1016
} catch {
1117
return fragment;
1218
}

test/evaluate.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
JSONPointerKeyError,
88
JSONPointerEvaluateError,
99
referenceTokenListEvaluator,
10+
JSONString,
11+
URIFragmentIdentifier,
1012
} from '../src/index.js';
1113
import unescape from '../src/unescape.js';
1214

@@ -24,6 +26,52 @@ describe('evaluate', function () {
2426
'm~n': 8,
2527
};
2628

29+
context('RCC 6901 JSON String tests', function () {
30+
const jsonStringRepEntries = [
31+
['""', data],
32+
['"/foo"', ['bar', 'baz']],
33+
['"/foo/0"', 'bar'],
34+
['"/"', 0],
35+
['"/a~1b"', 1],
36+
['"/c%d"', 2],
37+
['"/e^f"', 3],
38+
['"/g|h"', 4],
39+
['"/i\\\\j"', 5],
40+
['"/k\\"l"', 6],
41+
['"/ "', 7],
42+
['"/m~0n"', 8],
43+
];
44+
45+
jsonStringRepEntries.forEach(([jsonString, expected]) => {
46+
specify('should correctly evaluate JSON Pointer from JSON String', function () {
47+
assert.deepEqual(evaluate(data, JSONString.from(jsonString)), expected);
48+
});
49+
});
50+
});
51+
52+
context('RCC 6901 JSON String tests', function () {
53+
const jsonStringRepEntries = [
54+
['#', data],
55+
['#/foo', ['bar', 'baz']],
56+
['#/foo/0', 'bar'],
57+
['#/', 0],
58+
['#/a~1b', 1],
59+
['#/c%25d', 2],
60+
['#/e%5Ef', 3],
61+
['#/g%7Ch', 4],
62+
['#/i%5Cj', 5],
63+
['#/k%22l', 6],
64+
['#/%20', 7],
65+
['#/m~0n', 8],
66+
];
67+
68+
jsonStringRepEntries.forEach(([fragment, expected]) => {
69+
specify('should correctly evaluate JSON Pointer from JSON String', function () {
70+
assert.deepEqual(evaluate(data, URIFragmentIdentifier.from(fragment)), expected);
71+
});
72+
});
73+
});
74+
2775
context('valid JSON Pointers', function () {
2876
specify('should return entire document for ""', function () {
2977
assert.deepEqual(evaluate(data, ''), data);

test/representation/uri-fragment-identifier.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ describe('URI Fragment Identifier (to/from)', function () {
1919
assert.strictEqual(URIFragmentIdentifier.to(''), '#');
2020
});
2121

22+
specify('should percent encode disallowed character', function () {
23+
assert.strictEqual(URIFragmentIdentifier.to('/foo?#'), '#/foo?%23');
24+
});
25+
2226
specify('should correctly convert JSON Pointer to URI Fragment (RFC 6901)', function () {
2327
assert.strictEqual(URIFragmentIdentifier.to(''), '#');
2428
assert.strictEqual(URIFragmentIdentifier.to('/foo'), '#/foo');
@@ -60,6 +64,10 @@ describe('URI Fragment Identifier (to/from)', function () {
6064
assert.strictEqual(URIFragmentIdentifier.from('%E3%81%'), '%E3%81%'); // Malformed UTF-8 sequence
6165
});
6266

67+
specify('should percent decode disallowed percent encoded character', function () {
68+
assert.strictEqual(URIFragmentIdentifier.from('#/foo?%23'), '/foo?#');
69+
});
70+
6371
specify(
6472
'should correctly convert between JSON Pointer and URI Fragment (RFC 6901)',
6573
function () {

0 commit comments

Comments
 (0)