Skip to content

Commit 235e995

Browse files
committed
feat(evaluate): add contextual ApiDOM evaluation
Refs #33
1 parent abf5d6e commit 235e995

File tree

7 files changed

+276
-74
lines changed

7 files changed

+276
-74
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,19 @@ const objectElement = new ObjectElement({
468468
evaluate(objectElement, '/a/1', { realm: new ApiDOMEvaluationRealm() }); // => StringElement('c')
469469
```
470470

471+
or using contextual evaluation:
472+
473+
```js
474+
import { ObjectElement } from '@swagger-api/apidom-core';
475+
import { evaluate } from '@swaggerexpert/json-pointer/evaluate/realms/apidom';
476+
477+
const objectElement = new ObjectElement({
478+
a: ['b', 'c']
479+
});
480+
481+
evaluate(objectElement, '/a/1'); // => StringElement('c')
482+
```
483+
471484
###### Immutable.js Evaluation Realm
472485

473486
The [Immutable.js](https://immutable-js.com/) Evaluation Realm is an integration layer that enables
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import baseEvaluate from '../../../evaluate/index.js';
2+
import ApiDOMEvaluationRealm from './realm.js';
3+
4+
const evaluate = (value, jsonPointer, options = {}) => {
5+
return baseEvaluate(value, jsonPointer, { ...options, realm: new ApiDOMEvaluationRealm() });
6+
};
7+
8+
export default evaluate;
Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,2 @@
1-
import { isObjectElement, isArrayElement } from '@swagger-api/apidom-core';
2-
3-
import EvaluationRealm from '../EvaluationRealm.js';
4-
import JSONPointerKeyError from '../../../errors/JSONPointerKeyError.js';
5-
import JSONPointerIndexError from '../../../errors/JSONPointerIndexError.js';
6-
7-
class ApiDOMEvaluationRealm extends EvaluationRealm {
8-
name = 'apidom';
9-
10-
isArray(node) {
11-
return isArrayElement(node);
12-
}
13-
14-
isObject(node) {
15-
return isObjectElement(node);
16-
}
17-
18-
sizeOf(node) {
19-
if (this.isArray(node) || this.isObject(node)) {
20-
return node.length;
21-
}
22-
return 0;
23-
}
24-
25-
has(node, referenceToken) {
26-
if (this.isArray(node)) {
27-
const index = Number(referenceToken);
28-
const indexUint32 = index >>> 0;
29-
30-
if (index !== indexUint32) {
31-
throw new JSONPointerIndexError(
32-
`Invalid array index "${referenceToken}": index must be an unsinged 32-bit integer`,
33-
{
34-
referenceToken,
35-
currentValue: node,
36-
realm: this.name,
37-
},
38-
);
39-
}
40-
41-
return indexUint32 < this.sizeOf(node);
42-
}
43-
if (this.isObject(node)) {
44-
const keys = node.keys();
45-
const uniqueKeys = new Set(keys);
46-
47-
if (keys.length !== uniqueKeys.size) {
48-
throw new JSONPointerKeyError(
49-
`Object key "${referenceToken}" is not unique — JSON Pointer requires unique member names`,
50-
{
51-
referenceToken,
52-
currentValue: node,
53-
realm: this.name,
54-
},
55-
);
56-
}
57-
58-
return node.hasKey(referenceToken);
59-
}
60-
return false;
61-
}
62-
63-
evaluate(node, referenceToken) {
64-
if (this.isArray(node)) {
65-
return node.get(Number(referenceToken));
66-
}
67-
return node.get(referenceToken);
68-
}
69-
}
70-
71-
export default ApiDOMEvaluationRealm;
1+
export { default } from './realm.js';
2+
export { default as evaluate } from './evaluate.js';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { isObjectElement, isArrayElement } from '@swagger-api/apidom-core';
2+
3+
import EvaluationRealm from '../EvaluationRealm.js';
4+
import JSONPointerKeyError from '../../../errors/JSONPointerKeyError.js';
5+
import JSONPointerIndexError from '../../../errors/JSONPointerIndexError.js';
6+
7+
class ApiDOMEvaluationRealm extends EvaluationRealm {
8+
name = 'apidom';
9+
10+
isArray(node) {
11+
return isArrayElement(node);
12+
}
13+
14+
isObject(node) {
15+
return isObjectElement(node);
16+
}
17+
18+
sizeOf(node) {
19+
if (this.isArray(node) || this.isObject(node)) {
20+
return node.length;
21+
}
22+
return 0;
23+
}
24+
25+
has(node, referenceToken) {
26+
if (this.isArray(node)) {
27+
const index = Number(referenceToken);
28+
const indexUint32 = index >>> 0;
29+
30+
if (index !== indexUint32) {
31+
throw new JSONPointerIndexError(
32+
`Invalid array index "${referenceToken}": index must be an unsinged 32-bit integer`,
33+
{
34+
referenceToken,
35+
currentValue: node,
36+
realm: this.name,
37+
},
38+
);
39+
}
40+
41+
return indexUint32 < this.sizeOf(node);
42+
}
43+
if (this.isObject(node)) {
44+
const keys = node.keys();
45+
const uniqueKeys = new Set(keys);
46+
47+
if (keys.length !== uniqueKeys.size) {
48+
throw new JSONPointerKeyError(
49+
`Object key "${referenceToken}" is not unique — JSON Pointer requires unique member names`,
50+
{
51+
referenceToken,
52+
currentValue: node,
53+
realm: this.name,
54+
},
55+
);
56+
}
57+
58+
return node.hasKey(referenceToken);
59+
}
60+
return false;
61+
}
62+
63+
evaluate(node, referenceToken) {
64+
if (this.isArray(node)) {
65+
return node.get(Number(referenceToken));
66+
}
67+
return node.get(referenceToken);
68+
}
69+
}
70+
71+
export default ApiDOMEvaluationRealm;
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { assert } from 'chai';
2+
import { ObjectElement, MemberElement, toValue } from '@swagger-api/apidom-core';
3+
import { InfoElement } from '@swagger-api/apidom-ns-openapi-3-0';
4+
5+
import {
6+
JSONPointerIndexError,
7+
JSONPointerTypeError,
8+
JSONPointerKeyError,
9+
JSONPointerEvaluateError,
10+
URIFragmentIdentifier,
11+
} from '../../../../src/index.js';
12+
import { evaluate } from '../../../../src/evaluate/realms/apidom/index.js';
13+
14+
describe('evaluate', function () {
15+
context('ApiDOM realm - contextual evaluate', function () {
16+
const element = new ObjectElement({
17+
foo: ['bar', 'baz'],
18+
'': 0,
19+
'a/b': 1,
20+
'c%d': 2,
21+
'e^f': 3,
22+
'g|h': 4,
23+
'i\\j': 5,
24+
'k"l': 6,
25+
' ': 7,
26+
'm~n': 8,
27+
});
28+
29+
context('RFC 6901 JSON String tests', function () {
30+
const jsonStringRepEntries = [
31+
['', toValue(element)],
32+
['/foo', ['bar', 'baz']],
33+
['/foo/0', 'bar'],
34+
['/', 0],
35+
['/a~1b', 1],
36+
['/c%d', 2],
37+
['/e^f', 3],
38+
['/g|h', 4],
39+
['/i\\j', 5],
40+
['/k"l', 6],
41+
['/ ', 7],
42+
['/m~0n', 8],
43+
];
44+
45+
jsonStringRepEntries.forEach(([jsonString, expected]) => {
46+
specify('should correctly evaluate JSON Pointer from JSON String', function () {
47+
const actual = toValue(evaluate(element, jsonString));
48+
49+
assert.deepEqual(actual, expected);
50+
});
51+
});
52+
});
53+
54+
context('RFC 6901 URI Fragment Identifier tests', function () {
55+
const fragmentRepEntries = [
56+
['#', toValue(element)],
57+
['#/foo', ['bar', 'baz']],
58+
['#/foo/0', 'bar'],
59+
['#/', 0],
60+
['#/a~1b', 1],
61+
['#/c%25d', 2],
62+
['#/e%5Ef', 3],
63+
['#/g%7Ch', 4],
64+
['#/i%5Cj', 5],
65+
['#/k%22l', 6],
66+
['#/%20', 7],
67+
['#/m~0n', 8],
68+
];
69+
70+
fragmentRepEntries.forEach(([fragment, expected]) => {
71+
specify('should correctly evaluate JSON Pointer from URI Fragment Identifier', function () {
72+
const actual = toValue(evaluate(element, URIFragmentIdentifier.from(fragment)));
73+
74+
assert.deepEqual(actual, expected);
75+
});
76+
});
77+
});
78+
79+
context('given ApiDOM namespace element', function () {
80+
specify('should evaluate', function () {
81+
const infoElement = InfoElement.refract({
82+
contact: {
83+
name: 'SwaggerExpert',
84+
85+
},
86+
});
87+
const actual = toValue(evaluate(infoElement, '/contact/name'));
88+
89+
assert.strictEqual(actual, 'SwaggerExpert');
90+
});
91+
});
92+
93+
context('invalid JSON Pointers (should throw errors)', function () {
94+
specify('should throw JSONPointerEvaluateError for invalid JSON Pointer', function () {
95+
assert.throws(() => evaluate(element, 'invalid-pointer'), JSONPointerEvaluateError);
96+
});
97+
98+
specify(
99+
'should throw JSONPointerTypeError for accessing property on non-object/array',
100+
function () {
101+
assert.throws(() => evaluate(element, '/foo/0/bad'), JSONPointerTypeError);
102+
},
103+
);
104+
105+
specify('should throw JSONPointerKeyError for non-existing key', function () {
106+
assert.throws(() => evaluate(element, '/nonexistent'), JSONPointerKeyError);
107+
});
108+
109+
specify('should throw JSONPointerIndexError for non-numeric array index', function () {
110+
assert.throws(() => evaluate(element, '/foo/x'), JSONPointerIndexError);
111+
});
112+
113+
specify('should throw JSONPointerIndexError for out-of-bounds array index', function () {
114+
assert.throws(() => evaluate(element, '/foo/5'), JSONPointerIndexError);
115+
});
116+
117+
specify('should throw JSONPointerIndexError for leading zero in array index', function () {
118+
assert.throws(() => evaluate(element, '/foo/01'), JSONPointerIndexError);
119+
});
120+
121+
specify('should throw JSONPointerIndexError for "-" when strictArrays is true', function () {
122+
assert.throws(
123+
() => evaluate(element, '/foo/-', { strictArrays: true }),
124+
JSONPointerIndexError,
125+
);
126+
});
127+
128+
specify('should return undefined for "-" when strictArrays is false', function () {
129+
assert.strictEqual(evaluate(element, '/foo/-', { strictArrays: false }), undefined);
130+
});
131+
132+
specify(
133+
'should throw JSONPointerKeyError for accessing chain of object properties that do not exist',
134+
function () {
135+
assert.throws(() => evaluate(element, '/missing/key'), JSONPointerKeyError);
136+
},
137+
);
138+
139+
specify(
140+
'should return undefined accessing object property that does not exist when strictObject is false',
141+
function () {
142+
assert.isUndefined(evaluate(element, '/missing', { strictObjects: false }));
143+
},
144+
);
145+
146+
specify('should throw JSONPointerTypeError when evaluating on primitive', function () {
147+
assert.throws(() => evaluate('not-an-object', '/foo'), JSONPointerTypeError);
148+
});
149+
150+
specify(
151+
'should throw JSONPointerTypeError when trying to access deep path on primitive',
152+
function () {
153+
assert.throws(() => evaluate({ foo: 42 }, '/foo/bar'), JSONPointerTypeError);
154+
},
155+
);
156+
});
157+
158+
context('given member name is not unique in an object', function () {
159+
specify('should throw JSONPointerKeyError', function () {
160+
const objectElement = new ObjectElement({ a: 'b' });
161+
objectElement.content.push(new MemberElement('a', 'c'));
162+
163+
assert.throws(() => evaluate(objectElement, '/a'), JSONPointerKeyError);
164+
});
165+
});
166+
});
167+
});

test/evaluate/realms/apidom.js renamed to test/evaluate/realms/apidom/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
JSONPointerKeyError,
1010
JSONPointerEvaluateError,
1111
URIFragmentIdentifier,
12-
} from '../../../src/index.js';
13-
import ApiDOMEvaluationRealm from '../../../src/evaluate/realms/apidom/index.js';
12+
} from '../../../../src/index.js';
13+
import ApiDOMEvaluationRealm from '../../../../src/evaluate/realms/apidom/index.js';
1414

1515
describe('evaluate', function () {
1616
context('ApiDOM realm', function () {

types/evaluate/realms/apidom.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import type { EvaluationRealm } from '../../index.ts';
1+
import type { EvaluationRealm, JSONPointer, EvaluationOptions } from '../../index.ts';
22

3+
/**
4+
* Realm
5+
*/
36
declare class ApiDOMEvaluationRealm extends EvaluationRealm {
47
public readonly name: 'apidom';
58

@@ -10,4 +13,13 @@ declare class ApiDOMEvaluationRealm extends EvaluationRealm {
1013
public evaluate<T = unknown>(node: unknown, referenceToken: string): T;
1114
}
1215

16+
/**
17+
* Evaluating
18+
*/
19+
export function evaluate<T = unknown>(value: unknown, jsonPointer: JSONPointer, options?: ApiDOMRealmEvaluationOptions): T;
20+
21+
export interface ApiDOMRealmEvaluationOptions extends EvaluationOptions {
22+
realm: ApiDOMEvaluationRealm;
23+
}
24+
1325
export default ApiDOMEvaluationRealm;

0 commit comments

Comments
 (0)