Skip to content

Commit

Permalink
Add a nondeterministic recursive descent visitor (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp authored Mar 21, 2024
1 parent f4a2224 commit 6c45e8b
Show file tree
Hide file tree
Showing 3 changed files with 553 additions and 8 deletions.
123 changes: 116 additions & 7 deletions src/path/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,23 @@ export class RecursiveDescentSegment extends JSONPathSelector {

public resolve(nodes: JSONPathNode[]): JSONPathNode[] {
const rv: JSONPathNode[] = [];
for (const node of nodes) {
rv.push(node);
for (const _node of this.visit(node)) {
rv.push(_node);

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);
}
}
}

// console.log(JSON.stringify(rv.map((n: any) => n.value)));
return this.selector.resolve(rv);
}

Expand Down Expand Up @@ -401,7 +412,7 @@ export class RecursiveDescentSegment extends JSONPathSelector {
return `..${this.selector.toString()}`;
}

private visit(node: JSONPathNode, depth: number = 1): JSONPathNode[] {
private visitor(node: JSONPathNode, depth: number = 1): JSONPathNode[] {
if (depth >= this.environment.maxRecursionDepth) {
throw new JSONPathRecursionLimitError(
"recursion limit reached",
Expand All @@ -418,7 +429,7 @@ export class RecursiveDescentSegment extends JSONPathSelector {
node.root,
);
rv.push(_node);
for (const __node of this.visit(_node, depth + 1)) {
for (const __node of this.visitor(_node, depth + 1)) {
rv.push(__node);
}
}
Expand All @@ -430,14 +441,72 @@ export class RecursiveDescentSegment extends JSONPathSelector {
node.root,
);
rv.push(_node);
for (const __node of this.visit(_node, depth + 1)) {
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]);
}
}
}

return rv;
}

protected nondeterministicChildren(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++) {
_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;
}
}

export class FilterSelector extends JSONPathSelector {
Expand Down Expand Up @@ -565,3 +634,43 @@ export class BracketedSelection extends JSONPathSelector {
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<T, U>(arrayA: T[], arrayB: U[]): Array<T | U> {
if (arrayA.length === 0) {
return arrayB;
}

if (arrayB.length === 0) {
return arrayA;
}

// An array of iterators
const iterators: Array<Iterator<T> | Iterator<U>> = [];
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<T>(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;
}
2 changes: 1 addition & 1 deletion tests/path/cts
Submodule cts updated 3 files
+199 −0 cts.json
+22 −0 tests/basic.json
+17 −0 tests/filter.json
Loading

0 comments on commit 6c45e8b

Please sign in to comment.