Skip to content

Commit 1835985

Browse files
committed
feat(evaluate): add support for Immutable.js realm
Refs #13
1 parent 534a6dc commit 1835985

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
- [Map/Set](#mapset-evaluation-realm)
4646
- [Minim](#minim-evaluation-realm)
4747
- [ApiDOM](#apidom-evaluation-realm)
48+
- [Immutable.js]('#immutablejs-evaluation-realm)
4849
- [Custom](#custom-evaluation-realms)
4950
- [Composing Realms](#composing-evaluation-realms)
5051
- [Compilation](#compilation)
@@ -426,6 +427,30 @@ const objectElement = new ObjectElement({
426427
evaluate(objectElement, '/a/1', { realm: new ApiDOMEvaluationRealm() }); // => StringElement('c')
427428
```
428429

430+
###### Immutable.js Evaluation Realm
431+
432+
The [Immutable.js](https://immutable-js.com/) Evaluation Realm is an integration layer that enables
433+
evaluation of JSON Pointer expressions on Immutable.js structures.
434+
435+
Before using the Immutable.js Evaluation Realm, you need to install the `immutable` packages:
436+
437+
```sh
438+
$ npm install --save immutable
439+
```
440+
441+
```js
442+
import { fromJS } from 'immutable';
443+
import { evaluate } from '@swaggerexpert/json-pointer';
444+
import ImmutableEvaluationRealm from '@swaggerexpert/json-pointer/evaluate/realms/immutable';
445+
446+
const map = fromJS({
447+
a: ['b', 'c']
448+
});
449+
450+
evaluate(map, '/a/1', { realm: new ImmutableEvaluationRealm() }); // => 'c'
451+
```
452+
453+
429454
###### Custom Evaluation Realms
430455

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

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"chai": "=5.2.0",
8585
"cross-env": "^7.0.3",
8686
"husky": "=9.1.7",
87+
"immutable": "^5.1.1",
8788
"minim": "^0.23.8",
8889
"mocha": "=11.1.0",
8990
"npm-watch": "^0.13.0",

src/evaluate/realms/immutable.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { isMap, isList } from 'immutable';
2+
3+
import EvaluationRealm from '../EvaluationRealm.js';
4+
5+
class ImmutableEvaluationRealm extends EvaluationRealm {
6+
name = 'immutable';
7+
8+
isArray(node) {
9+
return isList(node);
10+
}
11+
12+
isObject(node) {
13+
return isMap(node);
14+
}
15+
16+
sizeOf(node) {
17+
if (this.isArray(node) || this.isObject(node)) {
18+
return node.size;
19+
}
20+
return 0;
21+
}
22+
23+
has(node, referenceToken) {
24+
if (this.isArray(node)) {
25+
return Number(referenceToken) < this.sizeOf(node);
26+
}
27+
if (this.isObject(node)) {
28+
return node.has(referenceToken);
29+
}
30+
return false;
31+
}
32+
33+
evaluate(node, referenceToken) {
34+
if (this.isArray(node)) {
35+
return node.get(Number(referenceToken));
36+
}
37+
return node.get(referenceToken);
38+
}
39+
}
40+
41+
export default ImmutableEvaluationRealm;

test/evaluate/realms/immutable.js

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

0 commit comments

Comments
 (0)