Skip to content

Commit 7f44cbe

Browse files
authored
Merge pull request #34 from jg-rp/fix-decode-location
Fix and test canonical serialisation of `JSONPathNode`
2 parents 2463256 + d6d44e1 commit 7f44cbe

14 files changed

+147
-17
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "tests/path/cts"]
22
path = tests/path/cts
33
url = https://github.com/jsonpath-standard/jsonpath-compliance-test-suite.git
4+
[submodule "tests/path/nts"]
5+
path = tests/path/nts
6+
url = git@github.com:jg-rp/jsonpath-compliance-normalized-paths.git

CHANGELOG.md

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

3+
## Version 2.1.0 (unreleased)
4+
5+
**Changes**
6+
7+
- Fixed `JSONPathQuery` serialization. `JSONPathQuery.toString()` was not handling name selectors containing `'` or `\`, and was a bit vague about the format serialized paths would use. `JSONPathQuery.toString()` now accepts an options object with a single `form` option. `form` can be one of `"pretty"` (the default) or `"canonical"`. The canonical format uses bracket notation and single quotes, whereas the pretty format uses shorthand notation where possible and double quotes. See [issue #30](https://github.com/jg-rp/json-p3/issues/30) and [PR #32](https://github.com/jg-rp/json-p3/pull/32).
8+
- Added `JSONPathNode.getPath(options?)`, which returns a string representation of the node's location. As above, the `form` option can be one of `"pretty"` (the default) or `"canonical"`.
9+
- Deprecated `JSONPathNode.path` in favour of `JSONPathNode.getPath(options?)`.
10+
- Changed the string representation of _filter selectors_. Both canonical and pretty formats now only include parentheses where necessary.
11+
312
## Version 2.0.0
413

514
**Breaking changes**

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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": "2.0.0",
3+
"version": "2.1.0",
44
"author": "James Prior",
55
"license": "MIT",
66
"description": "JSONPath, JSON Pointer and JSON Patch",

src/path/expression.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { JSONPathQuery } from "./path";
66
import { Token } from "./token";
77
import { FilterContext, Nothing, SerializationOptions } from "./types";
88
import { isNumber, isString } from "../types";
9+
import { toCanonical } from "./serialize";
910

1011
/**
1112
* Base class for all filter expressions.
@@ -70,7 +71,7 @@ export class StringLiteral extends FilterExpressionLiteral {
7071
}
7172

7273
public toString(): string {
73-
return JSON.stringify(this.value);
74+
return toCanonical(this.value);
7475
}
7576
}
7677

@@ -117,6 +118,10 @@ export class PrefixExpression extends FilterExpression {
117118
}
118119
}
119120

121+
const PRECEDENCE_LOGICAL_OR = 4;
122+
const PRECEDENCE_LOGICAL_AND = 5;
123+
const PRECEDENCE_PREFIX = 7;
124+
120125
export class InfixExpression extends FilterExpression {
121126
readonly logical: boolean;
122127

@@ -159,6 +164,7 @@ export class InfixExpression extends FilterExpression {
159164
}
160165

161166
public toString(options?: SerializationOptions): string {
167+
// Note that `LogicalExpression.toString()` does not call this.
162168
if (this.logical) {
163169
return `(${this.left.toString(options)} ${
164170
this.operator
@@ -183,7 +189,45 @@ export class LogicalExpression extends FilterExpression {
183189
}
184190

185191
public toString(options?: SerializationOptions): string {
186-
return this.expression.toString(options);
192+
// Minimize parentheses in logical expressions.
193+
function _toString(
194+
expression: FilterExpression,
195+
parentPrecedence: number,
196+
): string {
197+
if (expression instanceof InfixExpression) {
198+
let precedence: number;
199+
let op: string;
200+
let left: string;
201+
let right: string;
202+
203+
if (expression.operator === "&&") {
204+
precedence = PRECEDENCE_LOGICAL_AND;
205+
op = "&&";
206+
left = _toString(expression.left, precedence);
207+
right = _toString(expression.right, precedence);
208+
} else if (expression.operator === "||") {
209+
precedence = PRECEDENCE_LOGICAL_OR;
210+
op = "||";
211+
left = _toString(expression.left, precedence);
212+
right = _toString(expression.right, precedence);
213+
} else {
214+
return expression.toString(options);
215+
}
216+
217+
const expr = `${left} ${op} ${right}`;
218+
return precedence < parentPrecedence ? `(${expr})` : expr;
219+
}
220+
221+
if (expression instanceof PrefixExpression) {
222+
const operand = _toString(expression.right, PRECEDENCE_PREFIX);
223+
const expr = `!${operand}`;
224+
return parentPrecedence > PRECEDENCE_PREFIX ? `(${expr})` : expr;
225+
}
226+
227+
return expression.toString(options);
228+
}
229+
230+
return _toString(this.expression, 0);
187231
}
188232
}
189233

src/path/node.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,21 @@ export class JSONPathNode {
6262
name: string,
6363
options: SerializationOptions,
6464
): string {
65-
const serialize = options.form === "canonical" ? toCanonical : toQuoted;
65+
const normalized = options.form === "canonical";
66+
const serialize = normalized ? toCanonical : toQuoted;
6667
const hasKeyMark = name.startsWith(KEY_MARK);
6768
if (hasKeyMark) name = name.slice(1);
6869
const shorthand = toShorthand(name);
6970

7071
if (hasKeyMark) {
71-
return shorthand == null ? `[~${serialize(name)}]` : `.~${shorthand}`;
72+
return normalized || shorthand == null
73+
? `[~${serialize(name)}]`
74+
: `.~${shorthand}`;
7275
}
7376

74-
return shorthand == null ? `[${serialize(name)}]` : `.${shorthand}`;
77+
return normalized || shorthand == null
78+
? `[${serialize(name)}]`
79+
: `.${shorthand}`;
7580
}
7681
}
7782

tests/path/canonical_path.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ const testCases: Case[] = [
102102
{
103103
description: "filter, logical and",
104104
path: "$[?@.foo && @.bar]",
105-
want: "$[?(@['foo'] && @['bar'])]",
105+
want: "$[?@['foo'] && @['bar']]",
106106
},
107107
{
108108
description: "filter, logical or",
109109
path: "$[?@.foo || @.bar]",
110-
want: "$[?(@['foo'] || @['bar'])]",
110+
want: "$[?@['foo'] || @['bar']]",
111111
},
112112
{
113113
description: "filter, logical not",

tests/path/cts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { readFileSync } from "fs";
2+
3+
import { JSONPathEnvironment } from "../../src/path/environment";
4+
import { JSONValue } from "../../src/types";
5+
6+
type Case = {
7+
name: string;
8+
query: string;
9+
document: JSONValue;
10+
paths: string[];
11+
};
12+
13+
const normalized_paths = JSON.parse(
14+
readFileSync(
15+
process.env.JSONP3_NTS_PATH || "tests/path/nts/normalized_paths.json",
16+
{
17+
encoding: "utf8",
18+
},
19+
),
20+
);
21+
22+
const env = new JSONPathEnvironment();
23+
24+
describe("compliance normalized paths", () => {
25+
test.each<Case>(normalized_paths.tests)(
26+
"$name",
27+
({ query, document, paths }: Case) => {
28+
const rv = env.query(query, document).paths({ form: "canonical" });
29+
expect(paths).toStrictEqual(rv);
30+
},
31+
);
32+
});

tests/path/nts

Submodule nts added at e3539fa

0 commit comments

Comments
 (0)