Skip to content

Commit

Permalink
Merge pull request #6 from jg-rp/nondeterministic
Browse files Browse the repository at this point in the history
Control CTS location and nondeterminism when testing for compliance
  • Loading branch information
jg-rp authored Mar 5, 2024
2 parents ff931b3 + 05b3469 commit b5d8601
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 56 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# JSON P3 Change Log

# Version 1.1.0 (unreleased)

**Features**

- Added `nondeterministic` to `JSONPathEnvironmentOptions` and environment variables to control nondeterminism and the location of `cts.json` when testing for compliance. See the [README](https://github.com/jg-rp/json-p3/blob/main/README.md) for a description of these environment variables.

# Version 1.0.0

[RFC 9535](https://datatracker.ietf.org/doc/html/rfc9535) has been published, replacing the [draft IETF JSONPath base](https://datatracker.ietf.org/doc/html/draft-ietf-jsonpath-base-21).
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ JSON P3 has zero runtime dependencies.
| `json-p3-iife.js` | A bundle formatted as an Immediately Invoked Function Expression. |
| `json-p3-iife.min.js` | A minified bundle formatted as an Immediately Invoked Function Expression. |

## Compliance Environment Variables

These environment variables control the location of the compliance test suite under test and if nondeterministic object iteration is enabled for those tests.

| Environment Variable | Description |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `JSONP3_CTS_PATH` | The path to `cts.json` used by `compliance.test.ts`. Defaults to `tests/path/cts/cts.json`. |
| `JSONP3_CTS_NONDETERMINISTIC` | When set to `true`, enables nondeterministic iteration of JSON objects for `compliance.test.ts`. Defaults to `false`. |

## Contributing

Please see [Contributing to JSON P3](https://github.com/jg-rp/json-p3/blob/main/CONTRIBUTING.md)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion performance/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ function perf(repeat) {
return (stop - start) / 1e3;
}

console.log(perf(1000));
console.log(perf(10000));
38 changes: 38 additions & 0 deletions src/path/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export type JSONPathEnvironmentOptions = {
* can visit before a `JSONPathRecursionLimitError` is thrown.
*/
maxRecursionDepth?: number;

/**
* If `true`, enable nondeterministic ordering when iterating JSON object data.
*/
nondeterministic?: boolean;
};

/**
Expand Down Expand Up @@ -88,6 +93,11 @@ export class JSONPathEnvironment {
*/
readonly maxRecursionDepth: number;

/**
* If `true`, enable nondeterministic ordering when iterating JSON object data.
*/
readonly nondeterministic: boolean;

/**
* A map of function names to objects implementing the {@link FilterFunction}
* interface. You are free to set or delete custom filter functions directly.
Expand All @@ -104,6 +114,7 @@ export class JSONPathEnvironment {
this.maxIntIndex = options.maxIntIndex ?? Math.pow(2, 53) - 1;
this.minIntIndex = options.maxIntIndex ?? -Math.pow(2, 53) - 1;
this.maxRecursionDepth = options.maxRecursionDepth ?? 50;
this.nondeterministic = options.nondeterministic ?? false;
this.parser = new Parser(this);
this.setupFilterFunctions();
}
Expand Down Expand Up @@ -262,4 +273,31 @@ export class JSONPathEnvironment {

return args;
}

/**
* Return an array of key/values of the enumerable properties in _obj_.
*
* If you want to introduce some nondeterminism to iterating JSON-like
* objects, do it here. The wildcard selector, descendent segment and
* filter selector all use `this.environment.entries`.
*
* @param obj - A JSON-like object.
*/
public entries(obj: {
[key: string]: JSONValue;
}): Array<[string, JSONValue]> {
function shuffle(entries: Array<[string, JSONValue]>) {
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;
}

if (this.nondeterministic) {
return shuffle(Object.entries(obj));
}

return Object.entries(obj);
}
}
14 changes: 8 additions & 6 deletions src/path/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export class WildcardSelector extends JSONPathSelector {
);
}
} else if (isObject(node.value)) {
for (const [key, value] of Object.entries(node.value)) {
for (const [key, value] of this.environment.entries(node.value)) {
rv.push(
new JSONPathNode(value, node.location.concat(key), node.root),
);
Expand All @@ -300,7 +300,7 @@ export class WildcardSelector extends JSONPathSelector {
);
}
} else if (isObject(node.value)) {
for (const [key, value] of Object.entries(node.value)) {
for (const [key, value] of this.environment.entries(node.value)) {
yield new JSONPathNode(value, node.location.concat(key), node.root);
}
}
Expand Down Expand Up @@ -377,7 +377,9 @@ export class RecursiveDescentSegment extends JSONPathSelector {
}
}
} else if (isObject(currentNode.value)) {
for (const [key, value] of Object.entries(currentNode.value)) {
for (const [key, value] of this.environment.entries(
currentNode.value,
)) {
const __node = new JSONPathNode(
value,
currentNode.location.concat(key),
Expand Down Expand Up @@ -421,7 +423,7 @@ export class RecursiveDescentSegment extends JSONPathSelector {
}
}
} else if (isObject(node.value)) {
for (const [key, value] of Object.entries(node.value)) {
for (const [key, value] of this.environment.entries(node.value)) {
const _node = new JSONPathNode(
value,
node.location.concat(key),
Expand Down Expand Up @@ -467,7 +469,7 @@ export class FilterSelector extends JSONPathSelector {
}
}
} else if (isObject(node.value)) {
for (const [key, value] of Object.entries(node.value)) {
for (const [key, value] of this.environment.entries(node.value)) {
const filterContext: FilterContext = {
environment: this.environment,
currentValue: value,
Expand Down Expand Up @@ -502,7 +504,7 @@ export class FilterSelector extends JSONPathSelector {
}
}
} else if (isObject(node.value)) {
for (const [key, value] of Object.entries(node.value)) {
for (const [key, value] of this.environment.entries(node.value)) {
const filterContext: FilterContext = {
environment: this.environment,
currentValue: value,
Expand Down
47 changes: 47 additions & 0 deletions tests/path/compliance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { readFileSync } from "fs";

import { JSONPathEnvironment } from "../../src/path/environment";
import { JSONPathError } from "../../src/path/errors";
import { JSONValue } from "../../src/types";

type Case = {
name: string;
selector: string;
document?: JSONValue;
result?: JSONValue[];
results?: JSONValue[][];
invalid_selector?: boolean;
};

const cts = JSON.parse(
readFileSync(process.env.JSONP3_CTS_PATH || "tests/path/cts/cts.json", {
encoding: "utf8",
}),
);

const env = new JSONPathEnvironment({
nondeterministic: process.env.JSONP3_CTS_NONDETERMINISTIC === "true",
});

const testSuiteName = env.nondeterministic
? "compliance test suite (nondeterministic)"
: "compliance test suite";

describe(testSuiteName, () => {
test.each<Case>(cts.tests)(
"$name",
({ selector, document, result, results, invalid_selector }: Case) => {
if (invalid_selector) {
expect(() => env.compile(selector)).toThrow(JSONPathError);
} else if (document) {
if (result) {
const rv = env.query(selector, document).values();
expect(rv).toStrictEqual(result);
} else if (results) {
const rv = env.query(selector, document).values();
expect(results).toContainEqual(rv);
}
}
},
);
});
37 changes: 37 additions & 0 deletions tests/path/lazy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { JSONPathNodeList } from "../../src/path";
import { JSONPathEnvironment } from "../../src/path/environment";
import { JSONPathError } from "../../src/path/errors";
import { JSONValue } from "../../src/types";

import cts from "./cts/cts.json";

type Case = {
name: string;
selector: string;
document?: JSONValue;
result?: JSONValue[];
results?: JSONValue[][];
invalid_selector?: boolean;
};

describe("lazy resolution", () => {
test.each<Case>(cts.tests)(
"$name",
({ selector, document, result, results, invalid_selector }: Case) => {
const env = new JSONPathEnvironment();
if (invalid_selector) {
expect(() => env.compile(selector)).toThrow(JSONPathError);
} else if (document) {
if (result) {
const it = env.lazyQuery(selector, document);
const rv = new JSONPathNodeList(Array.from(it)).values();
expect(rv).toStrictEqual(result);
} else if (results) {
const it = env.lazyQuery(selector, document);
const rv = new JSONPathNodeList(Array.from(it)).values();
expect(results).toContainEqual(rv);
}
}
},
);
});
45 changes: 0 additions & 45 deletions tests/path/query.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"strict": true,
"target": "es2022",
"noImplicitAny": true,
"types": ["jest"],
"types": ["jest", "node"],
"incremental": true,
"importHelpers": true,
"isolatedModules": true,
Expand Down

0 comments on commit b5d8601

Please sign in to comment.