Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, evaluate } 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import JSONPointerError from '../../errors/JSONPointerError.js';
import JSONPointerError from '../errors/JSONPointerError.js';

class EvaluationRealm {
name = '';
Expand Down
48 changes: 48 additions & 0 deletions src/evaluate/compose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import EvaluationRealm from './EvaluationRealm.js';
import JSONPointerEvaluateError from '../errors/JSONPointerEvaluateError.js';

class CompositeEvaluationRealm extends EvaluationRealm {
name = 'composite';

realms = [];

constructor(realms) {
super();
this.realms = realms;
}

isArray(node) {
return this.#findRealm(node).isArray(node);
}

isObject(node) {
return this.#findRealm(node).isObject(node);
}

sizeOf(node) {
return this.#findRealm(node).sizeOf(node);
}

has(node, referenceToken) {
return this.#findRealm(node).has(node, referenceToken);
}

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;
}
}
throw new JSONPointerEvaluateError('No suitable evaluation realm found for value', {
currentValue: node,
});
}
}

const compose = (...realms) => new CompositeEvaluationRealm(realms);

export default compose;
2 changes: 1 addition & 1 deletion src/evaluate/realms/json.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import EvaluationRealm from './index.js';
import EvaluationRealm from '../EvaluationRealm.js';

class JSONEvaluationRealm extends EvaluationRealm {
name = 'json';
Expand Down
2 changes: 1 addition & 1 deletion src/evaluate/realms/map-set.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import EvaluationRealm from './index.js';
import EvaluationRealm from '../EvaluationRealm.js';

class MapSetEvaluationRealm extends EvaluationRealm {
name = 'map-set';
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
53 changes: 53 additions & 0 deletions test/evaluate/compose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { assert } from 'chai';

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';

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 }),
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);
}
});
});
});
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function compile(referenceTokens: readonly UnescapedReferenceToken[]): JS
* Evaluating
*/
export function evaluate<T = unknown>(value: unknown, jsonPointer: JSONPointer, options?: EvaluationOptions): T;
export function composeRealms(...realms: EvaluationRealm[]): EvaluationRealm;

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