From 07608c20a5573ff8619b545de52cfa394c9da10b Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Tue, 2 Dec 2025 19:16:53 +0100 Subject: [PATCH 1/2] feat: support using properties without needing the pipe character --- src/parse.ts | 8 +++++++- test-suite/parse.test.json | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index e179ac5..2acedac 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -120,7 +120,13 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery return ['get', ...props] } - return parseFunction() + return parseChainedProperty() + } + + const parseChainedProperty = () => { + // a function can be followed by a property, which is a shorthand for "fn() | .prop" + const result = parseFunction() + return query[i] === '.' ? ['pipe', result, parseProperty()] : result } const parseFunction = () => { diff --git a/test-suite/parse.test.json b/test-suite/parse.test.json index a8d4fc8..c34ff6b 100644 --- a/test-suite/parse.test.json +++ b/test-suite/parse.test.json @@ -46,6 +46,20 @@ { "input": ".array.2", "output": ["get", "array", 2] } ] }, + { + "category": "property", + "description": "should parse an implicitly piped a property", + "tests": [ + { + "input": "groupBy(.city).myCity", + "output": ["pipe", ["groupBy", ["get", "city"]], ["get", "myCity"]] + }, + { + "input": "\"hello\".length", + "output": ["pipe", "hello", ["get", "length"]] + } + ] + }, { "category": "property", "description": "should throw an error when a property misses an end quote", @@ -453,11 +467,10 @@ "description": "should throw an error in case of an invalid number", "tests": [ { "input": "-", "throws": "Value expected (pos: 0)" }, - { "input": "2.", "throws": "Unexpected part '.' (pos: 1)" }, + { "input": "2.", "throws": "Property expected (pos: 2)" }, { "input": "2.3e", "throws": "Unexpected part 'e' (pos: 3)" }, { "input": "2.3e+", "throws": "Unexpected part 'e+' (pos: 3)" }, - { "input": "2.3e-", "throws": "Unexpected part 'e-' (pos: 3)" }, - { "input": "2.", "throws": "Unexpected part '.' (pos: 1)" } + { "input": "2.3e-", "throws": "Unexpected part 'e-' (pos: 3)" } ] }, { From 3ab1ba4cc974f86355dbd5b61f6437d48be44a1a Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Sat, 6 Dec 2025 17:01:46 +0100 Subject: [PATCH 2/2] feat: optimize/improve parsing of implicit piped properties --- src/parse.ts | 17 ++++++++++------- test-suite/parse.test.json | 28 +++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 2acedac..2f61f5c 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -47,6 +47,13 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery while (true) { skipWhitespace() + if (query[i] === '.' && 'pipe' in currentOperators) { + // an implicitly piped property like "fn().prop" + const right = parseProperty() + left = left[0] === 'pipe' ? [...left, right] : ['pipe', left, right] + continue + } + const start = i const name = parseOperatorName(currentOperators) if (!name) { @@ -115,18 +122,14 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery parseInteger() ?? throwSyntaxError('Property expected') ) + + skipWhitespace() } return ['get', ...props] } - return parseChainedProperty() - } - - const parseChainedProperty = () => { - // a function can be followed by a property, which is a shorthand for "fn() | .prop" - const result = parseFunction() - return query[i] === '.' ? ['pipe', result, parseProperty()] : result + return parseFunction() } const parseFunction = () => { diff --git a/test-suite/parse.test.json b/test-suite/parse.test.json index c34ff6b..b61bca2 100644 --- a/test-suite/parse.test.json +++ b/test-suite/parse.test.json @@ -54,12 +54,34 @@ "input": "groupBy(.city).myCity", "output": ["pipe", ["groupBy", ["get", "city"]], ["get", "myCity"]] }, + { + "input": "groupBy(.city).myCity | size()", + "output": ["pipe", ["groupBy", ["get", "city"]], ["get", "myCity"], ["size"]] + }, + { + "input": "groupBy(.city).myCity.location | size()", + "output": ["pipe", ["groupBy", ["get", "city"]], ["get", "myCity", "location"], ["size"]] + }, + { + "input": ".data | groupBy(.city).myCity", + "output": ["pipe", ["get", "data"], ["groupBy", ["get", "city"]], ["get", "myCity"]] + }, { "input": "\"hello\".length", "output": ["pipe", "hello", ["get", "length"]] } ] }, + { + "category": "property", + "description": "should allow whitespace between multiple properties", + "tests": [ + { "input": ".\"address\" .\"city\"", "output": ["get", "address", "city"] }, + { "input": ".address .city", "output": ["get", "address", "city"] }, + { "input": ".address\t.city", "output": ["get", "address", "city"] }, + { "input": ".address\n.city", "output": ["get", "address", "city"] } + ] + }, { "category": "property", "description": "should throw an error when a property misses an end quote", @@ -68,11 +90,7 @@ { "category": "property", "description": "should throw an error when there is whitespace between the dot and the property name", - "tests": [ - { "input": ". \"name\"", "throws": "Property expected (pos: 1)" }, - { "input": ".\"address\" .\"city\"", "throws": "Unexpected part '.\"city\"' (pos: 11)" }, - { "input": ".address .city", "throws": "Unexpected part '.city' (pos: 9)" } - ] + "tests": [{ "input": ". \"name\"", "throws": "Property expected (pos: 1)" }] }, { "category": "function",