Skip to content

Commit

Permalink
Add the NTS as a submodule and minimize parentheses
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Jan 23, 2025
1 parent bec01d1 commit 25841b6
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 10 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "tests/path/cts"]
path = tests/path/cts
url = https://github.com/jsonpath-standard/jsonpath-compliance-test-suite.git
[submodule "tests/path/nts"]
path = tests/path/nts
url = git@github.com:jg-rp/jsonpath-compliance-normalized-paths.git
46 changes: 44 additions & 2 deletions src/path/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { JSONPathQuery } from "./path";
import { Token } from "./token";
import { FilterContext, Nothing, SerializationOptions } from "./types";
import { isNumber, isString } from "../types";
import { toCanonical } from "./serialize";

/**
* Base class for all filter expressions.
Expand Down Expand Up @@ -70,7 +71,7 @@ export class StringLiteral extends FilterExpressionLiteral {
}

public toString(): string {
return JSON.stringify(this.value);
return toCanonical(this.value);
}
}

Expand Down Expand Up @@ -117,6 +118,10 @@ export class PrefixExpression extends FilterExpression {
}
}

const PRECEDENCE_LOGICAL_OR = 4;
const PRECEDENCE_LOGICAL_AND = 5;
const PRECEDENCE_PREFIX = 7;

export class InfixExpression extends FilterExpression {
readonly logical: boolean;

Expand Down Expand Up @@ -159,6 +164,7 @@ export class InfixExpression extends FilterExpression {
}

public toString(options?: SerializationOptions): string {
// Note that `LogicalExpression.toString()` does not call this.
if (this.logical) {
return `(${this.left.toString(options)} ${
this.operator
Expand All @@ -183,7 +189,43 @@ export class LogicalExpression extends FilterExpression {
}

public toString(options?: SerializationOptions): string {
return this.expression.toString(options);
// Minimize parentheses in logical expressions.
function _toString(
expression: FilterExpression,
parentPrecedence: number,
): string {
let precedence: number;
let op: string;
let left: string;
let right: string;

if (expression instanceof InfixExpression) {
if (expression.operator === "&&") {
precedence = PRECEDENCE_LOGICAL_AND;
op = "&&";
left = _toString(expression.left, precedence);
right = _toString(expression.right, precedence);
} else if (expression.operator === "||") {
precedence = PRECEDENCE_LOGICAL_OR;
op = "||";
left = _toString(expression.left, precedence);
right = _toString(expression.right, precedence);
} else {
return expression.toString(options);
}
} else if (expression instanceof PrefixExpression) {
const operand = _toString(expression.right, PRECEDENCE_PREFIX);
const expr = `!${operand}`;
return parentPrecedence > PRECEDENCE_PREFIX ? `(${expr})` : expr;
} else {
return expression.toString(options);
}

const expr = `${left} ${op} ${right}`;
return precedence < parentPrecedence ? `(${expr})` : expr;
}

return _toString(this.expression, 0);
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/path/canonical_path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ const testCases: Case[] = [
{
description: "filter, logical and",
path: "$[?@.foo && @.bar]",
want: "$[?(@['foo'] && @['bar'])]",
want: "$[?@['foo'] && @['bar']]",
},
{
description: "filter, logical or",
path: "$[?@.foo || @.bar]",
want: "$[?(@['foo'] || @['bar'])]",
want: "$[?@['foo'] || @['bar']]",
},
{
description: "filter, logical not",
Expand Down
1 change: 1 addition & 0 deletions tests/path/nts
Submodule nts added at e3539f
31 changes: 31 additions & 0 deletions tests/path/nts_canonical_paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { readFileSync } from "fs";

import { JSONPathEnvironment } from "../../src/path/environment";

type Case = {
name: string;
query: string;
canonical: string;
};

const normalized_paths = JSON.parse(
readFileSync(
process.env.JSONP3_NTS_CANONICAL_PATH ||
"tests/path/nts/canonical_paths.json",
{
encoding: "utf8",
},
),
);

const env = new JSONPathEnvironment();

describe("serialize to canonical path", () => {
test.each<Case>(normalized_paths.tests)(
"$name",
({ query, canonical }: Case) => {
const q = env.compile(query).toString({ form: "canonical" });
expect(q).toStrictEqual(canonical);
},
);
});
13 changes: 9 additions & 4 deletions tests/path/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,27 @@ const TEST_CASES: TestCase[] = [
{
description: "not binds more tightly than and",
path: "$[?!@.a && !@.b]",
want: "$[?(!@.a && !@.b)]",
want: "$[?!@.a && !@.b]",
},
{
description: "not binds more tightly than or",
path: "$[?!@.a || !@.b]",
want: "$[?(!@.a || !@.b)]",
want: "$[?!@.a || !@.b]",
},
{
description: "control precedence with parens",
description: "control precedence with parens, not",
path: "$[?!(@.a && !@.b)]",
want: "$[?!(@.a && !@.b)]",
},
{
description: "control precedence with parens",
path: "$[?(@.a || @.b) && @.c]",
want: "$[?(@.a || @.b) && @.c]",
},
{
description: "non-singular query in logical expression",
path: "$[?@.* && @.b]",
want: "$[?(@[*] && @.b)]",
want: "$[?@[*] && @.b]",
},
];

Expand Down
4 changes: 2 additions & 2 deletions tests/path/pretty_path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ const testCases: Case[] = [
{
description: "filter, logical and",
path: "$[?@.foo && @.bar]",
want: "$[?(@.foo && @.bar)]",
want: "$[?@.foo && @.bar]",
},
{
description: "filter, logical or",
path: "$[?@.foo || @.bar]",
want: "$[?(@.foo || @.bar)]",
want: "$[?@.foo || @.bar]",
},
{
description: "filter, logical not",
Expand Down

0 comments on commit 25841b6

Please sign in to comment.