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
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,76 @@ reference token failed to resolve a concrete value.
evaluate(value, '/bar/baz', { strictObjects: false }); // => throw JSONPointerTypeError
```

##### Evaluation Realms

An **evaluation realm** defines the rules for interpreting and navigating data structures in JSON Pointer evaluation.
While JSON Pointer traditionally operates on JSON objects and arrays, evaluation realms allow the evaluation to work
polymorphically with different data structures, such as [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map),
[Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set), [Immutable.js](https://immutable-js.com/),
or even custom representations like [ApiDOM](https://github.com/swagger-api/apidom).
Realm can be specified via the `realm` option in the `evalute()` function.

###### JSON Evaluation Realm

By default, the evaluation operates under the **JSON realm**, which assumes that:

- **Arrays** are indexed numerically.
- **Objects** (plain JavaScript objects) are accessed by string keys.

The default realm is represented by the `JSONEvaluationRealm` class.

```js
import { evaluate } from '@swaggerexpert/json-pointer';

evaluate({ a: 'b' }, '/a'); // => 'b'
```

is equivalent to:

```js
import { evaluate } from '@swaggerexpert/json-pointer';
import JSONEvaluationRealm from '@swaggerexpert/json-pointer/evaluate/realms/json';

evaluate({ a: 'b' }, '/a', { realm: new JSONEvaluationRealm() }); // => 'b'
```

###### Custom Evaluation Realms

The evaluation is designed to support **custom evaluation realms**,
enabling JSON Pointer evaluation for **non-standard data structures**.

A valid custom evaluation realm must match the structure of the `EvaluationRealm` interface.

```ts
interface EvaluationRealm {
readonly name: string;

isArray(node: unknown): boolean;
isObject(node: unknown): boolean;
sizeOf(node: unknown): number;
has(node: unknown, referenceToken: string): boolean;
evaluate(node: unknown, referenceToken: string): unknown;
}
```

One way to create a custom realm is to extend the `EvaluationRealm` class and implement the required methods.

```js
import { evaluate, EvaluationRealm } from '@swaggerexpert/json-pointer';

class CustomEvaluationRealms extends EvaluationRealm {
name = 'cusotm';

isArray(node) { ... }
isObject(node) { ... }
sizeOf(node) { ... }
has(node, referenceToken) { ... }
evaluate(node, referenceToken) { ... }
}

