-
Notifications
You must be signed in to change notification settings - Fork 0
Extract JSON pointer primitives from vc-query. #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
6141627
Add JSON pointer matching and conversion primitives.
applesnort c5e12a1
Test the pointer primitives, not just the matcher.
applesnort 5abc80f
Refuse the inputs that used to match everything.
applesnort b5e23ed
Keep `@context` ordered when matching, and target 1.0.0.
applesnort bf3fe2b
Prefer an empty map as the wildcard.
applesnort eab4505
Cut cross-references out of comments and docs.
applesnort 093b1ad
Run the same specs in a browser as well as node.
applesnort 5c60db1
Address review: headers, config, licence id, readme.
applesnort a42a3d7
Add lint, test and coverage workflow.
applesnort 71a6fd6
Grant contents read so checkout can clone.
applesnort 87a8a4b
Cleanup copyright line.
davidlehn 7b85026
Revert to permissions for public repo.
davidlehn 4f7baaf
Omit package lock file for library.
davidlehn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| green |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| name: Main CI | ||
|
|
||
| on: [push] | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| lint: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| strategy: | ||
| matrix: | ||
| node-version: [24.x] | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - run: npm install | ||
| - name: Run eslint | ||
| run: npm run lint | ||
| test-node: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| strategy: | ||
| matrix: | ||
| node-version: [22.x, 24.x, 26.x] | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Install with Node.js 24.x | ||
| uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: 24.x | ||
| - run: npm install | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - name: Run tests with Node.js ${{ matrix.node-version }} | ||
| run: npm run test-node | ||
| test-browser: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| strategy: | ||
| matrix: | ||
| node-version: [24.x] | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - run: npm install | ||
| - name: Cache Playwright browsers | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: ~/.cache/ms-playwright | ||
| key: playwright-${{ runner.os }}-${{ hashFiles('package.json') }} | ||
| - name: Install Playwright browsers | ||
| run: npx playwright install --with-deps chromium | ||
| - name: Run browser tests | ||
| run: npm run test-browser | ||
| coverage: | ||
| runs-on: ubuntu-latest | ||
| # coverage runs the browser project too, so a browser is required | ||
| timeout-minutes: 15 | ||
| strategy: | ||
| matrix: | ||
| node-version: [24.x] | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - run: npm install | ||
| - name: Cache Playwright browsers | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: ~/.cache/ms-playwright | ||
| key: playwright-${{ runner.os }}-${{ hashFiles('package.json') }} | ||
| - name: Install Playwright browsers | ||
| run: npx playwright install --with-deps chromium | ||
| - name: Generate coverage report | ||
| run: npm run coverage-ci | ||
| - name: Upload coverage to Codecov | ||
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | ||
| with: | ||
| files: ./coverage/lcov.info | ||
| fail_ci_if_error: true | ||
| token: ${{ secrets.CODECOV_TOKEN }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| node_modules | ||
| coverage | ||
| .nyc_output | ||
| reports | ||
| .cache | ||
| scratchpad/ | ||
| *.log | ||
| *.tgz | ||
| .eslintcache | ||
| .vscode | ||
| .project | ||
| .settings | ||
| TAGS | ||
| *~ | ||
| *.sw[nop] | ||
| .DS_Store |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package-lock=false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # @digitalbazaar/json-pointer-primitives ChangeLog | ||
|
|
||
| ## 1.0.0 - TBD | ||
|
|
||
| ### Added | ||
| - Initial release. | ||
| - `matches({object, map})` — tests any object against a map of JSON pointers to | ||
| expected values. Every entry must match; a `Set` as a value means any of its | ||
| members may match; an empty `Map`, `Set` or string is a wildcard. An array is | ||
| the ordered `@context` case, where each element must equal the element at the | ||
| same index and the object may carry more. | ||
| - Prefer an empty `Map` as the wildcard — `{}` in a JSON-LD example, matching | ||
| JSON-LD Framing. The empty string means the same for QueryByExample | ||
| compatibility, but conflates "any value" with "the empty string". | ||
| - `toJsonPointerMap({obj, flat})` and `fromJsonPointerMap({map})` — build a | ||
| pointer map from an object and rebuild an object from one. Non-flat output | ||
| expresses arrays as a `Set`, except at `@context`, which stays ordered. | ||
| - `resolvePointer(obj, pointer)` — reads one pointer against own properties. | ||
| - `isObject`, `isNumber`, `toIntegerIfInteger`, `assert`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| Copyright (c) 2026 Digital Bazaar, Inc. | ||
|
|
||
| Redistribution and use in source and binary forms, with or without | ||
| modification, are permitted provided that the following conditions are met: | ||
|
|
||
| 1. Redistributions of source code must retain the above copyright notice, | ||
| this list of conditions and the following disclaimer. | ||
|
|
||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||
| this list of conditions and the following disclaimer in the documentation | ||
| and/or other materials provided with the distribution. | ||
|
|
||
| 3. Neither the name of the copyright holder nor the names of its contributors | ||
| may be used to endorse or promote products derived from this software | ||
| without specific prior written permission. | ||
|
|
||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # @digitalbazaar/json-pointer-primitives | ||
|
|
||
| Building blocks for matching objects against JSON pointers: convert a nested | ||
| object into a map of pointers to values, resolve a pointer, and test whether an | ||
| object satisfies such a map. | ||
|
|
||
| It does not know what it is matching. Give it any object and any map. | ||
|
|
||
| ```js | ||
| import {matches} from '@digitalbazaar/json-pointer-primitives'; | ||
|
|
||
| matches({ | ||
| object: credential, | ||
| map: new Map([ | ||
| ['/type', 'MovieTicketCredential'], | ||
| ['/issuer/id', 'did:example:issuer'] | ||
| ]) | ||
| }); | ||
| // true — every entry matched | ||
| ``` | ||
|
|
||
| One runtime dependency, `json-pointer`. Runs on node and in a browser. | ||
|
|
||
| ## Semantics | ||
|
applesnort marked this conversation as resolved.
|
||
|
|
||
| Every entry in the map must match, so entries are a conjunction: | ||
|
|
||
| ```js | ||
| const credential = { | ||
| type: ['VerifiableCredential', 'MovieTicketCredential'], | ||
| issuer: {id: 'did:example:issuer'}, | ||
| credentialSubject: {seat: 'A1', row: 12} | ||
| }; | ||
|
|
||
| matches({object: credential, map: new Map([ | ||
| ['/issuer/id', 'did:example:issuer'], | ||
| ['/credentialSubject/seat', 'A1'] | ||
| ])}); // true | ||
|
|
||
| matches({object: credential, map: new Map([ | ||
| ['/issuer/id', 'did:example:issuer'], | ||
| ['/credentialSubject/seat', 'B2'] | ||
| ])}); // false — one entry failed | ||
| ``` | ||
|
|
||
| A `Set` value is an alternation, nested inside that conjunction — any member | ||
| may match: | ||
|
|
||
| ```js | ||
| new Map([['/credentialSubject/seat', new Set(['A1', 'B2'])]]); // either seat | ||
| ``` | ||
|
|
||
| A pointer that resolves to an array matches if any element does: | ||
|
|
||
| ```js | ||
| new Map([['/type', 'MovieTicketCredential']]); // true — one of two types | ||
| ``` | ||
|
|
||
| An empty `Map` is a wildcard: any value, so long as the pointer resolves. An | ||
| empty `Set` and `''` mean the same; prefer the empty `Map`, which is what `{}` | ||
| in an example becomes. | ||
|
|
||
| ```js | ||
| new Map([['/issuer/id', new Map()]]); // true — an issuer id exists | ||
| new Map([['/absent', new Map()]]); // false — nothing there | ||
| ``` | ||
|
|
||
| A numeric string and a number compare equal by default: | ||
|
|
||
| ```js | ||
| new Map([['/credentialSubject/row', '12']]); // true | ||
| matches({object: credential, map, options: {coerceNumbers: false}}); // false | ||
| ``` | ||
|
|
||
| `@context` is the exception to array handling: it stays ordered, and each | ||
| element must equal the element at the same index. The object may carry more. | ||
|
|
||
| A pointer that resolves to nothing never matches. Pointers read own properties | ||
| only, so `/constructor` and `/toString` resolve to nothing. | ||
|
|
||
| ## Building a map | ||
|
|
||
| From a nested example that mirrors the object's shape: | ||
|
|
||
| ```js | ||
| toJsonPointerMap({obj: {credentialSubject: {name: 'John Doe'}}}); | ||
| // Map { '/credentialSubject/name' => 'John Doe' } | ||
| ``` | ||
|
|
||
| or by hand, from flat pointers: | ||
|
|
||
| ```js | ||
| new Map([ | ||
| ['/renderSuite', 'html'], | ||
| ['/template/mediaType', 'text/html'] | ||
| ]); | ||
| ``` | ||
|
|
||
| `fromJsonPointerMap({map})` rebuilds an object from a map. | ||
|
|
||
| ```js | ||
| resolvePointer({issuer: {id: 'did:example:issuer'}}, '/issuer/id'); | ||
| // 'did:example:issuer' | ||
| ``` | ||
|
|
||
| ## License | ||
|
|
||
| See [LICENSE.md](./LICENSE.md). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /*! | ||
| * Copyright (c) 2026 Digital Bazaar, Inc. | ||
| */ | ||
| import universalConfig | ||
| from '@digitalbazaar/eslint-config/universal-recommended'; | ||
|
|
||
| export default [ | ||
| ...universalConfig | ||
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /*! | ||
| * Copyright (c) 2026 Digital Bazaar, Inc. | ||
| */ | ||
| export {matches} from './match.js'; | ||
| export { | ||
| assert, fromJsonPointerMap, isNumber, isObject, resolvePointer, | ||
| toIntegerIfInteger, toJsonPointerMap | ||
| } from './util.js'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /*! | ||
| * Copyright (c) 2025-2026 Digital Bazaar, Inc. | ||
| */ | ||
| import {assert, isObject, resolvePointer, toIntegerIfInteger} from './util.js'; | ||
|
|
||
| /** | ||
| * Returns whether an object matches against a JSON pointer map. | ||
| * | ||
| * The map is a `Map` of JSON pointer to expected value. Every entry must | ||
| * match. A `Set` as a value means any of its members may match, so alternation | ||
| * nests inside conjunction. | ||
| * | ||
| * An empty `Map`, `Set` or string is a wildcard: any value, but the pointer | ||
| * must still resolve. Prefer the empty `Map` -- `{}` in a JSON-LD example. | ||
| * The empty string means the same, for QueryByExample compatibility. | ||
| * | ||
| * Nothing here knows what it is matching. Callers build the map from whatever | ||
| * they have -- a QueryByExample `example`, a DCQL credential query, a | ||
| * Presentation Exchange input descriptor, or a hand-written set of pointers -- | ||
| * and match credentials, render methods or anything else against it. Building | ||
| * the map once and reusing it across candidates is the cheaper order. | ||
| * | ||
| * @param {object} options - The options. | ||
| * @param {object} options.object - The object to try to match. | ||
| * @param {Map} options.map - The JSON pointer map. | ||
| * @param {object} [options.options] - Match options: | ||
| * [coerceNumbers=true] - Numeric strings and numbers compare equal. | ||
| * | ||
| * @returns {boolean} `true` if the object matches, `false` if not. | ||
| */ | ||
| export function matches({object, map, options = {coerceNumbers: true}} = {}) { | ||
|
dlongley marked this conversation as resolved.
|
||
| // a bad `map` must not read as "matched everything" | ||
| assert(map, 'map', Map); | ||
| // only an object can match | ||
| if(!isObject(object)) { | ||
| return false; | ||
| } | ||
| return _match({cursor: object, matchValue: map, options}); | ||
| } | ||
|
|
||
| function _match({cursor, matchValue, options}) { | ||
| // handle wildcard matching | ||
| if(_isWildcard(matchValue)) { | ||
| return true; | ||
| } | ||
|
|
||
| if(matchValue instanceof Set) { | ||
| // some element in the set must match `cursor` | ||
| return [...matchValue].some(e => _match({cursor, matchValue: e, options})); | ||
| } | ||
|
|
||
| if(matchValue instanceof Map) { | ||
| // all pointers and values in the map must match `cursor` | ||
| return [...matchValue.entries()].every(([pointer, matchValue]) => { | ||
| const value = resolvePointer(cursor, pointer); | ||
| if(value === undefined) { | ||
| // no value at `pointer`; no match | ||
| return false; | ||
| } | ||
| // handles case where `value` is an empty array + wildcard `matchValue` | ||
| if(_isWildcard(matchValue)) { | ||
| return true; | ||
| } | ||
| // normalize value to an array for matching | ||
| const values = Array.isArray(value) ? value : [value]; | ||
| // `matchValue` can only be an array for the `@context` case | ||
| if(Array.isArray(matchValue)) { | ||
| // each element of `matchValue` must be equal to the element in | ||
| // `values` at the same index (note: `values` may have more elements | ||
| // than `matchValue` and still match) | ||
| return matchValue.every((mv, i) => values[i] === mv); | ||
| } | ||
| // handle matching each individual value on its own | ||
| return values.some(v => _match({cursor: v, matchValue, options})); | ||
| }); | ||
| } | ||
|
|
||
| // primitive comparison | ||
| if(cursor === matchValue) { | ||
| return true; | ||
| } | ||
|
|
||
| // string/number coercion | ||
| if(options.coerceNumbers) { | ||
| const cursorNumber = toIntegerIfInteger(cursor); | ||
| const matchNumber = toIntegerIfInteger(matchValue); | ||
| return cursorNumber !== undefined && cursorNumber === matchNumber; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| function _isWildcard(value) { | ||
| // by type, not a bare `size`, which any object can carry | ||
| return value === '' || | ||
| (value instanceof Map && value.size === 0) || | ||
| (value instanceof Set && value.size === 0); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.