Skip to content

Commit 4e16890

Browse files
authored
feat(evaluate): add support for Map/Set realm (#15)
Refs #13
1 parent 91faf79 commit 4e16890

File tree

5 files changed

+278
-21
lines changed

5 files changed

+278
-21
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,24 @@ import JSONEvaluationRealm from '@swaggerexpert/json-pointer/evaluate/realms/jso
340340
evaluate({ a: 'b' }, '/a', { realm: new JSONEvaluationRealm() }); // => 'b'
341341
```
342342

343+
###### Map/Set Evaluation Realm
344+
345+
The Map/Set realm extends JSON Pointer evaluation to support [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instances,
346+
allowing structured traversal and access beyond traditional JavaScript objects and arrays.
347+
Map/Set realm is represented by the `MapSetEvaluationRealm` class.
348+
349+
350+
```js
351+
import { evaluate } from '@swaggerexpert/json-pointer';
352+
import MapSetEvaluationRealm from '@swaggerexpert/json-pointer/evaluate/realms/map-set';
353+
354+
const map = new Map([
355+
['a', new Set(['b', 'c'])]
356+
]);
357+
358+
evaluate(map, '/a/1', { realm: new MapSetEvaluationRealm() }); // => 'c'
359+
```
360+
343361
###### Custom Evaluation Realms
344362

345363
The evaluation is designed to support **custom evaluation realms**,

src/evaluate/realms/map-set.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import EvaluationRealm from './index.js';
2+
3+
class MapSetEvaluationRealm extends EvaluationRealm {
4+
name = 'map-set';
5+
6+
isArray(node) {
7+
return node instanceof Set || Object.prototype.toString.call(node) === '[object Set]';
8+
}
9+
10+
isObject(node) {
11+
return node instanceof Map || Object.prototype.toString.call(node) === '[object Map]';
12+
}
13+
14+
sizeOf(node) {
15+
if (this.isArray(node) || this.isObject(node)) {
16+
return node.size;
17+
}
18+
return 0;
19+
}
20+
21+
has(node, referenceToken) {
22+
if (this.isArray(node)) {
23+
return Number(referenceToken) < this.sizeOf(node);
24+
}
25+
if (this.isObject(node)) {
26+
return node.has(referenceToken);
27+
}
28+
return false;
29+
}
30+
31+
evaluate(node, referenceToken) {
32+
if (this.isArray(node)) {
33+
return [...node][Number(referenceToken)];
34+
}
35+
return node.get(referenceToken);
36+
}
37+
}
38+
39+
export default MapSetEvaluationRealm;
Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import {
77
JSONPointerKeyError,
88
JSONPointerEvaluateError,
99
referenceTokenListEvaluator,
10-
JSONString,
1110
URIFragmentIdentifier,
12-
} from '../src/index.js';
13-
import JSONEvaluationRealm from '../src/evaluate/realms/json.js';
11+
} from '../../src/index.js';
12+
import JSONEvaluationRealm from '../../src/evaluate/realms/json.js';
1413

1514
describe('evaluate', function () {
1615
const data = {
@@ -26,31 +25,31 @@ describe('evaluate', function () {
2625
'm~n': 8,
2726
};
2827

29-
context('RCC 6901 JSON String tests', function () {
28+
context('RFC 6901 JSON String tests', function () {
3029
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],
30+
['', data],
31+
['/foo', ['bar', 'baz']],
32+
['/foo/0', 'bar'],
33+
['/', 0],
34+
['/a~1b', 1],
35+
['/c%d', 2],
36+
['/e^f', 3],
37+
['/g|h', 4],
38+
['/i\\j', 5],
39+
['/k"l', 6],
40+
['/ ', 7],
41+
['/m~0n', 8],
4342
];
4443

4544
jsonStringRepEntries.forEach(([jsonString, expected]) => {
4645
specify('should correctly evaluate JSON Pointer from JSON String', function () {
47-
assert.deepEqual(evaluate(data, JSONString.from(jsonString)), expected);
46+
assert.deepEqual(evaluate(data, jsonString), expected);
4847
});
4948
});
5049
});
5150

52-
context('RCC 6901 JSON String tests', function () {
53-
const jsonStringRepEntries = [
51+
context('RFC 6901 URI Fragment Identifier tests', function () {
52+
const fragmentRepEntries = [
5453
['#', data],
5554
['#/foo', ['bar', 'baz']],
5655
['#/foo/0', 'bar'],
@@ -65,8 +64,8 @@ describe('evaluate', function () {
6564
['#/m~0n', 8],
6665
];
6766

68-
jsonStringRepEntries.forEach(([fragment, expected]) => {
69-
specify('should correctly evaluate JSON Pointer from JSON String', function () {
67+
fragmentRepEntries.forEach(([fragment, expected]) => {
68+
specify('should correctly evaluate JSON Pointer from URI Fragment Identifier', function () {
7069
assert.deepEqual(evaluate(data, URIFragmentIdentifier.from(fragment)), expected);
7170
});
7271
});

test/evaluate/realms/map-set.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { assert } from 'chai';
2+
3+
import {
4+
evaluate,
5+
JSONPointerIndexError,
6+
JSONPointerTypeError,
7+
JSONPointerKeyError,
8+
JSONPointerEvaluateError,
9+
URIFragmentIdentifier,
10+
} from '../../../src/index.js';
11+
import MapSetEvaluationRealm from '../../../src/evaluate/realms/map-set.js';
12+
13+
describe('evaluate', function () {
14+
const realm = new MapSetEvaluationRealm();
15+
const data = new Map([
16+
['foo', new Set(['bar', 'baz'])],
17+
['', 0],
18+
['a/b', 1],
19+
['c%d', 2],
20+
['e^f', 3],
21+
['g|h', 4],
22+
['i\\j', 5],
23+
['k"l', 6],
24+
[' ', 7],
25+
['m~n', 8],
26+
]);
27+
28+
context('RFC 6901 JSON String tests', function () {
29+
const jsonStringRepEntries = [
30+
['', data],
31+
['/foo', new Set(['bar', 'baz'])],
32+
['/foo/0', 'bar'],
33+
['/', 0],
34+
['/a~1b', 1],
35+
['/c%d', 2],
36+
['/e^f', 3],
37+
['/g|h', 4],
38+
['/i\\j', 5],
39+
['/k"l', 6],
40+
['/ ', 7],
41+
['/m~0n', 8],
42+
];
43+
44+
jsonStringRepEntries.forEach(([jsonString, expected]) => {
45+
specify('should correctly evaluate JSON Pointer from JSON String', function () {
46+
assert.deepEqual(evaluate(data, jsonString, { realm }), expected);
47+
});
48+
});
49+
});
50+
51+
context('RFC 6901 URI Fragment Identifier tests', function () {
52+
const fragmentRepEntries = [
53+
['#', data],
54+
['#/foo', new Set(['bar', 'baz'])],
55+
['#/foo/0', 'bar'],
56+
['#/', 0],
57+
['#/a~1b', 1],
58+
['#/c%25d', 2],
59+
['#/e%5Ef', 3],
60+
['#/g%7Ch', 4],
61+
['#/i%5Cj', 5],
62+
['#/k%22l', 6],
63+
['#/%20', 7],
64+
['#/m~0n', 8],
65+
];
66+
67+
fragmentRepEntries.forEach(([fragment, expected]) => {
68+
specify('should correctly evaluate JSON Pointer from URI Fragment Identifier', function () {
69+
assert.deepEqual(evaluate(data, URIFragmentIdentifier.from(fragment), { realm }), expected);
70+
});
71+
});
72+
});
73+
74+
context('valid JSON Pointers', function () {
75+
specify('should return entire document for ""', function () {
76+
assert.deepEqual(evaluate(data, '', { realm }), data);
77+
});
78+
79+
specify('should return Set(["bar", "baz"]) for "/foo"', function () {
80+
assert.deepEqual(evaluate(data, '/foo', { realm }), new Set(['bar', 'baz']));
81+
});
82+
83+
specify('should return "bar" for "/foo/0"', function () {
84+
assert.strictEqual(evaluate(data, '/foo/0', { realm }), 'bar');
85+
});
86+
87+
specify('should return 0 for "/"', function () {
88+
assert.strictEqual(evaluate(data, '/', { realm }), 0);
89+
});
90+
91+
specify('should return 1 for "/a~1b"', function () {
92+
assert.strictEqual(evaluate(data, '/a~1b', { realm }), 1);
93+
});
94+
95+
specify('should return 2 for "/c%d"', function () {
96+
assert.strictEqual(evaluate(data, '/c%d', { realm }), 2);
97+
});
98+
99+
specify('should return 3 for "/e^f"', function () {
100+
assert.strictEqual(evaluate(data, '/e^f', { realm }), 3);
101+
});
102+
103+
specify('should return 4 for "/g|h"', function () {
104+
assert.strictEqual(evaluate(data, '/g|h', { realm }), 4);
105+
});
106+
107+
specify('should return 5 for "/i\\j"', function () {
108+
assert.strictEqual(evaluate(data, '/i\\j', { realm }), 5);
109+
});
110+
111+
specify('should return 6 for "/k\"l"', function () {
112+
assert.strictEqual(evaluate(data, '/k"l', { realm }), 6);
113+
});
114+
115+
specify('should return 7 for "/ "', function () {
116+
assert.strictEqual(evaluate(data, '/ ', { realm }), 7);
117+
});
118+
119+
specify('should return 8 for "/m~0n"', function () {
120+
assert.strictEqual(evaluate(data, '/m~0n', { realm }), 8);
121+
});
122+
});
123+
124+
context('invalid JSON Pointers (should throw errors)', function () {
125+
specify('should throw JSONPointerEvaluateError for invalid JSON Pointer', function () {
126+
assert.throws(() => evaluate(data, 'invalid-pointer', { realm }), JSONPointerEvaluateError);
127+
});
128+
129+
specify(
130+
'should throw JSONPointerTypeError for accessing property on non-object/array',
131+
function () {
132+
assert.throws(() => evaluate(data, '/foo/0/bad', { realm }), JSONPointerTypeError);
133+
},
134+
);
135+
136+
specify('should throw JSONPointerKeyError for non-existing key', function () {
137+
assert.throws(() => evaluate(data, '/nonexistent', { realm }), JSONPointerKeyError);
138+
});
139+
140+
specify('should throw JSONPointerIndexError for non-numeric array index', function () {
141+
assert.throws(() => evaluate(data, '/foo/x', { realm }), JSONPointerIndexError);
142+
});
143+
144+
specify('should throw JSONPointerIndexError for out-of-bounds array index', function () {
145+
assert.throws(() => evaluate(data, '/foo/5', { realm }), JSONPointerIndexError);
146+
});
147+
148+
specify('should throw JSONPointerIndexError for leading zero in array index', function () {
149+
assert.throws(() => evaluate(data, '/foo/01', { realm }), JSONPointerIndexError);
150+
});
151+
152+
specify('should throw JSONPointerIndexError for "-" when strictArrays is true', function () {
153+
assert.throws(
154+
() => evaluate(data, '/foo/-', { strictArrays: true, realm }),
155+
JSONPointerIndexError,
156+
);
157+
});
158+
159+
specify('should return undefined for "-" when strictArrays is false', function () {
160+
assert.strictEqual(evaluate(data, '/foo/-', { strictArrays: false, realm }), undefined);
161+
});
162+
163+
specify(
164+
'should throw JSONPointerKeyError for accessing chain of object properties that do not exist',
165+
function () {
166+
assert.throws(() => evaluate(data, '/missing/key', { realm }), JSONPointerKeyError);
167+
},
168+
);
169+
170+
specify(
171+
'should return undefined accessing object property that does not exist when strictObject is false',
172+
function () {
173+
assert.isUndefined(evaluate(data, '/missing', { strictObjects: false, realm }));
174+
},
175+
);
176+
177+
specify('should throw JSONPointerTypeError when evaluating on primitive', function () {
178+
assert.throws(() => evaluate('not-an-object', '/foo', { realm }), JSONPointerTypeError);
179+
});
180+
181+
specify(
182+
'should throw JSONPointerTypeError when trying to access deep path on primitive',
183+
function () {
184+
assert.throws(() => evaluate({ foo: 42 }, '/foo/bar', { realm }), JSONPointerTypeError);
185+
},
186+
);
187+
});
188+
});

types/evaluate/realms/map-set.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { EvaluationRealm } from '../../index';
2+
3+
declare class MapSetEvaluationRealm extends EvaluationRealm {
4+
public readonly name: 'map-set';
5+
6+
public override isArray(node: unknown): node is Set<unknown>;
7+
public override isObject(node: unknown): node is Map<unknown, unknown>;
8+
public override sizeOf(node: unknown): number;
9+
public override has(node: unknown, referenceToken: string): boolean;
10+
public override evaluate<T = unknown>(node: unknown, referenceToken: string): T;
11+
}
12+
13+
export default MapSetEvaluationRealm;

0 commit comments

Comments
 (0)