From 2868292d10f31ae01ef7648e576dc777bff6332e Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Tue, 18 Mar 2025 14:55:04 +0100 Subject: [PATCH 1/3] feat(evaluate): add support for realm composition Refs #13 --- README.md | 32 +++++++++++++ .../{realms/index.js => EvaluationRealm.js} | 2 +- src/evaluate/compose.js | 45 +++++++++++++++++++ src/evaluate/realms/json.js | 2 +- src/evaluate/realms/map-set.js | 2 +- src/index.js | 3 +- test/evaluate/compose.js | 36 +++++++++++++++ types/index.d.ts | 1 + 8 files changed, 119 insertions(+), 4 deletions(-) rename src/evaluate/{realms/index.js => EvaluationRealm.js} (91%) create mode 100644 src/evaluate/compose.js create mode 100644 test/evaluate/compose.js diff --git a/README.md b/README.md index f659328..10c31f2 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,38 @@ class CustomEvaluationRealms extends EvaluationRealm { evaluate({ a: 'b' }, '/a', { realm: new CustomEvaluationRealms() }); // => 'b' ``` +###### Composing Evaluation Realms + +Evaluation realms can be composed to create complex evaluation scenarios, +allowing JSON Pointer evaluation to work across multiple data structures in a seamless manner. +By combining different realms, composite evaluation ensures that a JSON Pointer query can +resolve correctly whether the data structure is an object, array, Map, Set, or any custom type. + +When composing multiple evaluation realms, the **order matters**. The composition is performed from left to right, meaning: + +- More specific realms should be placed first (leftmost position). +- More generic realms should be placed later (rightmost position). + +This ensures that specialized data structures (e.g., Map, Set, Immutable.js) take precedence over generic JavaScript objects and arrays. + +```js +import { composeRealms } from '@swaggerexpert/json-pointer'; +import JSONEvaluationRealm from '@swaggerexpert/json-pointer/realms/json'; +import MapSetEvaluationRealm from '@swaggerexpert/json-pointer/realms/map-set'; + +const compositeRealm = composeRealms(new MapSetEvaluationRealm(), new JSONEvaluationRealm()); + +const structure = [ + { + a: new Map([ + ['b', new Set(['c', 'd'])] + ]), + }, +]; + +evaluate(structure, '/0/a/b/1', { realm : compositeRealm }); // => 'd' +``` + #### Compilation Compilation is the process of transforming a list of reference tokens into a JSON Pointer. diff --git a/src/evaluate/realms/index.js b/src/evaluate/EvaluationRealm.js similarity index 91% rename from src/evaluate/realms/index.js rename to src/evaluate/EvaluationRealm.js index 63e5e0d..3e6135b 100644 --- a/src/evaluate/realms/index.js +++ b/src/evaluate/EvaluationRealm.js @@ -1,4 +1,4 @@ -import JSONPointerError from '../../errors/JSONPointerError.js'; +import JSONPointerError from '../errors/JSONPointerError.js'; class EvaluationRealm { name = ''; diff --git a/src/evaluate/compose.js b/src/evaluate/compose.js new file mode 100644 index 0000000..1038432 --- /dev/null +++ b/src/evaluate/compose.js @@ -0,0 +1,45 @@ +import EvaluationRealm from './EvaluationRealm.js'; + +class CompositeEvaluationRealm extends EvaluationRealm { + name = 'composite'; + + realms = []; + + constructor(realms) { + super(); + this.realms = realms; + } + + isArray(node) { + return this.#findRealm(node)?.isArray(node) ?? false; + } + + isObject(node) { + return this.#findRealm(node)?.isObject(node) ?? false; + } + + sizeOf(node) { + return this.#findRealm(node)?.sizeOf(node) ?? 0; + } + + has(node, referenceToken) { + return this.#findRealm(node)?.has(node, referenceToken) ?? false; + } + + evaluate(node, referenceToken) { + return this.#findRealm(node)?.evaluate(node, referenceToken); + } + + #findRealm(node) { + for (const realm of this.realms) { + if (realm.isArray(node) || realm.isObject(node)) { + return realm; + } + } + return undefined; + } +} + +const compose = (...realms) => new CompositeEvaluationRealm(realms); + +export default compose; diff --git a/src/evaluate/realms/json.js b/src/evaluate/realms/json.js index fe394f6..dd0bf7e 100644 --- a/src/evaluate/realms/json.js +++ b/src/evaluate/realms/json.js @@ -1,4 +1,4 @@ -import EvaluationRealm from './index.js'; +import EvaluationRealm from '../EvaluationRealm.js'; class JSONEvaluationRealm extends EvaluationRealm { name = 'json'; diff --git a/src/evaluate/realms/map-set.js b/src/evaluate/realms/map-set.js index 3fc9ec1..49911e7 100644 --- a/src/evaluate/realms/map-set.js +++ b/src/evaluate/realms/map-set.js @@ -1,4 +1,4 @@ -import EvaluationRealm from './index.js'; +import EvaluationRealm from '../EvaluationRealm.js'; class MapSetEvaluationRealm extends EvaluationRealm { name = 'map-set'; diff --git a/src/index.js b/src/index.js index 5fe7575..f981f0f 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,8 @@ export { default as escape } from './escape.js'; export { default as unescape } from './unescape.js'; export { default as evaluate } from './evaluate/index.js'; -export { default as EvaluationRealm } from './evaluate/realms/index.js'; +export { default as EvaluationRealm } from './evaluate/EvaluationRealm.js'; +export { default as composeRealms } from './evaluate/compose.js'; export { default as JSONPointerError } from './errors/JSONPointerError.js'; export { default as JSONPointerParseError } from './errors/JSONPointerParseError.js'; diff --git a/test/evaluate/compose.js b/test/evaluate/compose.js new file mode 100644 index 0000000..460d79b --- /dev/null +++ b/test/evaluate/compose.js @@ -0,0 +1,36 @@ +import { assert } from 'chai'; + +import { evaluate, composeRealms, JSONPointerTypeError } from '../../src/index.js'; +import JSONEvaluationRealm from '../../src/evaluate/realms/json.js'; +import MapSetEvaluationRealm from '../../src/evaluate/realms/map-set.js'; + +describe('evaluate', function () { + context('composeRealms', function () { + specify('should compose realms', function () { + const compositeRealm = composeRealms(new MapSetEvaluationRealm(), new JSONEvaluationRealm()); + const structure = [ + { + a: new Map([['b', new Set(['c', 'd'])]]), + }, + ]; + const actual = evaluate(structure, '/0/a/b/1', { realm: compositeRealm }); + const expected = 'd'; + + assert.strictEqual(actual, expected); + }); + + specify('should throw on empty composition', function () { + const compositeRealm = composeRealms(); + const structure = [ + { + a: new Map([['b', new Set(['c', 'd'])]]), + }, + ]; + + assert.throws( + () => evaluate(structure, '/0/a/b/1', { realm: compositeRealm }), + JSONPointerTypeError, + ); + }); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index fb9b196..7ce271c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -55,6 +55,7 @@ export function compile(referenceTokens: readonly UnescapedReferenceToken[]): JS * Evaluating */ export function evaluate(value: unknown, jsonPointer: JSONPointer, options?: EvaluationOptions): T; +export function composeRealms(...realms: EvaluationRealm[]): EvaluationRealm; export interface EvaluationOptions { strictArrays?: boolean; From abe897825aa9bcd9e1831d4181ced32b389550dd Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Tue, 18 Mar 2025 15:10:55 +0100 Subject: [PATCH 2/3] fix: interface --- src/evaluate/compose.js | 15 +++++++++------ test/evaluate/compose.js | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/evaluate/compose.js b/src/evaluate/compose.js index 1038432..70af9b1 100644 --- a/src/evaluate/compose.js +++ b/src/evaluate/compose.js @@ -1,4 +1,5 @@ import EvaluationRealm from './EvaluationRealm.js'; +import JSONPointerEvaluateError from '../errors/JSONPointerEvaluateError.js'; class CompositeEvaluationRealm extends EvaluationRealm { name = 'composite'; @@ -11,23 +12,23 @@ class CompositeEvaluationRealm extends EvaluationRealm { } isArray(node) { - return this.#findRealm(node)?.isArray(node) ?? false; + return this.#findRealm(node).isArray(node); } isObject(node) { - return this.#findRealm(node)?.isObject(node) ?? false; + return this.#findRealm(node).isObject(node); } sizeOf(node) { - return this.#findRealm(node)?.sizeOf(node) ?? 0; + return this.#findRealm(node).sizeOf(node); } has(node, referenceToken) { - return this.#findRealm(node)?.has(node, referenceToken) ?? false; + return this.#findRealm(node).has(node, referenceToken); } evaluate(node, referenceToken) { - return this.#findRealm(node)?.evaluate(node, referenceToken); + return this.#findRealm(node).evaluate(node, referenceToken); } #findRealm(node) { @@ -36,7 +37,9 @@ class CompositeEvaluationRealm extends EvaluationRealm { return realm; } } - return undefined; + throw new JSONPointerEvaluateError('No suitable evaluation realm found for value', { + currentValue: node, + }); } } diff --git a/test/evaluate/compose.js b/test/evaluate/compose.js index 460d79b..87cb008 100644 --- a/test/evaluate/compose.js +++ b/test/evaluate/compose.js @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import { evaluate, composeRealms, JSONPointerTypeError } from '../../src/index.js'; +import { evaluate, composeRealms, JSONPointerEvaluateError } from '../../src/index.js'; import JSONEvaluationRealm from '../../src/evaluate/realms/json.js'; import MapSetEvaluationRealm from '../../src/evaluate/realms/map-set.js'; @@ -29,8 +29,25 @@ describe('evaluate', function () { assert.throws( () => evaluate(structure, '/0/a/b/1', { realm: compositeRealm }), - JSONPointerTypeError, + JSONPointerEvaluateError, ); }); + + specify('should throw on invalid realm', function () { + const compositeRealm = composeRealms({}); + const structure = [ + { + a: new Map([['b', new Set(['c', 'd'])]]), + }, + ]; + + try { + evaluate(structure, '/0/a/b/1', { realm: compositeRealm }); + assert.fail('Expected an error to be thrown'); + } catch (error) { + assert.instanceOf(error, JSONPointerEvaluateError); + assert.instanceOf(error.cause, TypeError); + } + }); }); }); From ffa4cc1fe6f2ebead3abc887703c20519cde28c7 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Tue, 18 Mar 2025 15:13:31 +0100 Subject: [PATCH 3/3] fix: missing import --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10c31f2..e28b534 100644 --- a/README.md +++ b/README.md @@ -410,7 +410,7 @@ When composing multiple evaluation realms, the **order matters**. The compositio This ensures that specialized data structures (e.g., Map, Set, Immutable.js) take precedence over generic JavaScript objects and arrays. ```js -import { composeRealms } from '@swaggerexpert/json-pointer'; +import { composeRealms, evaluate } from '@swaggerexpert/json-pointer'; import JSONEvaluationRealm from '@swaggerexpert/json-pointer/realms/json'; import MapSetEvaluationRealm from '@swaggerexpert/json-pointer/realms/map-set';