Skip to content

Commit 2a11ebc

Browse files
authored
Merge pull request #28 from jg-rp/data-model
Refactor to model JSONPath segments explicitly
2 parents 752ec52 + 92ebddf commit 2a11ebc

File tree

11 files changed

+537
-628
lines changed

11 files changed

+537
-628
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# JSON P3 Change Log
22

3+
## Version 2.0.0 (unreleased)
4+
5+
**Breaking changes**
6+
7+
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.
8+
9+
- Renamed `JSONPath` to `JSONPathQuery` to match terminology from RFC 9535.
10+
- Refactored `JSONPathQuery` to be composed of `JSONPathSegment`s, each of which is composed of one or more instances of `JSONPathSelector`.
11+
- 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.
12+
313
## Version 1.3.5
414

515
**Fixes**

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-p3",
3-
"version": "1.3.5",
3+
"version": "2.0.0",
44
"author": "James Prior",
55
"license": "MIT",
66
"description": "JSONPath, JSON Pointer and JSON Patch",

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export * as jsonpath from "./path";
44
export {
55
DEFAULT_ENVIRONMENT,
66
FunctionExpressionType,
7-
JSONPath,
7+
JSONPathQuery,
88
JSONPathEnvironment,
99
JSONPathError,
1010
JSONPathIndexError,

src/path/environment.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
FilterExpressionLiteral,
55
FunctionExtension,
66
InfixExpression,
7-
JSONPathQuery,
7+
FilterQuery,
88
} from "./expression";
99
import { Count as CountFilterFunction } from "./functions/count";
1010
import { FilterFunction, FunctionExpressionType } from "./functions/function";
@@ -15,7 +15,7 @@ import { Value as ValueFilterFunction } from "./functions/value";
1515
import { tokenize } from "./lex";
1616
import { JSONPathNode, JSONPathNodeList } from "./node";
1717
import { Parser } from "./parse";
18-
import { JSONPath } from "./path";
18+
import { JSONPathQuery } from "./path";
1919
import { Token, TokenStream } from "./token";
2020
import { JSONValue } from "../types";
2121
import { CurrentKey } from "./extra/expression";
@@ -140,10 +140,10 @@ export class JSONPathEnvironment {
140140

141141
/**
142142
* @param path - A JSONPath query to parse.
143-
* @returns A new {@link JSONPath} object, bound to this environment.
143+
* @returns A new {@link JSONPathQuery} object, bound to this environment.
144144
*/
145-
public compile(path: string): JSONPath {
146-
return new JSONPath(
145+
public compile(path: string): JSONPathQuery {
146+
return new JSONPathQuery(
147147
this,
148148
this.parser.parse(new TokenStream(tokenize(this, path))),
149149
);
@@ -252,7 +252,7 @@ export class JSONPathEnvironment {
252252
!(
253253
arg instanceof FilterExpressionLiteral ||
254254
arg instanceof CurrentKey ||
255-
(arg instanceof JSONPathQuery && arg.path.singularQuery()) ||
255+
(arg instanceof FilterQuery && arg.path.singularQuery()) ||
256256
(arg instanceof FunctionExtension &&
257257
this.functionRegister.get(arg.name)?.returnType ===
258258
FunctionExpressionType.ValueType)
@@ -265,9 +265,7 @@ export class JSONPathEnvironment {
265265
}
266266
break;
267267
case FunctionExpressionType.LogicalType:
268-
if (
269-
!(arg instanceof JSONPathQuery || arg instanceof InfixExpression)
270-
) {
268+
if (!(arg instanceof FilterQuery || arg instanceof InfixExpression)) {
271269
throw new JSONPathTypeError(
272270
`${token.value}() argument ${idx} must be of LogicalType`,
273271
arg.token,
@@ -277,7 +275,7 @@ export class JSONPathEnvironment {
277275
case FunctionExpressionType.NodesType:
278276
if (
279277
!(
280-
arg instanceof JSONPathQuery ||
278+
arg instanceof FilterQuery ||
281279
(arg instanceof FunctionExtension &&
282280
this.functionRegister.get(arg.name)?.returnType ===
283281
FunctionExpressionType.NodesType)

src/path/expression.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { deepEquals } from "../deep_equals";
22
import { JSONPathTypeError, UndefinedFilterFunctionError } from "./errors";
33
import { FunctionExpressionType } from "./functions/function";
44
import { JSONPathNodeList } from "./node";
5-
import { JSONPath } from "./path";
5+
import { JSONPathQuery } from "./path";
66
import { Token } from "./token";
77
import { FilterContext, Nothing } from "./types";
88
import { isNumber, isString } from "../types";
@@ -190,16 +190,16 @@ export class LogicalExpression extends FilterExpression {
190190
/**
191191
* Base class for relative and absolute JSONPath query expressions.
192192
*/
193-
export abstract class JSONPathQuery extends FilterExpression {
193+
export abstract class FilterQuery extends FilterExpression {
194194
constructor(
195195
readonly token: Token,
196-
readonly path: JSONPath,
196+
readonly path: JSONPathQuery,
197197
) {
198198
super(token);
199199
}
200200
}
201201

202-
export class RelativeQuery extends JSONPathQuery {
202+
export class RelativeQuery extends FilterQuery {
203203
public evaluate(context: FilterContext): JSONPathNodeList {
204204
return context.lazy
205205
? new JSONPathNodeList(
@@ -213,7 +213,7 @@ export class RelativeQuery extends JSONPathQuery {
213213
}
214214
}
215215

216-
export class RootQuery extends JSONPathQuery {
216+
export class RootQuery extends FilterQuery {
217217
public evaluate(context: FilterContext): JSONPathNodeList {
218218
return context.lazy
219219
? new JSONPathNodeList(Array.from(this.path.lazyQuery(context.rootValue)))

src/path/extra/selectors.ts

Lines changed: 78 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isArray, isObject } from "../../types";
1+
import { isArray, isObject, isString } from "../../types";
22
import { JSONPathEnvironment } from "../environment";
33
import { LogicalExpression } from "../expression";
44
import { JSONPathNode } from "../node";
@@ -11,45 +11,41 @@ export class KeySelector extends JSONPathSelector {
1111
readonly environment: JSONPathEnvironment,
1212
readonly token: Token,
1313
readonly key: string,
14-
readonly shorthand: boolean = false,
1514
) {
1615
super(environment, token);
1716
}
1817

19-
public resolve(nodes: JSONPathNode[]): JSONPathNode[] {
18+
public resolve(node: JSONPathNode): JSONPathNode[] {
2019
const rv: JSONPathNode[] = [];
21-
for (const node of nodes) {
22-
if (node.value instanceof String || isArray(node.value)) continue;
23-
if (isObject(node.value) && hasStringKey(node.value, this.key)) {
24-
rv.push(
25-
new JSONPathNode(
26-
this.key,
27-
node.location.concat(`${KEY_MARK}${this.key}`),
28-
node.root,
29-
),
30-
);
31-
}
20+
if (node.value instanceof String || isArray(node.value)) return rv;
21+
if (isObject(node.value) && hasStringKey(node.value, this.key)) {
22+
rv.push(
23+
new JSONPathNode(
24+
this.key,
25+
node.location.concat(`${KEY_MARK}${this.key}`),
26+
node.root,
27+
),
28+
);
3229
}
3330
return rv;
3431
}
3532

36-
public *lazyResolve(nodes: Iterable<JSONPathNode>): Generator<JSONPathNode> {
37-
for (const node of nodes) {
38-
if (node.value instanceof String || isArray(node.value)) continue;
39-
if (isObject(node.value) && hasStringKey(node.value, this.key)) {
40-
yield new JSONPathNode(
41-
this.key,
42-
node.location.concat(`${KEY_MARK}${this.key}`),
43-
node.root,
44-
);
45-
}
33+
public *lazyResolve(node: JSONPathNode): Generator<JSONPathNode> {
34+
if (
35+
!isString(node.value) &&
36+
isObject(node.value) &&
37+
hasStringKey(node.value, this.key)
38+
) {
39+
yield new JSONPathNode(
40+
this.key,
41+
node.location.concat(`${KEY_MARK}${this.key}`),
42+
node.root,
43+
);
4644
}
4745
}
4846

4947
public toString(): string {
50-
return this.shorthand
51-
? `[~'${this.key.replaceAll("'", "\\'")}']`
52-
: `~'${this.key.replaceAll("'", "\\'")}'`;
48+
return `~'${this.key.replaceAll("'", "\\'")}'`;
5349
}
5450
}
5551

@@ -60,47 +56,41 @@ export class KeysSelector extends JSONPathSelector {
6056
constructor(
6157
readonly environment: JSONPathEnvironment,
6258
readonly token: Token,
63-
readonly shorthand: boolean = false,
6459
) {
6560
super(environment, token);
6661
}
6762

68-
public resolve(nodes: JSONPathNode[]): JSONPathNode[] {
63+
public resolve(node: JSONPathNode): JSONPathNode[] {
6964
const rv: JSONPathNode[] = [];
70-
for (const node of nodes) {
71-
if (node.value instanceof String || isArray(node.value)) continue;
72-
if (isObject(node.value)) {
73-
for (const [key, _] of this.environment.entries(node.value)) {
74-
rv.push(
75-
new JSONPathNode(
76-
key,
77-
node.location.concat(`${KEY_MARK}${key}`),
78-
node.root,
79-
),
80-
);
81-
}
65+
if (node.value instanceof String || isArray(node.value)) return rv;
66+
if (isObject(node.value)) {
67+
for (const [key, _] of this.environment.entries(node.value)) {
68+
rv.push(
69+
new JSONPathNode(
70+
key,
71+
node.location.concat(`${KEY_MARK}${key}`),
72+
node.root,
73+
),
74+
);
8275
}
8376
}
8477
return rv;
8578
}
8679

87-
public *lazyResolve(nodes: Iterable<JSONPathNode>): Generator<JSONPathNode> {
88-
for (const node of nodes) {
89-
if (node.value instanceof String || isArray(node.value)) continue;
90-
if (isObject(node.value)) {
91-
for (const [key, _] of this.environment.entries(node.value)) {
92-
yield new JSONPathNode(
93-
key,
94-
node.location.concat(`${KEY_MARK}${key}`),
95-
node.root,
96-
);
97-
}
80+
public *lazyResolve(node: JSONPathNode): Generator<JSONPathNode> {
81+
if (isObject(node.value) && !isString(node.value) && !isArray(node.value)) {
82+
for (const [key, _] of this.environment.entries(node.value)) {
83+
yield new JSONPathNode(
84+
key,
85+
node.location.concat(`${KEY_MARK}${key}`),
86+
node.root,
87+
);
9888
}
9989
}
10090
}
10191

10292
public toString(): string {
103-
return this.shorthand ? "[~]" : "~";
93+
return "~";
10494
}
10595
}
10696

@@ -113,52 +103,48 @@ export class KeysFilterSelector extends JSONPathSelector {
113103
super(environment, token);
114104
}
115105

116-
public resolve(nodes: JSONPathNode[]): JSONPathNode[] {
106+
public resolve(node: JSONPathNode): JSONPathNode[] {
117107
const rv: JSONPathNode[] = [];
118-
for (const node of nodes) {
119-
if (node.value instanceof String || isArray(node.value)) continue;
120-
if (isObject(node.value)) {
121-
for (const [key, value] of this.environment.entries(node.value)) {
122-
const filterContext: FilterContext = {
123-
environment: this.environment,
124-
currentValue: value,
125-
rootValue: node.root,
126-
currentKey: key,
127-
};
128-
if (this.expression.evaluate(filterContext)) {
129-
rv.push(
130-
new JSONPathNode(
131-
key,
132-
node.location.concat(`${KEY_MARK}${key}`),
133-
node.root,
134-
),
135-
);
136-
}
108+
if (node.value instanceof String || isArray(node.value)) return rv;
109+
if (isObject(node.value)) {
110+
for (const [key, value] of this.environment.entries(node.value)) {
111+
const filterContext: FilterContext = {
112+
environment: this.environment,
113+
currentValue: value,
114+
rootValue: node.root,
115+
currentKey: key,
116+
};
117+
if (this.expression.evaluate(filterContext)) {
118+
rv.push(
119+
new JSONPathNode(
120+
key,
121+
node.location.concat(`${KEY_MARK}${key}`),
122+
node.root,
123+
),
124+
);
137125
}
138126
}
139127
}
140128
return rv;
141129
}
142130

143-
public *lazyResolve(nodes: Iterable<JSONPathNode>): Generator<JSONPathNode> {
144-
for (const node of nodes) {
145-
if (node.value instanceof String || isArray(node.value)) continue;
146-
if (isObject(node.value)) {
147-
for (const [key, value] of this.environment.entries(node.value)) {
148-
const filterContext: FilterContext = {
149-
environment: this.environment,
150-
currentValue: value,
151-
rootValue: node.root,
152-
lazy: true,
153-
currentKey: key,
154-
};
155-
if (this.expression.evaluate(filterContext)) {
156-
yield new JSONPathNode(
157-
key,
158-
node.location.concat(`${KEY_MARK}${key}`),
159-
node.root,
160-
);
161-
}
131+
public *lazyResolve(node: JSONPathNode): Generator<JSONPathNode> {
132+
if (node.value instanceof String || isArray(node.value)) return;
133+
if (isObject(node.value)) {
134+
for (const [key, value] of this.environment.entries(node.value)) {
135+
const filterContext: FilterContext = {
136+
environment: this.environment,
137+
currentValue: value,
138+
rootValue: node.root,
139+
lazy: true,
140+
currentKey: key,
141+
};
142+
if (this.expression.evaluate(filterContext)) {
143+
yield new JSONPathNode(
144+
key,
145+
node.location.concat(`${KEY_MARK}${key}`),
146+
node.root,
147+
);
162148
}
163149
}
164150
}

src/path/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { JSONValue } from "../types";
22
import { JSONPathEnvironment } from "./environment";
33
import { JSONPathNode, JSONPathNodeList } from "./node";
4-
import { JSONPath } from "./path";
4+
import { JSONPathQuery } from "./path";
55

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

9-
export { JSONPath } from "./path";
9+
export { JSONPathQuery } from "./path";
1010
export { JSONPathNodeList, JSONPathNode } from "./node";
1111
export { Token, TokenKind } from "./token";
1212

@@ -85,7 +85,7 @@ export function lazyQuery(
8585
* If filter function arguments are invalid, or filter expression are
8686
* used in an invalid way.
8787
*/
88-
export function compile(path: string): JSONPath {
88+
export function compile(path: string): JSONPathQuery {
8989
return DEFAULT_ENVIRONMENT.compile(path);
9090
}
9191

0 commit comments

Comments
 (0)