Skip to content
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

Add non-standard keys selector and current key identifier #12

Merged
merged 9 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ benchmark/*.txt
tests/dev.test.ts
dev.js
dev.mjs

# system
.DS_Store
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# JSON P3 Change Log

## Version 1.1.2 (unreleased)
## Version 1.2.0 (unreleased)

**Fixes**

- Fixed the error and error message arising from JSONPath queries with filter expressions and a missing closing bracket for the segment. Previously we would get a `JSONPathLexerError`, stating we "can't backup beyond start", which is meant to be an internal error. We now get a `JSONPathSyntaxError` with the message "unclosed bracketed selection".

**Features**

- Added a non-standard _keys_ selector (`~`), selecting property names from objects. The keys selector is only enabled when setting `JSONPathEnvironment`'s `strict` option to `false`.
- Added a non-standard _current key_ identifier (`#`). `#` will be the key or index corresponding to `@` in a filter expression. The current key identifier is only enabled when setting `JSONPathEnvironment`'s `strict` option to `false`.

## Version 1.1.1

**Fixes**
Expand Down
73 changes: 73 additions & 0 deletions docs/docs/guides/jsonpath-extra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Extra JSONPath Syntax

**_New in version 1.2.0_**

JSON P3 includes some extra, non-standard JSONPath syntax that is disabled by default. Setting the [`strict`](../api/namespaces/jsonpath.md#jsonpathenvironmentoptions) option to `false` when instantiating a [`JSONPathEnvironment`](../api/classes/jsonpath.JSONPathEnvironment.md) will enable all non-standard syntax.

```javascript
import { JSONPathEnvironment } from "json-p3";

const env = new JSONPathEnvironment({ strict: false });
values = env.query("$.some.path", data).values();
```

:::warning
Non-standard features are subject to change if:

- conflicting syntax is included in a future JSONPath standard or draft standard.
- an overwhelming consensus from the JSONPath community emerges that differs from our choices.
:::

## Keys selector

`~` is the _keys_ selector, selecting property names from objects. The keys selector can be used in a bracketed selection (`[~]`) or in its shorthand form (`.~`).

```text
$.users[[email protected] == 86].~
```

Output using example data from the [previous page](./jsonpath-syntax.md):

```json
["name", "score", "admin"]
```

When applied to an array or primitive value, the keys selector select nothing.

:::warning
Creating a [JSON Pointer](./json-pointer.md) from a [`JSONPathNode`](../api/classes/jsonpath.JSONPathNode.md#topointer) built using the keys selector will result in an unresolvable pointer. JSON Pointer does not support pointing to property names.
:::

### Custom keys token

The token representing the keys selector can be customized by setting the `keysPattern` option on a `JSONPathEnvironment` to a regular expression with the sticky flag set. For example, to change the keys selector to be `*~` instead of `~`:

```javascript
import { JSONPathEnvironment } from "json-p3";

const env = new JSONPathEnvironment({ strict: false, keysPattern: /\*~/y });
```

## Current key identifier

`#` is the _current key_ identifier. `#` will be the property name of an object or index of an array corresponding to `@` in a filter expression.

```text
$.users[?# > 1]
```

Again, using example data from the [previous page](./jsonpath-syntax.md):

```json
[
{
"name": "Sally",
"score": 84,
"admin": false
},
{
"name": "Jane",
"score": 55
}
]
```
2 changes: 1 addition & 1 deletion docs/docs/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Download and include JSON P3 in a script tag:
Or use a CDN

```html
<script src="https://cdn.jsdelivr.net/npm/json-p3@0.1.0/dist/json-p3.iife.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/json-p3@1.1.1/dist/json-p3.iife.min.js"></script>
<script>
const data = {
players: [{ name: "Sue" }, { name: "John" }, { name: "Sally" }],
Expand Down
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const sidebars = {
collapsed: false,
items: [
"guides/jsonpath-syntax",
"guides/jsonpath-extra",
"guides/jsonpath-functions",
"guides/json-pointer",
"guides/json-patch",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-p3",
"version": "1.1.1",
"version": "1.2.0",
"author": "James Prior",
"license": "MIT",
"description": "JSONPath, JSON Pointer and JSON Patch",
Expand Down
30 changes: 25 additions & 5 deletions src/path/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Parser } from "./parse";
import { JSONPath } from "./path";
import { Token, TokenStream } from "./token";
import { JSONValue } from "../types";
import { CurrentKey } from "./extra/expression";

/**
* JSONPath environment options. The defaults are in compliance with JSONPath
Expand All @@ -26,11 +27,13 @@ import { JSONValue } from "../types";
export type JSONPathEnvironmentOptions = {
/**
* Indicates if the environment should to be strict about its compliance with
* JSONPath standards.
* RFC 9535.
*
* Defaults to `true`. Setting `strict` to `false` currently has no effect.
* If/when we add non-standard features, the environment's strictness will
* control their availability.
* Defaults to `true`. Setting `strict` to `false` enables non-standard
* features. Non-standard features are subject to change if conflicting
* features are included in a future JSONPath standard or draft standard, or
* an overwhelming consensus amongst the JSONPath community emerges that
* differs from this implementation.
*/
strict?: boolean;

