diff --git a/spec/jsonata-suite/baseline.lua b/spec/jsonata-suite/baseline.lua index e473789..df552a1 100644 --- a/spec/jsonata-suite/baseline.lua +++ b/spec/jsonata-suite/baseline.lua @@ -3,6 +3,7 @@ return { ["array-constructor/array-sequences/2"] = true, ["array-constructor/array-sequences/3"] = true, + ["array-constructor/array-sequences/4"] = true, ["array-constructor/case000"] = true, ["array-constructor/case001"] = true, ["array-constructor/case002"] = true, @@ -15,11 +16,13 @@ return { ["array-constructor/case009"] = true, ["array-constructor/case011"] = true, ["array-constructor/case012"] = true, + ["array-constructor/case015"] = true, ["array-constructor/case016"] = true, ["array-constructor/case017"] = true, ["array-constructor/case019"] = true, ["blocks/case000"] = true, ["blocks/case001"] = true, + ["blocks/case002"] = true, ["blocks/case003"] = true, ["blocks/case004"] = true, ["blocks/case005"] = true, @@ -36,6 +39,7 @@ return { ["boolean-expresssions/case009"] = true, ["boolean-expresssions/case010"] = true, ["boolean-expresssions/case011"] = true, + ["boolean-expresssions/case016"] = true, ["boolean-expresssions/case017"] = true, ["boolean-expresssions/case018"] = true, ["boolean-expresssions/case019"] = true, @@ -50,6 +54,7 @@ return { ["boolean-expresssions/case028"] = true, ["boolean-expresssions/case029"] = true, ["boolean-expresssions/case030"] = true, + ["closures/case000"] = true, ["closures/case001"] = true, ["coalescing-operator/case000"] = true, ["coalescing-operator/case001"] = true, @@ -124,6 +129,7 @@ return { ["context/case000"] = true, ["context/case001"] = true, ["context/case002"] = true, + ["context/case003"] = true, ["default-operator/case000"] = true, ["default-operator/case001"] = true, ["default-operator/case002"] = true, @@ -146,6 +152,9 @@ return { ["descendent-operator/case000"] = true, ["descendent-operator/case001"] = true, ["descendent-operator/case002"] = true, + ["descendent-operator/case003"] = true, + ["descendent-operator/case004"] = true, + ["descendent-operator/case005"] = true, ["descendent-operator/case006"] = true, ["descendent-operator/case007"] = true, ["descendent-operator/case008"] = true, @@ -189,6 +198,7 @@ return { ["fields/case007"] = true, ["flattening/array-inputs/0"] = true, ["flattening/array-inputs/1"] = true, + ["flattening/array-inputs/2"] = true, ["flattening/array-inputs/3"] = true, ["flattening/array-inputs/4"] = true, ["flattening/array-inputs/5"] = true, @@ -221,7 +231,15 @@ return { ["flattening/case034a"] = true, ["flattening/case035"] = true, ["flattening/case036"] = true, + ["flattening/case037"] = true, + ["flattening/case038"] = true, + ["flattening/case039"] = true, ["flattening/case040"] = true, + ["flattening/case041"] = true, + ["flattening/case042"] = true, + ["flattening/case043"] = true, + ["flattening/case044"] = true, + ["flattening/case045"] = true, ["flattening/sequence-of-arrays/0"] = true, ["flattening/sequence-of-arrays/1"] = true, ["function-abs/case000"] = true, @@ -251,6 +269,7 @@ return { ["function-applications/case015"] = true, ["function-applications/case016"] = true, ["function-applications/case017"] = true, + ["function-applications/case018"] = true, ["function-applications/case019"] = true, ["function-applications/case020"] = true, ["function-assert/case000"] = true, @@ -631,6 +650,7 @@ return { ["function-sort/case002"] = true, ["function-sort/case003"] = true, ["function-sort/case005"] = true, + ["function-sort/case006"] = true, ["function-sort/case007"] = true, ["function-sort/case008"] = true, ["function-sort/case009"] = true, @@ -759,7 +779,10 @@ return { ["hof-filter/case003"] = true, ["hof-map/case000"] = true, ["hof-map/case001"] = true, + ["hof-map/case0010"] = true, ["hof-map/case002"] = true, + ["hof-map/case003"] = true, + ["hof-map/case004"] = true, ["hof-map/case005"] = true, ["hof-map/case006"] = true, ["hof-map/case007"] = true, @@ -879,6 +902,8 @@ return { ["object-constructor/case005"] = true, ["object-constructor/case006"] = true, ["object-constructor/case007"] = true, + ["object-constructor/case008"] = true, + ["object-constructor/case009"] = true, ["object-constructor/case010"] = true, ["object-constructor/case011"] = true, ["object-constructor/case012"] = true, @@ -888,6 +913,8 @@ return { ["object-constructor/case016"] = true, ["object-constructor/case017"] = true, ["object-constructor/case018"] = true, + ["object-constructor/case019"] = true, + ["object-constructor/case020"] = true, ["object-constructor/case021"] = true, ["object-constructor/case022"] = true, ["object-constructor/case023"] = true, @@ -952,7 +979,12 @@ return { ["predicates/case001"] = true, ["predicates/case002"] = true, ["predicates/case003"] = true, + ["quoted-selectors/case000"] = true, + ["quoted-selectors/case001"] = true, + ["quoted-selectors/case002"] = true, + ["quoted-selectors/case003"] = true, ["quoted-selectors/case004"] = true, + ["quoted-selectors/case005"] = true, ["quoted-selectors/case006"] = true, ["quoted-selectors/case007"] = true, ["range-operator/case000"] = true, @@ -1037,6 +1069,8 @@ return { ["tail-recursion/case000"] = true, ["tail-recursion/case003"] = true, ["tail-recursion/case004"] = true, + ["token-conversion/case000"] = true, + ["token-conversion/case001"] = true, ["token-conversion/case002"] = true, ["transform/case000"] = true, ["transform/case001"] = true, @@ -1066,6 +1100,8 @@ return { ["transform/case025"] = true, ["transform/case026"] = true, ["transform/case027"] = true, + ["transform/case030"] = true, + ["transform/case031"] = true, ["transform/case032"] = true, ["transform/case033"] = true, ["transform/case034"] = true, @@ -1171,6 +1207,7 @@ return { ["wildcards/case004"] = true, ["wildcards/case005"] = true, ["wildcards/case006"] = true, + ["wildcards/case007"] = true, ["wildcards/case008"] = true, ["wildcards/case009"] = true, ["wildcards/case010/0"] = true, diff --git a/spec/keeparray_spec.lua b/spec/keeparray_spec.lua new file mode 100644 index 0000000..f1c9d20 --- /dev/null +++ b/spec/keeparray_spec.lua @@ -0,0 +1,66 @@ +local jsonata = require("jsonata") +local function run(src, input) + return jsonata.compile(src):evaluate(input) +end + +describe("M6b A-2: quoted-string path step selects a field", function() + it('foo."bar" selects the field bar', function() + assert.are.equal(7, run('foo."bar"', { foo = { bar = 7 } })) + end) + it("a top-level string is still the literal", function() + assert.are.equal("bar", run('"bar"', { foo = { bar = 7 } })) + end) + it('foo."bar" over a non-object foo is undefined (faithful)', function() + assert.is_nil(run('foo."bar"', { foo = 5 })) + end) + it("a standalone string with a predicate stays a literal", function() + assert.are.equal("Red", run('"Red"[$$ = "Bus"]', "Bus")) + assert.is_nil(run('"Red"[$$ = "Car"]', "Bus")) + end) + it("a first-position quoted string in a path is also a field selector", function() + assert.are.equal(1, run('"x"."y"', { x = { y = 1 } })) + assert.are.equal(1, run('"x".y', { x = { y = 1 } })) + assert.are.equal(99, run('"a"."b"."c"', { a = { b = { c = 99 } } })) + end) + + it("a standalone string / parenthesized literal is unaffected for non-path uses", function() + assert.are.equal("bar", run('"bar"', {})) + assert.are.equal("Red", run('"Red"[$$ = "Bus"]', "Bus")) + end) +end) + +describe("M6b A-4: trailing ; in a block", function() + it("tolerates a trailing ; before )", function() + assert.are.equal(2, run("(1; 2;)")) + end) + it("( a; ) yields a", function() + assert.are.equal(1, run("(1;)")) + end) + it("empty () is unaffected", function() + assert.is_nil(run("()")) + end) +end) + +describe("M6b []: keepArray (any step keeps the final array)", function() + it("a.b[] forces a single result into an array", function() + assert.are.same({ 1 }, run("a.b[]", { a = { b = 1 } })) + end) + it("a.b[] is a no-op on a multi-element result", function() + assert.are.same({ 1, 2 }, run("a.b[]", { a = { { b = 1 }, { b = 2 } } })) + end) + it("a mid-path [] keeps the final array (a[].b)", function() + assert.are.same({ 1 }, run("a[].b", { a = { b = 1 } })) + end) + it("a mid-path [] with a predicate keeps the final array (case037 shape)", function() + assert.are.same({ 1 }, run('x[k="a"][].v', { x = { { k = "a", v = 1 } } })) + end) + it("foo[] over a missing field is undefined, not []", function() + assert.is_nil(run("foo[]", {})) + end) + it("foo[][] is idempotent", function() + assert.are.same({ 5 }, run("foo[][]", { foo = 5 })) + end) + it("a normal path still unwraps a single result (regression)", function() + assert.are.equal(1, run("a.b", { a = { b = 1 } })) + end) +end) diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index 3eb81e6..128bee7 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -502,6 +502,19 @@ local function eval_path(node, input, env) return context end +-- True iff any step of this path (or of any nested path step) carries keepArray. +local function path_keeps_array(node) + for _, s in ipairs(node.steps) do + if s.keepArray then + return true + end + if s.steps and path_keeps_array(s) then + return true + end + end + return false +end + -- Singleton unwrapping applied at the boundary of path/array results. local function finalize_sequence(seq, keep_singleton) if #seq == 0 then @@ -750,6 +763,7 @@ local function _evaluate(node, input, env) end return V.obj_get(input, node.value) elseif t == "path" then + local keep = path_keeps_array(node) local tuple_mode = false for _, s in ipairs(node.steps) do if s.tuple then @@ -762,9 +776,9 @@ local function _evaluate(node, input, env) if node.tuple then return seq -- tuple stream for an enclosing tuple step; no unwrap end - return finalize_sequence(seq, false) + return finalize_sequence(seq, keep) end - return finalize_sequence(eval_path(node, input, env), false) + return finalize_sequence(eval_path(node, input, env), keep) elseif t == "array" then local arr = V.array({}) for _, e in ipairs(node.expressions) do diff --git a/src/jsonata/parser.lua b/src/jsonata/parser.lua index 5555bde..c6953e0 100644 --- a/src/jsonata/parser.lua +++ b/src/jsonata/parser.lua @@ -203,6 +203,9 @@ do expressions[#expressions + 1] = p.expression(0) while p.node.id == ";" do p.advance() + if p.node.id == ")" then + break + end expressions[#expressions + 1] = p.expression(0) end end @@ -257,6 +260,10 @@ do return { type = "array", expressions = expressions, position = t.position } end s.led = function(p, t, left) + if p.node.id == "]" then + p.advance() + return { type = "predicate", expr = left, keepArray = true, position = t.position } + end local filter = p.expression(0) if p.node.id ~= "]" then errors.raise("S0203", { position = p.node.position, token = "]" }) @@ -674,7 +681,6 @@ process_ast = function(ast, ctx) end if ast.type == "predicate" then local target = process_ast(ast.expr, ctx) - local filter = process_ast(ast.filter, ctx) local step, path if target.type == "path" then path = target @@ -683,6 +689,12 @@ process_ast = function(ast, ctx) step = target path = { type = "path", steps = { target }, position = ast.position } end + if ast.keepArray then + step.keepArray = true + push_ancestry(path, step) + return path + end + local filter = process_ast(ast.filter, ctx) if filter.seekingParent ~= nil then for _, slot in ipairs(filter.seekingParent) do if slot.level == 1 then @@ -737,6 +749,14 @@ process_ast = function(ast, ctx) if ast.type == "binary" and ast.value == "." then local steps = {} flatten_path(ast, steps, ctx) + -- A string step after the path head selects a field (jsonata: `"x".y` ≡ `x.y`). + -- i=1 also covers a first-position quoted string. (Edge: `("x").y` collapses to a + -- bare string in the parser and is wrongly treated as a field here — obscure, accepted.) + for i = 1, #steps do + if steps[i].type == "string" then + steps[i] = { type = "name", value = steps[i].value, position = steps[i].position } + end + end local path = { type = "path", steps = steps, position = ast.position } resolve_ancestry(path, ctx) return path