evaluate({ a: 'b' }, '/a', { realm: new CustomEvaluationRealms() }); // => 'b'
```

#### Compilation

Compilation is the process of transforming a list of reference tokens into a JSON Pointer.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
"import": "./es/index.mjs",
"require": "./cjs/index.cjs"
},
"./evaluate/realms/*": {
"types": "./types/evaluate/realms/*.d.ts",
"import": "./es/evaluate/realms/*.mjs",
"require": "./cjs/evaluate/realms/*.cjs"
},
"./package.json": "./package.json"
},
"watch": {
Expand Down
88 changes: 0 additions & 88 deletions src/evaluate.js

This file was deleted.

101 changes: 101 additions & 0 deletions src/evaluate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import parse from '../parse/index.js';
import testArrayDash from '../test/array-dash.js';
import testArrayIndex from '../test/array-index.js';
import JSONRealm from './realms/json.js';
import JSONPointerEvaluateError from '../errors/JSONPointerEvaluateError.js';
import JSONPointerTypeError from '../errors/JSONPointerTypeError.js';
import JSONPointerIndexError from '../errors/JSONPointerIndexError.js';
import JSONPointerKeyError from '../errors/JSONPointerKeyError.js';

const evaluate = (
value,
jsonPointer,
{ strictArrays = true, strictObjects = true, evaluator = null, realm = new JSONRealm() } = {},
) => {
const parseOptions = typeof evaluator === 'function' ? { evaluator } : undefined;
const { result, computed: referenceTokens } = parse(jsonPointer, parseOptions);

if (!result.success) {
throw new JSONPointerEvaluateError(`Invalid JSON Pointer: ${jsonPointer}`, {
jsonPointer,
});
}

try {
return referenceTokens.reduce((current, referenceToken, referenceTokenPosition) => {
if (realm.isArray(current)) {
if (testArrayDash(referenceToken)) {
if (strictArrays) {
throw new JSONPointerIndexError(
'Invalid array index: "-" always refers to a nonexistent element during evaluation',
{
jsonPointer,
referenceTokens,
referenceToken,
referenceTokenPosition,
},
);
} else {
return realm.evaluate(current, String(realm.sizeOf(current)));
}
}

if (!testArrayIndex(referenceToken) && strictArrays) {
throw new JSONPointerIndexError(
`Invalid array index: '${referenceToken}' (MUST be "0", or digits without a leading "0")`,
{
jsonPointer,
referenceTokens,
referenceToken,
referenceTokenPosition,
},
);
}

const index = Number(referenceToken);
if (index >= realm.sizeOf(current) && strictArrays) {
throw new JSONPointerIndexError(`Invalid array index: '${index}' out of bounds`, {
jsonPointer,
referenceTokens,
referenceToken: index,
referenceTokenPosition,
});
}
return realm.evaluate(current, referenceToken);
}

if (realm.isObject(current)) {
if (!realm.has(current, referenceToken) && strictObjects) {
throw new JSONPointerKeyError(undefined, {
jsonPointer,
referenceTokens,
referenceToken,
referenceTokenPosition,
});
}

return realm.evaluate(current, referenceToken);
}

throw new JSONPointerTypeError(undefined, {
jsonPointer,
referenceTokens,
referenceToken,
referenceTokenPosition,
currentValue: current,
});
}, value);
} catch (error) {
if (error instanceof JSONPointerEvaluateError) {
throw error;
}

throw new JSONPointerEvaluateError('Unexpected error during JSON Pointer evaluation', {
cause: error,
jsonPointer,
referenceTokens,
});
}
};

export default evaluate;
27 changes: 27 additions & 0 deletions src/evaluate/realms/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import JSONPointerError from '../../errors/JSONPointerError.js';

class EvaluationRealm {
name = '';

isArray(node) {
throw new JSONPointerError('Realm.isArray(node) must be implemented in a subclass');
}

isObject(node) {
throw new JSONPointerError('Realm.isObject(node) must be implemented in a subclass');
}

sizeOf(node) {
throw new JSONPointerError('Realm.sizeOf(node) must be implemented in a subclass');
}

has(node, referenceToken) {
throw new JSONPointerError('Realm.has(node) must be implemented in a subclass');
}

evaluate(node, referenceToken) {
throw new JSONPointerError('Realm.evaluate(node) must be implemented in a subclass');
}
}

export default EvaluationRealm;
42 changes: 42 additions & 0 deletions src/evaluate/realms/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import EvaluationRealm from './index.js';

class JSONEvaluationRealm extends EvaluationRealm {
name = 'json';

isArray(node) {
return Array.isArray(node);
}

isObject(node) {
return typeof node === 'object' && node !== null && !this.isArray(node);
}

sizeOf(node) {
if (this.isArray(node)) {
return node.length;
}
if (this.isObject(node)) {
return Object.keys(node).length;
}
return 0;
}

has(node, referenceToken) {
if (this.isArray(node)) {
return Number(referenceToken) < this.sizeOf(node);
}
if (this.isObject(node)) {
return Object.prototype.hasOwnProperty.call(node, referenceToken);
}
return false;
}

evaluate(node, referenceToken) {
if (this.isArray(node)) {
return node[Number(referenceToken)];
}
return node[referenceToken];
}
}

export default JSONEvaluationRealm;
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export { default as compile } from './compile.js';
export { default as escape } from './escape.js';
export { default as unescape } from './unescape.js';

export { default as evaluate } from './evaluate.js';
export { default as evaluate } from './evaluate/index.js';
export { default as EvaluationRealm } from './evaluate/realms/index.js';

export { default as JSONPointerError } from './errors/JSONPointerError.js';
export { default as JSONPointerParseError } from './errors/JSONPointerParseError.js';
Expand Down
17 changes: 16 additions & 1 deletion test/evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
JSONString,
URIFragmentIdentifier,
} from '../src/index.js';
import JSONEvaluationRealm from '../src/evaluate/realms/json.js';

describe('evaluate', function () {
const data = {
Expand Down Expand Up @@ -121,7 +122,7 @@ describe('evaluate', function () {
});
});

context('custom evaluator option', function () {
context('given custom evaluator option', function () {
specify('should correctly use a default evaluator', function () {
const result = evaluate(data, '/a~1b', { evaluator: referenceTokenListEvaluator });

Expand All @@ -141,6 +142,20 @@ describe('evaluate', function () {
});
});

context('given custom realm option', function () {
specify('should use custom realm', function () {
const result = evaluate(data, '/a~1b', { realm: new JSONEvaluationRealm() });

assert.deepEqual(result, 1);
});

specify('should throw on invalid custom realm', function () {
const realm = {};

assert.throws(() => evaluate(data, '/a~1b', { realm }), JSONPointerEvaluateError);
});
});

context('invalid JSON Pointers (should throw errors)', function () {
specify('should throw JSONPointerEvaluateError for invalid JSON Pointer', function () {
assert.throws(() => evaluate(data, 'invalid-pointer'), JSONPointerEvaluateError);
Expand Down
Loading