Skip to content

Commit

Permalink
Merge pull request #28 from jg-rp/data-model
Browse files Browse the repository at this point in the history
Refactor to model JSONPath segments explicitly
  • Loading branch information
jg-rp authored Dec 11, 2024
2 parents 752ec52 + 92ebddf commit 2a11ebc
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 628 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# JSON P3 Change Log

## Version 2.0.0 (unreleased)

**Breaking changes**

These changes should only affect you if you're customizing the JSONPath parser, defining custom JSONPath selectors or inspecting `JSONPath.selectors` (now `JSONPathQuery.segments`). Otherwise query parsing and evaluation remains unchanged. See [issue 11](https://github.com/jg-rp/json-p3/issues/11) for more information.

- Renamed `JSONPath` to `JSONPathQuery` to match terminology from RFC 9535.
- Refactored `JSONPathQuery` to be composed of `JSONPathSegment`s, each of which is composed of one or more instances of `JSONPathSelector`.
- Changed abstract method `JSONPathSelector.resolve` and `JSONPathSelector.lazyResolve` to accept a single node argument instead of an array or iterator of nodes. Both still return zero or more nodes.

## Version 1.3.5

**Fixes**
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.3.5",
"version": "2.0.0",
"author": "James Prior",
"license": "MIT",
"description": "JSONPath, JSON Pointer and JSON Patch",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export * as jsonpath from "./path";
export {
DEFAULT_ENVIRONMENT,
FunctionExpressionType,
JSONPath,
JSONPathQuery,
JSONPathEnvironment,
JSONPathError,
JSONPathIndexError,
Expand Down
18 changes: 8 additions & 10 deletions src/path/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
FilterExpressionLiteral,
FunctionExtension,
InfixExpression,
JSONPathQuery,
FilterQuery,
} from "./expression";
import { Count as CountFilterFunction } from "./functions/count";
import { FilterFunction, FunctionExpressionType } from "./functions/function";
Expand All @@ -15,7 +15,7 @@ import { Value as ValueFilterFunction } from "./functions/value";
import { tokenize } from "./lex";
import { JSONPathNode, JSONPathNodeList } from "./node";
import { Parser } from "./parse";
import { JSONPath } from "./path";
import { JSONPathQuery } from "./path";
import { Token, TokenStream } from "./token";
import { JSONValue } from "../types";
import { CurrentKey } from "./extra/expression";
Expand Down Expand Up @@ -140,10 +140,10 @@ export class JSONPathEnvironment {

/**
* @param path - A JSONPath query to parse.
* @returns A new {@link JSONPath} object, bound to this environment.
* @returns A new {@link JSONPathQuery} object, bound to this environment.
*/
public compile(path: string): JSONPath {
return new JSONPath(
public compile(path: string): JSONPathQuery {
return new JSONPathQuery(
this,
this.parser.parse(new TokenStream(tokenize(this, path))),
);
Expand Down Expand Up @@ -252,7 +252,7 @@ export class JSONPathEnvironment {
!(
arg instanceof FilterExpressionLiteral ||
arg instanceof CurrentKey ||
(arg instanceof JSONPathQuery && arg.path.singularQuery()) ||
(arg instanceof FilterQuery && arg.path.singularQuery()) ||
(arg instanceof FunctionExtension &&
this.functionRegister.get(arg.name)?.returnType ===
FunctionExpressionType.ValueType)
Expand All @@ -265,9 +265,7 @@ export class JSONPathEnvironment {
}
break;
case FunctionExpressionType.LogicalType:
if (
!(arg instanceof JSONPathQuery || arg instanceof InfixExpression)
) {
if (!(arg instanceof FilterQuery || arg instanceof InfixExpression)) {
throw new JSONPathTypeError(
`${token.value}() argument ${idx} must be of LogicalType`,
arg.token,
Expand All @@ -277,7 +275,7 @@ export class JSONPathEnvironment {
case FunctionExpressionType.NodesType:
if (
!(
arg instanceof JSONPathQuery ||
arg instanceof FilterQuery ||
(arg instanceof FunctionExtension &&
this.functionRegister.get(arg.name)?.returnType ===
FunctionExpressionType.NodesType)
Expand Down
10 changes: 5 additions & 5 deletions src/path/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { deepEquals } from "../deep_equals";
import { JSONPathTypeError, UndefinedFilterFunctionError } from "./errors";
import { FunctionExpressionType } from "./functions/function";
import { JSONPathNodeList } from "./node";
import { JSONPath } from "./path";
import { JSONPathQuery } from "./path";
import { Token } from "./token";
import { FilterContext, Nothing } from "./types";
import { isNumber, isString } from "../types";
Expand Down Expand Up @@ -190,16 +190,16 @@ export class LogicalExpression extends FilterExpression {
/**
* Base class for relative and absolute JSONPath query expressions.
*/
export abstract class JSONPathQuery extends FilterExpression {
export abstract class FilterQuery extends FilterExpression {
constructor(
readonly token: Token,
readonly path: JSONPath,
readonly path: JSONPathQuery,
) {
super(token);
}
}

export class RelativeQuery extends JSONPathQuery {
export class RelativeQuery extends FilterQuery {
public evaluate(context: FilterContext): JSONPathNodeList {
return context.lazy
? new JSONPathNodeList(
Expand All @@ -213,7 +213,7 @@ export class RelativeQuery extends JSONPathQuery {
}
}

export class RootQuery extends JSONPathQuery {
export class RootQuery extends FilterQuery {
public evaluate(context: FilterContext): JSONPathNodeList {
return context.lazy
? new JSONPathNodeList(Array.from(this.path.lazyQuery(context.rootValue)))
Expand Down
170 changes: 78 additions & 92 deletions src/path/extra/selectors.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<JSONPathNode>): Generator<JSONPathNode> {
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<JSONPathNode> {
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("'", "\\'")}'`;
}
}

Expand All @@ -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<JSONPathNode>): Generator<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)) {
yield new JSONPathNode(
key,
node.location.concat(`${KEY_MARK}${key}`),
node.root,
);
}
public *lazyResolve(node: JSONPathNode): Generator<JSONPathNode> {
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 "~";
}
}

Expand All @@ -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<JSONPathNode>): Generator<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,
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<JSONPathNode> {
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,
);
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/path/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { JSONValue } from "../types";
import { JSONPathEnvironment } from "./environment";
import { JSONPathNode, JSONPathNodeList } from "./node";
import { JSONPath } from "./path";
import { JSONPathQuery } from "./path";

export { JSONPathEnvironment } from "./environment";
export type { JSONPathEnvironmentOptions } from "./environment";

export { JSONPath } from "./path";
export { JSONPathQuery } from "./path";
export { JSONPathNodeList, JSONPathNode } from "./node";
export { Token, TokenKind } from "./token";

Expand Down Expand Up @@ -85,7 +85,7 @@ export function lazyQuery(
* If filter function arguments are invalid, or filter expression are
* used in an invalid way.
*/
export function compile(path: string): JSONPath {
export function compile(path: string): JSONPathQuery {
return DEFAULT_ENVIRONMENT.compile(path);
}

Expand Down
Loading

0 comments on commit 2a11ebc

Please sign in to comment.