diff --git a/CHANGELOG.md b/CHANGELOG.md index c86fd61..67588ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ **Features** -- Implement [Relative JSON Pointers](https://www.ietf.org/id/draft-hha-relative-json-pointer-00.html). Use the `to(rel)` method of `JSONPointer`, where `rel` is a relative JSON pointer string and a new `JSONPointer` is returned. +- Implemented [Relative JSON Pointers](https://www.ietf.org/id/draft-hha-relative-json-pointer-00.html). Use the `to(rel)` method of `JSONPointer`, where `rel` is a relative JSON pointer string and a new `JSONPointer` is returned. - Guard against recursive data structures by implementing the `JSONPathEnvironment.maxRecursionDepth` option. When using the recursive descent selector (`..`), if the maximum recursion depth is reached, a `JSONPathRecursionLimitError` is thrown. +- Added `JSONPathEnvironment.match()` and `JSONPath.match()`, which returns a `JSONPathNode` for the first value matching a query, or `undefined` if there are no matches. # Version 0.1.1 diff --git a/package.json b/package.json index fb76a0b..a303b2b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build": "npm run build:types && npm run build:js", "type-check": "tsc --noEmit", "coverage": "jest --collectCoverage", - "clean": "rm -rf ./lib ./dist ./coverage", + "clean": "rm -rf ./lib ./dist ./coverage ./docs/docs/api", "lint": "eslint src tests --ext .js,.jsx,.ts,.tsx" }, "browserslist": [ diff --git a/src/path/environment.ts b/src/path/environment.ts index 2485cea..baf37c7 100644 --- a/src/path/environment.ts +++ b/src/path/environment.ts @@ -13,7 +13,7 @@ import { Match as MatchFilterFunction } from "./functions/match"; import { Search as SearchFilterFunction } from "./functions/search"; import { Value as ValueFilterFunction } from "./functions/value"; import { tokenize } from "./lex"; -import { JSONPathNodeList } from "./node"; +import { JSONPathNode, JSONPathNodeList } from "./node"; import { Parser } from "./parse"; import { JSONPath } from "./path"; import { Token, TokenStream } from "./token"; @@ -128,7 +128,18 @@ export class JSONPathEnvironment { return this.compile(path).query(value); } - // TODO: match(path, value): boolean + /** + * Return a {@link JSONPathNode} instance for the first object found in + * _value_ matching _path_. + * + * @param path - A JSONPath query. + * @param value - JSON-like data to which the query _path_ will be applied. + * @returns The first node in _value_ matching _path_, or `undefined` if + * there are no matches. + */ + public match(path: string, value: JSONValue): JSONPathNode | undefined { + return this.compile(path).match(value); + } protected setupFilterFunctions(): void { this.functionRegister.set("count", new CountFilterFunction()); diff --git a/src/path/index.ts b/src/path/index.ts index 7401d1e..4f0079f 100644 --- a/src/path/index.ts +++ b/src/path/index.ts @@ -1,6 +1,6 @@ import { JSONValue } from "../types"; import { JSONPathEnvironment } from "./environment"; -import { JSONPathNodeList } from "./node"; +import { JSONPathNode, JSONPathNodeList } from "./node"; import { JSONPath } from "./path"; export { JSONPathEnvironment } from "./environment"; @@ -64,3 +64,19 @@ export function query(path: string, value: JSONValue): JSONPathNodeList { export function compile(path: string): JSONPath { return DEFAULT_ENVIRONMENT.compile(path); } + +/** + * Return a {@link JSONPathNode} instance for the first object found in + * _value_ matching _path_. + * + * @param path - A JSONPath query. + * @param value - JSON-like data to which the query _path_ will be applied. + * @returns The first node in _value_ matching _path_, or `undefined` if + * there are no matches. + */ +export function match( + path: string, + value: JSONValue, +): JSONPathNode | undefined { + return DEFAULT_ENVIRONMENT.match(path, value); +} diff --git a/src/path/path.ts b/src/path/path.ts index 5c40d5c..5aecc3f 100644 --- a/src/path/path.ts +++ b/src/path/path.ts @@ -35,6 +35,18 @@ export class JSONPath { return nodes; } + /** + * Return a {@link JSONPathNode} instance for the first object found in + * _value_ matching this query. + * + * @param value - JSON-like data to which this query will be applied. + * @returns The first node in _value_ matching this query, or `undefined` if + * there are no matches. + */ + public match(value: JSONValue): JSONPathNode | undefined { + return this.query(value).nodes.at(0); + } + /** * */ diff --git a/src/pointer/pointer.ts b/src/pointer/pointer.ts index 226ed6d..346f060 100644 --- a/src/pointer/pointer.ts +++ b/src/pointer/pointer.ts @@ -71,8 +71,6 @@ export class JSONPointer { } } - // TODO: add `resolveWithFallback` to handle explicit `undefined` - /** * * @param value - diff --git a/tests/path/api.test.ts b/tests/path/api.test.ts index f0cdd55..a6b711c 100644 --- a/tests/path/api.test.ts +++ b/tests/path/api.test.ts @@ -1,4 +1,4 @@ -import { JSONPathEnvironment } from "../../src/path"; +import { JSONPathEnvironment, JSONPathNode } from "../../src/path"; describe("JSONPath API", () => { const env = new JSONPathEnvironment(); @@ -65,4 +65,15 @@ describe("JSONPath API", () => { "/some/bar/0", ]); }); + test("pointer from empty node", () => { + const node = new JSONPathNode("", [], {}); + expect(node.toPointer().toString()).toBe(""); + }); + test("match first available node", () => { + const node = env.match("$.foo", { foo: [1, 2, 3] }); + expect(node).toBeInstanceOf(JSONPathNode); + if (node) { + expect(node.value).toStrictEqual([1, 2, 3]); + } + }); });