Skip to content

Commit 567ae9d

Browse files
authored
feat(evaluate): add support for evaluation realm composition (#16)
Refs #13
1 parent 33c8c57 commit 567ae9d

File tree

8 files changed

+139
-4
lines changed

8 files changed

+139
-4
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,38 @@ class CustomEvaluationRealms extends EvaluationRealm {
395395
evaluate({ a: 'b' }, '/a', { realm: new CustomEvaluationRealms() }); // => 'b'
396396
```
397397

398+
###### Composing Evaluation Realms
399+
400+
Evaluation realms can be composed to create complex evaluation scenarios,
401+
allowing JSON Pointer evaluation to work across multiple data structures in a seamless manner.
402+
By combining different realms, composite evaluation ensures that a JSON Pointer query can
403+
resolve correctly whether the data structure is an object, array, Map, Set, or any custom type.
404+
405+
When composing multiple evaluation realms, the **order matters**. The composition is performed from left to right, meaning:
406+
407+
- More specific realms should be placed first (leftmost position).
408+
- More generic realms should be placed later (rightmost position).
409+
410+
This ensures that specialized data structures (e.g., Map, Set, Immutable.js) take precedence over generic JavaScript objects and arrays.
411+
412+
```js
413+
import { composeRealms, evaluate } from '@swaggerexpert/json-pointer';
414+
import JSONEvaluationRealm from '@swaggerexpert/json-pointer/realms/json';
415+
import MapSetEvaluationRealm from '@swaggerexpert/json-pointer/realms/map-set';
416+
417+
const compositeRealm = composeRealms(new MapSetEvaluationRealm(), new JSONEvaluationRealm());
418+
419+
const structure = [
420+
{
421+
a: new Map([
422+
['b', new Set(['c', 'd'])]
423+
]),
424+
},
425+
];
426+
427+
evaluate(structure, '/0/a/b/1', { realm : compositeRealm }); // => 'd'
428+
```
429+
398430
#### Compilation
399431

400432
Compilation is the process of transforming a list of reference tokens into a JSON Pointer.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import JSONPointerError from '../../errors/JSONPointerError.js';
1+
import JSONPointerError from '../errors/JSONPointerError.js';
22

33
class EvaluationRealm {
44
name = '';

src/evaluate/compose.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import EvaluationRealm from './EvaluationRealm.js';
2+
import JSONPointerEvaluateError from '../errors/JSONPointerEvaluateError.js';
3+
4+
class CompositeEvaluationRealm extends EvaluationRealm {
5+
name = 'composite';
6+
7+
realms = [];
8+
9+
constructor(realms) {
10+
super();
11+
this.realms = realms;
12+
}
13+
14+
isArray(node) {
15+
return this.#findRealm(node).isArray(node);
16+
}
17+
18+
isObject(node) {
19+
return this.#findRealm(node).isObject(node);
20+
}
21+
22+
sizeOf(node) {
23+
return this.#findRealm(node).sizeOf(node);
24+
}
25+
26+
has(node, referenceToken) {
27+
return this.#findRealm(node).has(node, referenceToken);
28+
}
29+
30+
evaluate(node, referenceToken) {
31+
return this.#findRealm(node).evaluate(node, referenceToken);
32+
}
33+
34+
#findRealm(node) {
35+
for (const realm of this.realms) {
36+
if (realm.isArray(node) || realm.isObject(node)) {
37+
return realm;
38+
}
39+
}
40+
throw new JSONPointerEvaluateError('No suitable evaluation realm found for value', {
41+
currentValue: node,
42+
});
43+
}
44+
}
45+
46+
const compose = (...realms) => new CompositeEvaluationRealm(realms);
47+
48+
export default compose;

src/evaluate/realms/json.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import EvaluationRealm from './index.js';
1+
import EvaluationRealm from '../EvaluationRealm.js';
22

33
class JSONEvaluationRealm extends EvaluationRealm {
44
name = 'json';

src/evaluate/realms/map-set.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import EvaluationRealm from './index.js';
1+
import EvaluationRealm from '../EvaluationRealm.js';
22

33
class MapSetEvaluationRealm extends EvaluationRealm {
44
name = 'map-set';

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export { default as escape } from './escape.js';
1818
export { default as unescape } from './unescape.js';
1919

2020
export { default as evaluate } from './evaluate/index.js';
21-
export { default as EvaluationRealm } from './evaluate/realms/index.js';
21+
export { default as EvaluationRealm } from './evaluate/EvaluationRealm.js';
22+
export { default as composeRealms } from './evaluate/compose.js';
2223

2324
export { default as JSONPointerError } from './errors/JSONPointerError.js';
2425
export { default as JSONPointerParseError } from './errors/JSONPointerParseError.js';

test/evaluate/compose.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { assert } from 'chai';
2+
3+
import { evaluate, composeRealms, JSONPointerEvaluateError } from '../../src/index.js';
4+
import JSONEvaluationRealm from '../../src/evaluate/realms/json.js';
5+
import MapSetEvaluationRealm from '../../src/evaluate/realms/map-set.js';
6+
7+
describe('evaluate', function () {
8+
context('composeRealms', function () {
9+
specify('should compose realms', function () {
10+
const compositeRealm = composeRealms(new MapSetEvaluationRealm(), new JSONEvaluationRealm());
11+
const structure = [
12+
{
13+
a: new Map([['b', new Set(['c', 'd'])]]),
14+
},
15+
];
16+
const actual = evaluate(structure, '/0/a/b/1', { realm: compositeRealm });
17+
const expected = 'd';
18+
19+
assert.strictEqual(actual, expected);
20+
});
21+
22+
specify('should throw on empty composition', function () {
23+
const compositeRealm = composeRealms();
24+
const structure = [
25+
{
26+
a: new Map([['b', new Set(['c', 'd'])]]),
27+
},
28+
];
29+
30+
assert.throws(
31+
() => evaluate(structure, '/0/a/b/1', { realm: compositeRealm }),
32+
JSONPointerEvaluateError,
33+
);
34+
});
35+
36+
specify('should throw on invalid realm', function () {
37+
const compositeRealm = composeRealms({});
38+
const structure = [
39+
{
40+
a: new Map([['b', new Set(['c', 'd'])]]),
41+
},
42+
];
43+
44+
try {
45+
evaluate(structure, '/0/a/b/1', { realm: compositeRealm });
46+
assert.fail('Expected an error to be thrown');
47+
} catch (error) {
48+
assert.instanceOf(error, JSONPointerEvaluateError);
49+
assert.instanceOf(error.cause, TypeError);
50+
}
51+
});
52+
});
53+
});

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function compile(referenceTokens: readonly UnescapedReferenceToken[]): JS
5555
* Evaluating
5656
*/
5757
export function evaluate<T = unknown>(value: unknown, jsonPointer: JSONPointer, options?: EvaluationOptions): T;
58+
export function composeRealms(...realms: EvaluationRealm[]): EvaluationRealm;
5859

5960
export interface EvaluationOptions<R extends EvaluationRealm = JSONEvaluationRealm> {
6061
strictArrays?: boolean;

0 commit comments

Comments
 (0)