Expand All @@ -54,8 +57,17 @@ export type JSONPathEnvironmentOptions = {

/**
* If `true`, enable nondeterministic ordering when iterating JSON object data.
*
* This is mainly useful for validating the JSONPath Compliance Test Suite.
*/
nondeterministic?: boolean;

/**
* The pattern to use for the non-standard _keys selector_.
*
* The lexer expects the sticky bit to be set. Defaults to `/~/y`.
*/
keysPattern?: RegExp;
};

/**
Expand Down Expand Up @@ -98,6 +110,11 @@ export class JSONPathEnvironment {
*/
readonly nondeterministic: boolean;

/**
* The pattern to use for the non-standard _keys selector_.
*/
readonly keysPattern: RegExp;

/**
* 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 @@ -115,6 +132,8 @@ export class JSONPathEnvironment {
this.minIntIndex = options.maxIntIndex ?? -Math.pow(2, 53) - 1;
this.maxRecursionDepth = options.maxRecursionDepth ?? 50;
this.nondeterministic = options.nondeterministic ?? false;
this.keysPattern = options.keysPattern ?? /~/y;

this.parser = new Parser(this);
this.setupFilterFunctions();
}
Expand All @@ -126,7 +145,7 @@ export class JSONPathEnvironment {
public compile(path: string): JSONPath {
return new JSONPath(
this,
this.parser.parse(new TokenStream(tokenize(path))),
this.parser.parse(new TokenStream(tokenize(this, path))),
);
}

Expand Down Expand Up @@ -232,6 +251,7 @@ export class JSONPathEnvironment {
if (
!(
arg instanceof FilterExpressionLiteral ||
arg instanceof CurrentKey ||
(arg instanceof JSONPathQuery && arg.path.singularQuery()) ||
(arg instanceof FunctionExtension &&
this.functionRegister.get(arg.name)?.returnType ===
Expand Down
12 changes: 12 additions & 0 deletions src/path/extra/expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FilterExpression } from "../expression";
import { FilterContext, Nothing } from "../types";

export class CurrentKey extends FilterExpression {
public evaluate(context: FilterContext): string | number | typeof Nothing {
return context.currentKey ?? Nothing;
}

public toString(): string {
return "#";
}
}
Empty file added src/path/extra/index.ts
Empty file.
60 changes: 60 additions & 0 deletions src/path/extra/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isArray, isObject } from "../../types";
import { JSONPathEnvironment } from "../environment";
import { JSONPathNode } from "../node";
import { JSONPathSelector } from "../selectors";
import { Token } from "../token";

/**
* Object property name selector or array index selector.
*/
export class KeysSelector 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 || isArray(node.value)) continue;
if (isObject(node.value)) {
let i = 0;
for (const [key, _] of this.environment.entries(node.value)) {
rv.push(
new JSONPathNode(
key,
node.location.concat("[~]", `[${i}]`),
node.root,
),
);
i++;
}
}
}
return rv;
}

public *lazyResolve(nodes: Iterable<JSONPathNode>): Generator<JSONPathNode> {
for (const node of nodes) {
if (node.value instanceof String || isArray(node.value)) continue;
if (isObject(node.value)) {
let i = 0;
for (const [key, _] of this.environment.entries(node.value)) {
yield new JSONPathNode(
key,
node.location.concat("[~]", `[${i}]`),
node.root,
);
i++;
}
}
}
}

public toString(): string {
return this.shorthand ? "[~]" : "~";
}
}
Loading
Loading