From 8010c0e3ac43c86c33887f1ea5c94b790169da1e Mon Sep 17 00:00:00 2001 From: James Prior Date: Mon, 9 Dec 2024 19:42:09 +0000 Subject: [PATCH] Refactor to model JSONPath segments explicitly --- src/path/extra/selectors.ts | 170 +++++------ src/path/parse.ts | 136 ++++----- src/path/path.ts | 37 ++- src/path/segments.ts | 227 +++++++++++++++ src/path/selectors.ts | 547 ++++++++---------------------------- 5 files changed, 511 insertions(+), 606 deletions(-) create mode 100644 src/path/segments.ts diff --git a/src/path/extra/selectors.ts b/src/path/extra/selectors.ts index 822b808..e1bfd28 100644 --- a/src/path/extra/selectors.ts +++ b/src/path/extra/selectors.ts @@ -1,4 +1,4 @@ -import { isArray, isObject } from "../../types"; +import { isArray, isObject, isString } from "../../types"; import { JSONPathEnvironment } from "../environment"; import { LogicalExpression } from "../expression"; import { JSONPathNode } from "../node"; @@ -11,45 +11,41 @@ export class KeySelector extends JSONPathSelector { readonly environment: JSONPathEnvironment, readonly token: Token, readonly key: string, - readonly shorthand: boolean = false, ) { super(environment, token); } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (node.value instanceof String || isArray(node.value)) continue; - if (isObject(node.value) && hasStringKey(node.value, this.key)) { - rv.push( - new JSONPathNode( - this.key, - node.location.concat(`${KEY_MARK}${this.key}`), - node.root, - ), - ); - } + if (node.value instanceof String || isArray(node.value)) return rv; + if (isObject(node.value) && hasStringKey(node.value, this.key)) { + rv.push( + new JSONPathNode( + this.key, + node.location.concat(`${KEY_MARK}${this.key}`), + node.root, + ), + ); } return rv; } - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (node.value instanceof String || isArray(node.value)) continue; - if (isObject(node.value) && hasStringKey(node.value, this.key)) { - yield new JSONPathNode( - this.key, - node.location.concat(`${KEY_MARK}${this.key}`), - node.root, - ); - } + public *lazyResolve(node: JSONPathNode): Generator { + if ( + !isString(node.value) && + isObject(node.value) && + hasStringKey(node.value, this.key) + ) { + yield new JSONPathNode( + this.key, + node.location.concat(`${KEY_MARK}${this.key}`), + node.root, + ); } } public toString(): string { - return this.shorthand - ? `[~'${this.key.replaceAll("'", "\\'")}']` - : `~'${this.key.replaceAll("'", "\\'")}'`; + return `~'${this.key.replaceAll("'", "\\'")}'`; } } @@ -60,47 +56,41 @@ export class KeysSelector extends JSONPathSelector { constructor( readonly environment: JSONPathEnvironment, readonly token: Token, - readonly shorthand: boolean = false, ) { super(environment, token); } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (node.value instanceof String || isArray(node.value)) continue; - if (isObject(node.value)) { - for (const [key, _] of this.environment.entries(node.value)) { - rv.push( - new JSONPathNode( - key, - node.location.concat(`${KEY_MARK}${key}`), - node.root, - ), - ); - } + if (node.value instanceof String || isArray(node.value)) return rv; + if (isObject(node.value)) { + for (const [key, _] of this.environment.entries(node.value)) { + rv.push( + new JSONPathNode( + key, + node.location.concat(`${KEY_MARK}${key}`), + node.root, + ), + ); } } return rv; } - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (node.value instanceof String || isArray(node.value)) continue; - if (isObject(node.value)) { - for (const [key, _] of this.environment.entries(node.value)) { - yield new JSONPathNode( - key, - node.location.concat(`${KEY_MARK}${key}`), - node.root, - ); - } + public *lazyResolve(node: JSONPathNode): Generator { + if (isObject(node.value) && !isString(node.value) && !isArray(node.value)) { + for (const [key, _] of this.environment.entries(node.value)) { + yield new JSONPathNode( + key, + node.location.concat(`${KEY_MARK}${key}`), + node.root, + ); } } } public toString(): string { - return this.shorthand ? "[~]" : "~"; + return "~"; } } @@ -113,52 +103,48 @@ export class KeysFilterSelector extends JSONPathSelector { super(environment, token); } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (node.value instanceof String || isArray(node.value)) continue; - if (isObject(node.value)) { - for (const [key, value] of this.environment.entries(node.value)) { - const filterContext: FilterContext = { - environment: this.environment, - currentValue: value, - rootValue: node.root, - currentKey: key, - }; - if (this.expression.evaluate(filterContext)) { - rv.push( - new JSONPathNode( - key, - node.location.concat(`${KEY_MARK}${key}`), - node.root, - ), - ); - } + if (node.value instanceof String || isArray(node.value)) return rv; + if (isObject(node.value)) { + for (const [key, value] of this.environment.entries(node.value)) { + const filterContext: FilterContext = { + environment: this.environment, + currentValue: value, + rootValue: node.root, + currentKey: key, + }; + if (this.expression.evaluate(filterContext)) { + rv.push( + new JSONPathNode( + key, + node.location.concat(`${KEY_MARK}${key}`), + node.root, + ), + ); } } } return rv; } - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (node.value instanceof String || isArray(node.value)) continue; - if (isObject(node.value)) { - for (const [key, value] of this.environment.entries(node.value)) { - const filterContext: FilterContext = { - environment: this.environment, - currentValue: value, - rootValue: node.root, - lazy: true, - currentKey: key, - }; - if (this.expression.evaluate(filterContext)) { - yield new JSONPathNode( - key, - node.location.concat(`${KEY_MARK}${key}`), - node.root, - ); - } + public *lazyResolve(node: JSONPathNode): Generator { + if (node.value instanceof String || isArray(node.value)) return; + if (isObject(node.value)) { + for (const [key, value] of this.environment.entries(node.value)) { + const filterContext: FilterContext = { + environment: this.environment, + currentValue: value, + rootValue: node.root, + lazy: true, + currentKey: key, + }; + if (this.expression.evaluate(filterContext)) { + yield new JSONPathNode( + key, + node.location.concat(`${KEY_MARK}${key}`), + node.root, + ); } } } diff --git a/src/path/parse.ts b/src/path/parse.ts index 0ccd019..b1e248c 100644 --- a/src/path/parse.ts +++ b/src/path/parse.ts @@ -17,16 +17,18 @@ import { import { FunctionExpressionType } from "./functions/function"; import { JSONPath } from "./path"; import { - BracketedSelection, - BracketedSegment, FilterSelector, IndexSelector, JSONPathSelector, NameSelector, - RecursiveDescentSegment, SliceSelector, WildcardSelector, } from "./selectors"; +import { + RecursiveDescentSegment, + ChildSegment, + JSONPathSegment, +} from "./segments"; import { Token, TokenKind, TokenStream } from "./token"; import { CurrentKey } from "./extra/expression"; import { @@ -90,78 +92,80 @@ export class Parser { ]); } - public parse(stream: TokenStream): JSONPathSelector[] { + public parse(stream: TokenStream): JSONPathSegment[] { if (stream.current.kind === TokenKind.ROOT) stream.next(); - const selectors = this.parsePath(stream); + const segments = this.parseQuery(stream); if (stream.current.kind !== TokenKind.EOF) { throw new JSONPathSyntaxError( `unexpected token '${stream.current.kind}'`, stream.current, ); } - return selectors; + return segments; } - protected parsePath( + protected parseQuery( stream: TokenStream, inFilter: boolean = false, - ): JSONPathSelector[] { - const selectors: JSONPathSelector[] = []; - for (;;) { - const selector = this.parseSegment(stream); - if (!selector) { - if (inFilter) { - stream.backup(); + ): JSONPathSegment[] { + const segments: JSONPathSegment[] = []; + loop: for (;;) { + switch (stream.current.kind) { + case TokenKind.DDOT: { + const token = stream.next(); + const selectors = this.parseSelectors(stream); + segments.push( + new RecursiveDescentSegment(this.environment, token, selectors), + ); + break; + } + case TokenKind.LBRACKET: + case TokenKind.KEY: + case TokenKind.KEYS: + case TokenKind.NAME: + case TokenKind.WILD: { + const token = stream.current; + const selectors = this.parseSelectors(stream); + segments.push(new ChildSegment(this.environment, token, selectors)); + break; + } + default: { + if (inFilter) stream.backup(); + break loop; } - break; } - selectors.push(selector); stream.next(); } - return selectors; + return segments; } - protected parseSegment(stream: TokenStream): JSONPathSelector | null { + protected parseSelectors(stream: TokenStream): JSONPathSelector[] { switch (stream.current.kind) { case TokenKind.NAME: - return new NameSelector( - this.environment, - stream.current, - stream.current.value, - true, - ); + return [ + new NameSelector( + this.environment, + stream.current, + stream.current.value, + ), + ]; case TokenKind.WILD: - return new WildcardSelector(this.environment, stream.current, true); + return [new WildcardSelector(this.environment, stream.current)]; case TokenKind.KEY: - return new KeySelector( - this.environment, - stream.current, - stream.current.value, - true, - ); - case TokenKind.KEYS: - return new KeysSelector(this.environment, stream.current, true); - case TokenKind.DDOT: { - const segmentToken = stream.current; - stream.next(); - const selector = this.parseSegment(stream); - if (!selector) { - throw new JSONPathSyntaxError( - "bald descendant segment", + return [ + new KeySelector( + this.environment, stream.current, - ); - } - return new RecursiveDescentSegment( - this.environment, - segmentToken, - selector, - ); - } + stream.current.value, + ), + ]; + case TokenKind.KEYS: + return [new KeysSelector(this.environment, stream.current)]; case TokenKind.LBRACKET: return this.parseBracketedSelection(stream); default: - return null; + return []; } } @@ -239,55 +243,55 @@ export class Parser { return new SliceSelector(this.environment, tok, ...indices); } - protected parseBracketedSelection(stream: TokenStream): BracketedSelection { + protected parseBracketedSelection(stream: TokenStream): JSONPathSelector[] { const token = stream.next(); - const items: BracketedSegment[] = []; + const selectors: JSONPathSelector[] = []; while (stream.current.kind !== TokenKind.RBRACKET) { switch (stream.current.kind) { case TokenKind.SINGLE_QUOTE_STRING: case TokenKind.DOUBLE_QUOTE_STRING: - items.push( + selectors.push( new NameSelector( this.environment, stream.current, this.decodeString(stream.current), - false, ), ); break; case TokenKind.FILTER: - items.push(this.parseFilter(stream)); + selectors.push(this.parseFilter(stream)); break; case TokenKind.INDEX: if (stream.peek.kind === TokenKind.COLON) { - items.push(this.parseSlice(stream)); + selectors.push(this.parseSlice(stream)); } else { - items.push(this.parseIndex(stream)); + selectors.push(this.parseIndex(stream)); } break; case TokenKind.COLON: - items.push(this.parseSlice(stream)); + selectors.push(this.parseSlice(stream)); break; case TokenKind.WILD: - items.push(new WildcardSelector(this.environment, stream.current)); + selectors.push( + new WildcardSelector(this.environment, stream.current), + ); break; case TokenKind.KEY_SINGLE_QUOTE_STRING: case TokenKind.KEY_DOUBLE_QUOTE_STRING: - items.push( + selectors.push( new KeySelector( this.environment, stream.current, this.decodeString(stream.current), - false, ), ); break; case TokenKind.KEYS_FILTER: - items.push(this.parseFilter(stream, true)); + selectors.push(this.parseFilter(stream, true)); break; case TokenKind.KEYS: - items.push(new KeysSelector(this.environment, stream.current)); + selectors.push(new KeysSelector(this.environment, stream.current)); break; case TokenKind.EOF: throw new JSONPathSyntaxError( @@ -310,11 +314,11 @@ export class Parser { stream.next(); } - if (!items.length) { + if (!selectors.length) { throw new JSONPathSyntaxError("empty bracketed segment", token); } - return new BracketedSelection(this.environment, token, items); + return selectors; } protected parseFilter( @@ -449,7 +453,7 @@ export class Parser { const tok = stream.next(); return new RootQuery( tok, - new JSONPath(this.environment, this.parsePath(stream, true)), + new JSONPath(this.environment, this.parseQuery(stream, true)), ); } @@ -457,7 +461,7 @@ export class Parser { const tok = stream.next(); return new RelativeQuery( tok, - new JSONPath(this.environment, this.parsePath(stream, true)), + new JSONPath(this.environment, this.parseQuery(stream, true)), ); } diff --git a/src/path/path.ts b/src/path/path.ts index cf46f50..28b350b 100644 --- a/src/path/path.ts +++ b/src/path/path.ts @@ -1,12 +1,8 @@ import { JSONPathEnvironment } from "./environment"; import { JSONPathNode, JSONPathNodeList } from "./node"; -import { - BracketedSelection, - IndexSelector, - JSONPathSelector, - NameSelector, -} from "./selectors"; +import { IndexSelector, NameSelector } from "./selectors"; import { JSONValue } from "../types"; +import { JSONPathSegment, RecursiveDescentSegment } from "./segments"; /** * @@ -15,11 +11,11 @@ export class JSONPath { /** * * @param environment - - * @param selectors - + * @param segments - */ constructor( readonly environment: JSONPathEnvironment, - readonly selectors: JSONPathSelector[], + readonly segments: JSONPathSegment[], ) {} /** @@ -29,8 +25,8 @@ export class JSONPath { */ public query(value: JSONValue): JSONPathNodeList { let nodes = [new JSONPathNode(value, [], value)]; - for (const selector of this.selectors) { - nodes = selector.resolve(nodes); + for (const segment of this.segments) { + nodes = segment.resolve(nodes); } return new JSONPathNodeList(nodes); } @@ -44,8 +40,8 @@ export class JSONPath { let nodes: IterableIterator = [ new JSONPathNode(value, [], value), ][Symbol.iterator](); - for (const selector of this.selectors) { - nodes = selector.lazyResolve(nodes); + for (const segment of this.segments) { + nodes = segment.lazyResolve(nodes); } return nodes; } @@ -69,19 +65,20 @@ export class JSONPath { * */ public toString(): string { - return `$${this.selectors.map((s) => s.toString()).join("")}`; + return `$${this.segments.map((s) => s.toString()).join("")}`; } public singularQuery(): boolean { - for (const selector of this.selectors) { - if (selector instanceof NameSelector) continue; + for (const segment of this.segments) { + if (segment instanceof RecursiveDescentSegment) return false; + if ( - selector instanceof BracketedSelection && - selector.items.length === 1 && - (selector.items[0] instanceof NameSelector || - selector.items[0] instanceof IndexSelector) - ) + segment.selectors.length === 1 && + (segment.selectors[0] instanceof NameSelector || + segment.selectors[0] instanceof IndexSelector) + ) { continue; + } return false; } return true; diff --git a/src/path/segments.ts b/src/path/segments.ts new file mode 100644 index 0000000..94960c4 --- /dev/null +++ b/src/path/segments.ts @@ -0,0 +1,227 @@ +import { isArray, isObject, isString } from "../types"; +import { JSONPathEnvironment } from "./environment"; +import { JSONPathRecursionLimitError } from "./errors"; +import { JSONPathNode } from "./node"; +import { JSONPathSelector } from "./selectors"; +import { Token } from "./token"; + +/** Base class for all JSONPath segments. Both shorthand and bracketed. */ +export abstract class JSONPathSegment { + constructor( + readonly environment: JSONPathEnvironment, + readonly token: Token, + readonly selectors: JSONPathSelector[], + ) {} + + /** + * @param nodes - Nodes matched by preceding segments. + */ + public abstract resolve(nodes: JSONPathNode[]): JSONPathNode[]; + + /** + * @param nodes - Nodes matched by preceding segments. + */ + public abstract lazyResolve( + nodes: Iterable, + ): Generator; + + /** + * Return a canonical string representation of this segment. + */ + public abstract toString(): string; +} + +/** The child selection segment. */ +export class ChildSegment extends JSONPathSegment { + public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + const rv: JSONPathNode[] = []; + for (const node of nodes) { + for (const selector of this.selectors) { + rv.push(...selector.resolve(node)); + } + } + return rv; + } + + public *lazyResolve(nodes: Iterable): Generator { + for (const node of nodes) { + for (const selector of this.selectors) { + yield* selector.resolve(node); + } + } + } + + public toString(): string { + return `[${this.selectors.map((s) => s.toString()).join(", ")}]`; + } +} + +/** The recursive descent segment. */ +export class RecursiveDescentSegment extends JSONPathSegment { + public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + const rv: JSONPathNode[] = []; + + const visitor = ( + this.environment.nondeterministic + ? this.nondeterministicVisit + : this.visit + ).bind(this); + + for (const node of nodes) { + for (const _node of visitor(node)) { + for (const selector of this.selectors) { + rv.push(...selector.resolve(_node)); + } + } + } + + return rv; + } + + public *lazyResolve(nodes: Iterable): Generator { + for (const node of nodes) { + for (const _node of this.visit(node)) { + for (const selector of this.selectors) { + yield* selector.resolve(_node); + } + } + } + } + + public toString(): string { + return `..[${this.selectors.map((s) => s.toString()).join(", ")}]`; + } + + private *visit( + node: JSONPathNode, + depth: number = 1, + ): Generator { + if (depth >= this.environment.maxRecursionDepth) { + throw new JSONPathRecursionLimitError( + "recursion limit reached", + this.token, + ); + } + + yield node; + + if (isArray(node.value)) { + for (let i = 0; i < node.value.length; i++) { + const _node = new JSONPathNode( + node.value[i], + node.location.concat(i), + node.root, + ); + yield* this.visit(_node, depth + 1); + } + } else if (isObject(node.value)) { + for (const [key, value] of this.environment.entries(node.value)) { + const _node = new JSONPathNode( + value, + node.location.concat(key), + node.root, + ); + yield* this.visit(_node, depth + 1); + } + } + } + + private *nondeterministicVisit( + root: JSONPathNode, + depth: number = 1, + ): Generator { + let queue: Array<[JSONPathNode, number]> = Array.from( + this.nondeterministicChildren(root), + ).map((node) => [node, depth]); + + yield root; + + while (queue.length) { + const [node, _depth] = queue.shift() as [JSONPathNode, number]; + yield node; + + if (_depth >= this.environment.maxRecursionDepth) { + throw new JSONPathRecursionLimitError( + "recursion limit reached", + this.token, + ); + } + + // Visit child nodes now or queue them for later? + const visitChildren = Math.random() < 0.5; + + for (const child of this.nondeterministicChildren(node)) { + if (visitChildren) { + yield child; + + const grandchildren: Array<[JSONPathNode, number]> = Array.from( + this.nondeterministicChildren(child), + ).map((n) => [n, _depth + 2]); + + queue = interleave(queue, grandchildren); + } else { + queue.push([child, _depth + 1]); + } + } + } + } + + private *nondeterministicChildren( + node: JSONPathNode, + ): Generator { + if (isString(node.value)) return; + if (isArray(node.value)) { + for (let i = 0; i < node.value.length; i++) { + yield new JSONPathNode( + node.value[i], + node.location.concat(i), + node.root, + ); + } + } else if (isObject(node.value)) { + for (const [key, value] of this.environment.entries(node.value)) { + yield new JSONPathNode(value, node.location.concat(key), node.root); + } + } + } +} + +/** + * Randomly interleave elements from two arrays while maintaining relative + * order of each input array. + * + * If _arrayA_ is empty, _arrayB_ is returned, and vice versa. + */ +function interleave(arrayA: T[], arrayB: U[]): Array { + if (arrayA.length === 0) { + return arrayB; + } + + if (arrayB.length === 0) { + return arrayA; + } + + // An array of iterators + const iterators: Array | Iterator> = []; + const itA = arrayA[Symbol.iterator](); + const itB = arrayB[Symbol.iterator](); + + for (let i = 0; i < arrayA.length; i++) { + iterators.push(itA); + } + + for (let i = 0; i < arrayB.length; i++) { + iterators.push(itB); + } + + shuffle(iterators); + return iterators.map((it) => it.next().value); +} + +function shuffle(entries: T[]): T[] { + for (let i = entries.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [entries[i], entries[j]] = [entries[j], entries[i]]; + } + return entries; +} diff --git a/src/path/selectors.ts b/src/path/selectors.ts index 9506add..82e530b 100644 --- a/src/path/selectors.ts +++ b/src/path/selectors.ts @@ -1,10 +1,10 @@ import { JSONPathEnvironment } from "./environment"; -import { JSONPathIndexError, JSONPathRecursionLimitError } from "./errors"; +import { JSONPathIndexError } from "./errors"; import { LogicalExpression } from "./expression"; import { JSONPathNode } from "./node"; import { Token } from "./token"; import { FilterContext, hasStringKey } from "./types"; -import { isArray, isObject, JSONValue } from "../types"; +import { isArray, isObject, isString, JSONValue } from "../types"; /** * Base class for all JSONPath segments and selectors. @@ -21,14 +21,12 @@ export abstract class JSONPathSelector { /** * @param nodes - Nodes matched by preceding selectors. */ - public abstract resolve(nodes: JSONPathNode[]): JSONPathNode[]; + public abstract resolve(node: JSONPathNode): JSONPathNode[]; /** * @param nodes - Nodes matched by preceding selectors. */ - public abstract lazyResolve( - nodes: Iterable, - ): Generator; + public abstract lazyResolve(nodes: JSONPathNode): Generator; /** * Return a canonical string representation of this selector. @@ -44,41 +42,36 @@ export class NameSelector extends JSONPathSelector { readonly environment: JSONPathEnvironment, readonly token: Token, readonly name: string, - readonly shorthand: boolean, ) { super(environment, token); } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (!isArray(node.value) && hasStringKey(node.value, this.name)) { - rv.push( - new JSONPathNode( - node.value[this.name], - node.location.concat(this.name), - node.root, - ), - ); - } + if (!isArray(node.value) && hasStringKey(node.value, this.name)) { + rv.push( + new JSONPathNode( + node.value[this.name], + node.location.concat(this.name), + node.root, + ), + ); } return rv; } - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (!isArray(node.value) && hasStringKey(node.value, this.name)) { - yield new JSONPathNode( - node.value[this.name], - node.location.concat(this.name), - node.root, - ); - } + public *lazyResolve(node: JSONPathNode): Generator { + if (!isArray(node.value) && hasStringKey(node.value, this.name)) { + yield new JSONPathNode( + node.value[this.name], + node.location.concat(this.name), + node.root, + ); } } public toString(): string { - return this.shorthand ? `['${this.name}']` : `'${this.name}'`; + return `'${this.name}'`; } } @@ -100,36 +93,32 @@ export class IndexSelector extends JSONPathSelector { } } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (isArray(node.value)) { - const normIndex = this.normalizedIndex(node.value.length); - if (normIndex in node.value) { - rv.push( - new JSONPathNode( - node.value[normIndex], - node.location.concat(normIndex), - node.root, - ), - ); - } + if (isArray(node.value)) { + const normIndex = this.normalizedIndex(node.value.length); + if (normIndex in node.value) { + rv.push( + new JSONPathNode( + node.value[normIndex], + node.location.concat(normIndex), + node.root, + ), + ); } } return rv; } - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (isArray(node.value)) { - const normIndex = this.normalizedIndex(node.value.length); - if (normIndex in node.value) { - yield new JSONPathNode( - node.value[normIndex], - node.location.concat(normIndex), - node.root, - ); - } + public *lazyResolve(node: JSONPathNode): Generator { + if (isArray(node.value)) { + const normIndex = this.normalizedIndex(node.value.length); + if (normIndex in node.value) { + yield new JSONPathNode( + node.value[normIndex], + node.location.concat(normIndex), + node.root, + ); } } } @@ -157,27 +146,24 @@ export class SliceSelector extends JSONPathSelector { this.checkRange(start, stop, step); } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (!isArray(node.value)) continue; - - for (const [i, value] of this.slice( - node.value, - this.start, - this.stop, - this.step, - )) { - rv.push(new JSONPathNode(value, node.location.concat(i), node.root)); - } + if (!isArray(node.value)) return rv; + + for (const [i, value] of this.slice( + node.value, + this.start, + this.stop, + this.step, + )) { + rv.push(new JSONPathNode(value, node.location.concat(i), node.root)); } + return rv; } - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (!isArray(node.value)) continue; - + public *lazyResolve(node: JSONPathNode): Generator { + if (isArray(node.value)) { for (const [i, value] of this.lazySlice( node.value, this.start, @@ -306,249 +292,45 @@ export class WildcardSelector extends JSONPathSelector { constructor( readonly environment: JSONPathEnvironment, readonly token: Token, - readonly shorthand: boolean = false, - ) { - super(environment, token); - } - - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { - const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (node.value instanceof String) continue; - if (isArray(node.value)) { - for (let i = 0; i < node.value.length; i++) { - rv.push( - new JSONPathNode(node.value[i], node.location.concat(i), node.root), - ); - } - } else if (isObject(node.value)) { - for (const [key, value] of this.environment.entries(node.value)) { - rv.push( - new JSONPathNode(value, node.location.concat(key), node.root), - ); - } - } - } - return rv; - } - - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (node.value instanceof String) continue; - if (isArray(node.value)) { - for (let i = 0; i < node.value.length; i++) { - yield new JSONPathNode( - node.value[i], - node.location.concat(i), - node.root, - ); - } - } else if (isObject(node.value)) { - for (const [key, value] of this.environment.entries(node.value)) { - yield new JSONPathNode(value, node.location.concat(key), node.root); - } - } - } - } - - public toString(): string { - return this.shorthand ? "[*]" : "*"; - } -} - -export class RecursiveDescentSegment extends JSONPathSelector { - constructor( - readonly environment: JSONPathEnvironment, - readonly token: Token, - readonly selector: JSONPathSelector, ) { super(environment, token); } - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { - const rv: JSONPathNode[] = []; - - if (this.environment.nondeterministic) { - for (const root of nodes) { - for (const node of this.nondeterministicVisitor(root)) { - rv.push(node); - } - } - } else { - for (const node of nodes) { - rv.push(node); - for (const _node of this.visitor(node)) { - rv.push(_node); - } - } - } - - return this.selector.resolve(rv); - } - - public *lazyResolve(nodes: Iterable): Generator { - yield* this.selector.lazyResolve(this._lazyResolve(nodes)); - } - - // eslint-disable-next-line sonarjs/cognitive-complexity - protected *_lazyResolve( - nodes: Iterable, - ): Generator { - for (const _node of nodes) { - const stack: Array<{ node: JSONPathNode; depth: number }> = [ - { node: _node, depth: 0 }, - ]; - - yield _node; - - while (stack.length) { - const { node: currentNode, depth } = stack.pop() as { - node: JSONPathNode; - depth: number; - }; - - if (depth >= this.environment.maxRecursionDepth) { - throw new JSONPathRecursionLimitError( - "recursion limit reached", - this.token, - ); - } - - if (currentNode.value instanceof String) continue; - - if (isArray(currentNode.value)) { - for (let i = 0; i < currentNode.value.length; i++) { - const __node = new JSONPathNode( - currentNode.value[i], - currentNode.location.concat(i), - currentNode.root, - ); - - yield __node; - - if (isObject(__node.value)) { - stack.push({ node: __node, depth: depth + 1 }); - } - } - } else if (isObject(currentNode.value)) { - for (const [key, value] of this.environment.entries( - currentNode.value, - )) { - const __node = new JSONPathNode( - value, - currentNode.location.concat(key), - currentNode.root, - ); - - yield __node; - - if (isObject(__node.value)) { - stack.push({ node: __node, depth: depth + 1 }); - } - } - } - } - } - } - - public toString(): string { - return `..${this.selector.toString()}`; - } - - private visitor(node: JSONPathNode, depth: number = 1): JSONPathNode[] { - if (depth >= this.environment.maxRecursionDepth) { - throw new JSONPathRecursionLimitError( - "recursion limit reached", - this.token, - ); - } + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; if (node.value instanceof String) return rv; if (isArray(node.value)) { for (let i = 0; i < node.value.length; i++) { - const _node = new JSONPathNode( - node.value[i], - node.location.concat(i), - node.root, + rv.push( + new JSONPathNode(node.value[i], node.location.concat(i), node.root), ); - rv.push(_node); - for (const __node of this.visitor(_node, depth + 1)) { - rv.push(__node); - } } } else if (isObject(node.value)) { for (const [key, value] of this.environment.entries(node.value)) { - const _node = new JSONPathNode( - value, - node.location.concat(key), - node.root, - ); - rv.push(_node); - for (const __node of this.visitor(_node, depth + 1)) { - rv.push(__node); - } - } - } - - return rv; - } - - protected nondeterministicVisitor( - root: JSONPathNode, - depth: number = 1, - ): JSONPathNode[] { - const rv: JSONPathNode[] = [root]; - let queue: Array<[JSONPathNode, number]> = this.nondeterministicChildren( - root, - ).map((node) => [node, depth]); - - while (queue.length) { - const [node, _depth] = queue.shift() as [JSONPathNode, number]; - rv.push(node); - - if (_depth >= this.environment.maxRecursionDepth) { - throw new JSONPathRecursionLimitError( - "recursion limit reached", - this.token, - ); - } - - // Visit child nodes now or queue them for later? - const visitChildren = Math.random() < 0.5; - - for (const child of this.nondeterministicChildren(node)) { - if (visitChildren) { - rv.push(child); - - const grandchildren: Array<[JSONPathNode, number]> = - this.nondeterministicChildren(child).map((n) => [n, _depth + 2]); - - queue = interleave(queue, grandchildren); - } else { - queue.push([child, _depth + 1]); - } + rv.push(new JSONPathNode(value, node.location.concat(key), node.root)); } } - return rv; } - protected nondeterministicChildren(node: JSONPathNode): JSONPathNode[] { - const _rv: JSONPathNode[] = []; - if (node.value instanceof String) return _rv; + public *lazyResolve(node: JSONPathNode): Generator { if (isArray(node.value)) { for (let i = 0; i < node.value.length; i++) { - _rv.push( - new JSONPathNode(node.value[i], node.location.concat(i), node.root), + yield new JSONPathNode( + node.value[i], + node.location.concat(i), + node.root, ); } - } else if (isObject(node.value)) { + } else if (isObject(node.value) && !isString(node.value)) { for (const [key, value] of this.environment.entries(node.value)) { - _rv.push(new JSONPathNode(value, node.location.concat(key), node.root)); + yield new JSONPathNode(value, node.location.concat(key), node.root); } } + } - return _rv; + public toString(): string { + return "*"; } } @@ -561,163 +343,72 @@ export class FilterSelector extends JSONPathSelector { super(environment, token); } - // eslint-disable-next-line sonarjs/cognitive-complexity - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { + public resolve(node: JSONPathNode): JSONPathNode[] { const rv: JSONPathNode[] = []; - for (const node of nodes) { - if (node.value instanceof String) continue; - if (isArray(node.value)) { - for (let i = 0; i < node.value.length; i++) { - const value = node.value[i]; - const filterContext: FilterContext = { - environment: this.environment, - currentValue: value, - rootValue: node.root, - currentKey: i, - }; - if (this.expression.evaluate(filterContext)) { - rv.push( - new JSONPathNode(value, node.location.concat(i), node.root), - ); - } + if (node.value instanceof String) return rv; + if (isArray(node.value)) { + for (let i = 0; i < node.value.length; i++) { + const value = node.value[i]; + const filterContext: FilterContext = { + environment: this.environment, + currentValue: value, + rootValue: node.root, + currentKey: i, + }; + if (this.expression.evaluate(filterContext)) { + rv.push(new JSONPathNode(value, node.location.concat(i), node.root)); } - } else if (isObject(node.value)) { - for (const [key, value] of this.environment.entries(node.value)) { - const filterContext: FilterContext = { - environment: this.environment, - currentValue: value, - rootValue: node.root, - currentKey: key, - }; - if (this.expression.evaluate(filterContext)) { - rv.push( - new JSONPathNode(value, node.location.concat(key), node.root), - ); - } + } + } else if (isObject(node.value)) { + for (const [key, value] of this.environment.entries(node.value)) { + const filterContext: FilterContext = { + environment: this.environment, + currentValue: value, + rootValue: node.root, + currentKey: key, + }; + if (this.expression.evaluate(filterContext)) { + rv.push( + new JSONPathNode(value, node.location.concat(key), node.root), + ); } } } return rv; } - // eslint-disable-next-line sonarjs/cognitive-complexity - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - if (node.value instanceof String) continue; - if (isArray(node.value)) { - for (let i = 0; i < node.value.length; i++) { - const value = node.value[i]; - const filterContext: FilterContext = { - environment: this.environment, - currentValue: value, - rootValue: node.root, - lazy: true, - currentKey: i, - }; - if (this.expression.evaluate(filterContext)) { - yield new JSONPathNode(value, node.location.concat(i), node.root); - } - } - } else if (isObject(node.value)) { - for (const [key, value] of this.environment.entries(node.value)) { - const filterContext: FilterContext = { - environment: this.environment, - currentValue: value, - rootValue: node.root, - lazy: true, - currentKey: key, - }; - if (this.expression.evaluate(filterContext)) { - yield new JSONPathNode(value, node.location.concat(key), node.root); - } + public *lazyResolve(node: JSONPathNode): Generator { + if (isArray(node.value)) { + for (let i = 0; i < node.value.length; i++) { + const value = node.value[i]; + const filterContext: FilterContext = { + environment: this.environment, + currentValue: value, + rootValue: node.root, + lazy: true, + currentKey: i, + }; + if (this.expression.evaluate(filterContext)) { + yield new JSONPathNode(value, node.location.concat(i), node.root); } } - } - } - - public toString(): string { - return `?${this.expression.toString()}`; - } -} - -export type BracketedSegment = - | FilterSelector - | IndexSelector - | NameSelector - | SliceSelector - | WildcardSelector; - -export class BracketedSelection extends JSONPathSelector { - constructor( - readonly environment: JSONPathEnvironment, - readonly token: Token, - readonly items: BracketedSegment[], - ) { - super(environment, token); - } - - public resolve(nodes: JSONPathNode[]): JSONPathNode[] { - const rv: JSONPathNode[] = []; - for (const node of nodes) { - for (const item of this.items) { - for (const _node of item.resolve([node])) { - rv.push(_node); + } else if (isObject(node.value) && !isString(node.value)) { + for (const [key, value] of this.environment.entries(node.value)) { + const filterContext: FilterContext = { + environment: this.environment, + currentValue: value, + rootValue: node.root, + lazy: true, + currentKey: key, + }; + if (this.expression.evaluate(filterContext)) { + yield new JSONPathNode(value, node.location.concat(key), node.root); } } } - - return rv; - } - - public *lazyResolve(nodes: Iterable): Generator { - for (const node of nodes) { - for (const item of this.items) { - yield* item.lazyResolve([node]); - } - } } public toString(): string { - return `[${this.items.map((itm) => itm.toString()).join(", ")}]`; - } -} - -/** - * Randomly interleave elements from two arrays while maintaining relative - * order of each input array. - * - * If _arrayA_ is empty, _arrayB_ is returned, and vice versa. - */ -function interleave(arrayA: T[], arrayB: U[]): Array { - if (arrayA.length === 0) { - return arrayB; - } - - if (arrayB.length === 0) { - return arrayA; - } - - // An array of iterators - const iterators: Array | Iterator> = []; - const itA = arrayA[Symbol.iterator](); - const itB = arrayB[Symbol.iterator](); - - for (let i = 0; i < arrayA.length; i++) { - iterators.push(itA); - } - - for (let i = 0; i < arrayB.length; i++) { - iterators.push(itB); - } - - shuffle(iterators); - return iterators.map((it) => it.next().value); -} - -function shuffle(entries: T[]): T[] { - for (let i = entries.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [entries[i], entries[j]] = [entries[j], entries[i]]; + return `?${this.expression.toString()}`; } - return entries; }