From a320f4699276e011dffd0a151844a41da50a2fab Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Mon, 17 Mar 2025 22:28:37 +0100 Subject: [PATCH 1/4] feat(evaluate): add support for Map/Set realm Refs #13 --- README.md | 18 +++ src/evaluate/realms/map-set.js | 39 +++++ test/{evaluate.js => evaluate/index.js} | 41 +++--- test/evaluate/realms/map-set.js | 188 ++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 src/evaluate/realms/map-set.js rename test/{evaluate.js => evaluate/index.js} (89%) create mode 100644 test/evaluate/realms/map-set.js diff --git a/README.md b/README.md index 33a19c0..f659328 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,24 @@ import JSONEvaluationRealm from '@swaggerexpert/json-pointer/evaluate/realms/jso evaluate({ a: 'b' }, '/a', { realm: new JSONEvaluationRealm() }); // => 'b' ``` +###### Map/Set Evaluation Realm + +The Map/Set realm extends JSON Pointer evaluation to support [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instances, +allowing structured traversal and access beyond traditional JavaScript objects and arrays. +Map/Set realm is represented by the `MapSetEvaluationRealm` class. + + +```js +import { evaluate } from '@swaggerexpert/json-pointer'; +import MapSetEvaluationRealm from '@swaggerexpert/json-pointer/evaluate/realms/map-set'; + +const map = new Map([ + ['a', new Set(['b', 'c'])] +]); + +evaluate(map, '/a/1', { realm: new MapSetEvaluationRealm() }); // => 'c' +``` + ###### Custom Evaluation Realms The evaluation is designed to support **custom evaluation realms**, diff --git a/src/evaluate/realms/map-set.js b/src/evaluate/realms/map-set.js new file mode 100644 index 0000000..3fc9ec1 --- /dev/null +++ b/src/evaluate/realms/map-set.js @@ -0,0 +1,39 @@ +import EvaluationRealm from './index.js'; + +class MapSetEvaluationRealm extends EvaluationRealm { + name = 'map-set'; + + isArray(node) { + return node instanceof Set || Object.prototype.toString.call(node) === '[object Set]'; + } + + isObject(node) { + return node instanceof Map || Object.prototype.toString.call(node) === '[object Map]'; + } + + sizeOf(node) { + if (this.isArray(node) || this.isObject(node)) { + return node.size; + } + return 0; + } + + has(node, referenceToken) { + if (this.isArray(node)) { + return Number(referenceToken) < this.sizeOf(node); + } + if (this.isObject(node)) { + return node.has(referenceToken); + } + return false; + } + + evaluate(node, referenceToken) { + if (this.isArray(node)) { + return [...node][Number(referenceToken)]; + } + return node.get(referenceToken); + } +} + +export default MapSetEvaluationRealm; diff --git a/test/evaluate.js b/test/evaluate/index.js similarity index 89% rename from test/evaluate.js rename to test/evaluate/index.js index d63c4a2..0515c37 100644 --- a/test/evaluate.js +++ b/test/evaluate/index.js @@ -7,10 +7,9 @@ import { JSONPointerKeyError, JSONPointerEvaluateError, referenceTokenListEvaluator, - JSONString, URIFragmentIdentifier, -} from '../src/index.js'; -import JSONEvaluationRealm from '../src/evaluate/realms/json.js'; +} from '../../src/index.js'; +import JSONEvaluationRealm from '../../src/evaluate/realms/json.js'; describe('evaluate', function () { const data = { @@ -26,31 +25,31 @@ describe('evaluate', function () { 'm~n': 8, }; - context('RCC 6901 JSON String tests', function () { + context('RFC 6901 JSON String tests', function () { const jsonStringRepEntries = [ - ['""', data], - ['"/foo"', ['bar', 'baz']], - ['"/foo/0"', 'bar'], - ['"/"', 0], - ['"/a~1b"', 1], - ['"/c%d"', 2], - ['"/e^f"', 3], - ['"/g|h"', 4], - ['"/i\\\\j"', 5], - ['"/k\\"l"', 6], - ['"/ "', 7], - ['"/m~0n"', 8], + ['', data], + ['/foo', ['bar', 'baz']], + ['/foo/0', 'bar'], + ['/', 0], + ['/a~1b', 1], + ['/c%d', 2], + ['/e^f', 3], + ['/g|h', 4], + ['/i\\j', 5], + ['/k"l', 6], + ['/ ', 7], + ['/m~0n', 8], ]; jsonStringRepEntries.forEach(([jsonString, expected]) => { specify('should correctly evaluate JSON Pointer from JSON String', function () { - assert.deepEqual(evaluate(data, JSONString.from(jsonString)), expected); + assert.deepEqual(evaluate(data, jsonString), expected); }); }); }); - context('RCC 6901 JSON String tests', function () { - const jsonStringRepEntries = [ + context('RFC 6901 JSON String tests', function () { + const fragmentRepEntries = [ ['#', data], ['#/foo', ['bar', 'baz']], ['#/foo/0', 'bar'], @@ -65,8 +64,8 @@ describe('evaluate', function () { ['#/m~0n', 8], ]; - jsonStringRepEntries.forEach(([fragment, expected]) => { - specify('should correctly evaluate JSON Pointer from JSON String', function () { + fragmentRepEntries.forEach(([fragment, expected]) => { + specify('should correctly evaluate JSON Pointer from URI Fragment Identifier', function () { assert.deepEqual(evaluate(data, URIFragmentIdentifier.from(fragment)), expected); }); }); diff --git a/test/evaluate/realms/map-set.js b/test/evaluate/realms/map-set.js new file mode 100644 index 0000000..7749874 --- /dev/null +++ b/test/evaluate/realms/map-set.js @@ -0,0 +1,188 @@ +import { assert } from 'chai'; + +import { + evaluate, + JSONPointerIndexError, + JSONPointerTypeError, + JSONPointerKeyError, + JSONPointerEvaluateError, + URIFragmentIdentifier, +} from '../../../src/index.js'; +import MapSetEvaluationRealm from '../../../src/evaluate/realms/map-set.js'; + +describe('evaluate', function () { + const realm = new MapSetEvaluationRealm(); + const data = new Map([ + ['foo', new Set(['bar', 'baz'])], + ['', 0], + ['a/b', 1], + ['c%d', 2], + ['e^f', 3], + ['g|h', 4], + ['i\\j', 5], + ['k"l', 6], + [' ', 7], + ['m~n', 8], + ]); + + context('RCC 6901 JSON String tests', function () { + const jsonStringRepEntries = [ + ['', data], + ['/foo', new Set(['bar', 'baz'])], + ['/foo/0', 'bar'], + ['/', 0], + ['/a~1b', 1], + ['/c%d', 2], + ['/e^f', 3], + ['/g|h', 4], + ['/i\\j', 5], + ['/k"l', 6], + ['/ ', 7], + ['/m~0n', 8], + ]; + + jsonStringRepEntries.forEach(([jsonString, expected]) => { + specify('should correctly evaluate JSON Pointer from JSON String', function () { + assert.deepEqual(evaluate(data, jsonString, { realm }), expected); + }); + }); + }); + + context('RCC 6901 JSON String tests', function () { + const fragmentRepEntries = [ + ['#', data], + ['#/foo', new Set(['bar', 'baz'])], + ['#/foo/0', 'bar'], + ['#/', 0], + ['#/a~1b', 1], + ['#/c%25d', 2], + ['#/e%5Ef', 3], + ['#/g%7Ch', 4], + ['#/i%5Cj', 5], + ['#/k%22l', 6], + ['#/%20', 7], + ['#/m~0n', 8], + ]; + + fragmentRepEntries.forEach(([fragment, expected]) => { + specify('should correctly evaluate JSON Pointer from URI Fragment Identifier', function () { + assert.deepEqual(evaluate(data, URIFragmentIdentifier.from(fragment), { realm }), expected); + }); + }); + }); + + context('valid JSON Pointers', function () { + specify('should return entire document for ""', function () { + assert.deepEqual(evaluate(data, '', { realm }), data); + }); + + specify('should return array ["bar", "baz"] for "/foo"', function () { + assert.deepEqual(evaluate(data, '/foo', { realm }), new Set(['bar', 'baz'])); + }); + + specify('should return "bar" for "/foo/0"', function () { + assert.strictEqual(evaluate(data, '/foo/0', { realm }), 'bar'); + }); + + specify('should return 0 for "/"', function () { + assert.strictEqual(evaluate(data, '/', { realm }), 0); + }); + + specify('should return 1 for "/a~1b"', function () { + assert.strictEqual(evaluate(data, '/a~1b', { realm }), 1); + }); + + specify('should return 2 for "/c%d"', function () { + assert.strictEqual(evaluate(data, '/c%d', { realm }), 2); + }); + + specify('should return 3 for "/e^f"', function () { + assert.strictEqual(evaluate(data, '/e^f', { realm }), 3); + }); + + specify('should return 4 for "/g|h"', function () { + assert.strictEqual(evaluate(data, '/g|h', { realm }), 4); + }); + + specify('should return 5 for "/i\\j"', function () { + assert.strictEqual(evaluate(data, '/i\\j', { realm }), 5); + }); + + specify('should return 6 for "/k\"l"', function () { + assert.strictEqual(evaluate(data, '/k"l', { realm }), 6); + }); + + specify('should return 7 for "/ "', function () { + assert.strictEqual(evaluate(data, '/ ', { realm }), 7); + }); + + specify('should return 8 for "/m~0n"', function () { + assert.strictEqual(evaluate(data, '/m~0n', { realm }), 8); + }); + }); + + context('invalid JSON Pointers (should throw errors)', function () { + specify('should throw JSONPointerEvaluateError for invalid JSON Pointer', function () { + assert.throws(() => evaluate(data, 'invalid-pointer', { realm }), JSONPointerEvaluateError); + }); + + specify( + 'should throw JSONPointerTypeError for accessing property on non-object/array', + function () { + assert.throws(() => evaluate(data, '/foo/0/bad', { realm }), JSONPointerTypeError); + }, + ); + + specify('should throw JSONPointerKeyError for non-existing key', function () { + assert.throws(() => evaluate(data, '/nonexistent', { realm }), JSONPointerKeyError); + }); + + specify('should throw JSONPointerIndexError for non-numeric array index', function () { + assert.throws(() => evaluate(data, '/foo/x', { realm }), JSONPointerIndexError); + }); + + specify('should throw JSONPointerIndexError for out-of-bounds array index', function () { + assert.throws(() => evaluate(data, '/foo/5', { realm }), JSONPointerIndexError); + }); + + specify('should throw JSONPointerIndexError for leading zero in array index', function () { + assert.throws(() => evaluate(data, '/foo/01', { realm }), JSONPointerIndexError); + }); + + specify('should throw JSONPointerIndexError for "-" when strictArrays is true', function () { + assert.throws( + () => evaluate(data, '/foo/-', { strictArrays: true, realm }), + JSONPointerIndexError, + ); + }); + + specify('should return undefined for "-" when strictArrays is false', function () { + assert.strictEqual(evaluate(data, '/foo/-', { strictArrays: false, realm }), undefined); + }); + + specify( + 'should throw JSONPointerKeyError for accessing chain of object properties that do not exist', + function () { + assert.throws(() => evaluate(data, '/missing/key', { realm }), JSONPointerKeyError); + }, + ); + + specify( + 'should return undefined accessing object property that does not exist when strictObject is false', + function () { + assert.isUndefined(evaluate(data, '/missing', { strictObjects: false, realm })); + }, + ); + + specify('should throw JSONPointerTypeError when evaluating on primitive', function () { + assert.throws(() => evaluate('not-an-object', '/foo', { realm }), JSONPointerTypeError); + }); + + specify( + 'should throw JSONPointerTypeError when trying to access deep path on primitive', + function () { + assert.throws(() => evaluate({ foo: 42 }, '/foo/bar', { realm }), JSONPointerTypeError); + }, + ); + }); +}); From abcbd849795bc905554e55d51d100ccee2fae824 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Mon, 17 Mar 2025 22:30:41 +0100 Subject: [PATCH 2/4] test: fix --- test/evaluate/index.js | 2 +- test/evaluate/realms/map-set.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/evaluate/index.js b/test/evaluate/index.js index 0515c37..944c7a2 100644 --- a/test/evaluate/index.js +++ b/test/evaluate/index.js @@ -48,7 +48,7 @@ describe('evaluate', function () { }); }); - context('RFC 6901 JSON String tests', function () { + context('RFC 6901 URI Fragment Identifier tests', function () { const fragmentRepEntries = [ ['#', data], ['#/foo', ['bar', 'baz']], diff --git a/test/evaluate/realms/map-set.js b/test/evaluate/realms/map-set.js index 7749874..9dca6fb 100644 --- a/test/evaluate/realms/map-set.js +++ b/test/evaluate/realms/map-set.js @@ -25,7 +25,7 @@ describe('evaluate', function () { ['m~n', 8], ]); - context('RCC 6901 JSON String tests', function () { + context('RFC 6901 JSON String tests', function () { const jsonStringRepEntries = [ ['', data], ['/foo', new Set(['bar', 'baz'])], @@ -48,7 +48,7 @@ describe('evaluate', function () { }); }); - context('RCC 6901 JSON String tests', function () { + context('RFC 6901 URI Fragment Identifier tests', function () { const fragmentRepEntries = [ ['#', data], ['#/foo', new Set(['bar', 'baz'])], From d9bfeb77730f561eec26d3876ce33c94be9073c4 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Mon, 17 Mar 2025 22:31:53 +0100 Subject: [PATCH 3/4] test: fix --- test/evaluate/realms/map-set.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/evaluate/realms/map-set.js b/test/evaluate/realms/map-set.js index 9dca6fb..4d28293 100644 --- a/test/evaluate/realms/map-set.js +++ b/test/evaluate/realms/map-set.js @@ -76,7 +76,7 @@ describe('evaluate', function () { assert.deepEqual(evaluate(data, '', { realm }), data); }); - specify('should return array ["bar", "baz"] for "/foo"', function () { + specify('should return Set(["bar", "baz"]) for "/foo"', function () { assert.deepEqual(evaluate(data, '/foo', { realm }), new Set(['bar', 'baz'])); }); From c47de2725e0035389aa6b35ac0a5331595723747 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Mon, 17 Mar 2025 22:34:05 +0100 Subject: [PATCH 4/4] feat: add types --- types/evaluate/realms/map-set.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 types/evaluate/realms/map-set.d.ts diff --git a/types/evaluate/realms/map-set.d.ts b/types/evaluate/realms/map-set.d.ts new file mode 100644 index 0000000..d80f77c --- /dev/null +++ b/types/evaluate/realms/map-set.d.ts @@ -0,0 +1,13 @@ +import type { EvaluationRealm } from '../../index'; + +declare class MapSetEvaluationRealm extends EvaluationRealm { + public readonly name: 'map-set'; + + public override isArray(node: unknown): node is Set; + public override isObject(node: unknown): node is Map; + public override sizeOf(node: unknown): number; + public override has(node: unknown, referenceToken: string): boolean; + public override evaluate(node: unknown, referenceToken: string): T; +} + +export default MapSetEvaluationRealm;