diff --git a/src/jsonata.js b/src/jsonata.js index 5807736..e1e6d27 100644 --- a/src/jsonata.js +++ b/src/jsonata.js @@ -558,6 +558,10 @@ var jsonata = (function() { result = await evaluateGroupExpression(expr, input, environment); break; + case '...': + result = await evaluate(expr.expression, input, environment); + break; + } return result; } @@ -915,8 +919,29 @@ var jsonata = (function() { for(var pairIndex = 0; pairIndex < expr.lhs.length; pairIndex++) { var pair = expr.lhs[pairIndex]; var key = await evaluate(pair[0], reduce ? item['@'] : item, env); + if(key === undefined) { + continue; + } + // handle spread operator + if (pair[0].value === '...') { + if(typeof key !== 'object' || key === null || Array.isArray(key)) { + throw { + code: "T2015", + stack: (new Error()).stack, + position: expr.position, + value: key + } + } + for (const [_key, _value] of Object.entries(key)) { + groups[_key] = { + data: _value, + exprIndex: pairIndex, + canOverride: true + }; + } + } // key has to be a string - if (typeof key !== 'string' && key !== undefined) { + else if (typeof key !== 'string') { throw { code: "T1003", stack: (new Error()).stack, @@ -925,9 +950,9 @@ var jsonata = (function() { }; } - if (key !== undefined) { + else { var entry = {data: item, exprIndex: pairIndex}; - if (groups.hasOwnProperty(key)) { + if (groups.hasOwnProperty(key) && !groups[key].canOverride) { // a value already exists in this slot if(groups[key].exprIndex !== pairIndex) { // this key has been generated by another expression in this group @@ -2005,6 +2030,7 @@ var jsonata = (function() { "T2012": "The delete clause of the transform expression must evaluate to a string or array of strings: {{value}}", "T2013": "The transform expression clones the input object using the $clone() function. This has been overridden in the current scope by a non-function.", "D2014": "The size of the sequence allocated by the range operator (..) must not exceed 1e6. Attempted to allocate {{value}}.", + "T2015": "The right side of the spread operator must evaluate to an object: {{value}}", "D3001": "Attempting to invoke string function on Infinity or NaN", "D3010": "Second argument of replace function cannot be an empty string", "D3011": "Fourth argument of replace function must evaluate to a positive number", diff --git a/src/parser.js b/src/parser.js index 9cffaf3..9901d1f 100644 --- a/src/parser.js +++ b/src/parser.js @@ -35,6 +35,7 @@ const parser = (() => { '^': 40, '**': 60, '..': 20, + '...': 50, ':=': 10, '!=': 40, '<=': 40, @@ -164,6 +165,11 @@ const parser = (() => { } // handle double-char operators if (currentChar === '.' && path.charAt(position + 1) === '.') { + // triple-dot ... spread operator + if (path.charAt(position + 2) === '.') { + position += 3; + return create('operator', '...'); + } // double-dot .. range operator position += 2; return create('operator', '..'); @@ -565,6 +571,7 @@ const parser = (() => { terminal("in"); // prefix("-"); // unary numeric negation infix("~>"); // function application + prefix("..."); // spread operator infixr("(error)", 10, function (left) { this.lhs = left; @@ -759,9 +766,13 @@ const parser = (() => { if (node.id !== "}") { for (; ;) { var n = expression(0); - advance(":"); - var v = expression(0); - a.push([n, v]); // holds an array of name/value expression pairs + if (n.id === '...') { // Spread operator + a.push([n, {type: 'variable', value: ''}]); // the value is an identity expression + } else { + advance(":"); + var v = expression(0); + a.push([n, v]); // holds an array of name/value expression pairs + } if (node.id !== ",") { break; } diff --git a/test/test-suite/groups/spread/case000.json b/test/test-suite/groups/spread/case000.json new file mode 100644 index 0000000..fafa2b9 --- /dev/null +++ b/test/test-suite/groups/spread/case000.json @@ -0,0 +1,11 @@ +{ + "expr": "{...Address, \"hello\":\"world\"}", + "dataset": "dataset1", + "bindings": {}, + "result": { + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN", + "hello": "world" + } +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case001.json b/test/test-suite/groups/spread/case001.json new file mode 100644 index 0000000..c555ef4 --- /dev/null +++ b/test/test-suite/groups/spread/case001.json @@ -0,0 +1,10 @@ +{ + "expr": "{...Address, \"Street\":\"world\"}", + "dataset": "dataset1", + "bindings": {}, + "result": { + "City": "Winchester", + "Postcode": "SO21 2JN", + "Street": "world" + } +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case002.json b/test/test-suite/groups/spread/case002.json new file mode 100644 index 0000000..8369317 --- /dev/null +++ b/test/test-suite/groups/spread/case002.json @@ -0,0 +1,10 @@ +{ + "expr": "{\"Street\": \"test\", ...Address}", + "dataset": "dataset1", + "bindings": {}, + "result": { + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN" + } +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case003.json b/test/test-suite/groups/spread/case003.json new file mode 100644 index 0000000..474fc9d --- /dev/null +++ b/test/test-suite/groups/spread/case003.json @@ -0,0 +1,11 @@ +{ + "expr": "{...Address, \"Test\": FirstName}", + "dataset": "dataset1", + "bindings": {}, + "result": { + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN", + "Test": "Fred" + } +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case004.json b/test/test-suite/groups/spread/case004.json new file mode 100644 index 0000000..72e1097 --- /dev/null +++ b/test/test-suite/groups/spread/case004.json @@ -0,0 +1,11 @@ +{ + "expr": "['yo', ...foo.blah.baz.fud, 'whats up']", + "dataset": "dataset0", + "bindings": {}, + "result": [ + "yo", + "hello", + "world", + "whats up" + ] +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case005.json b/test/test-suite/groups/spread/case005.json new file mode 100644 index 0000000..51039d7 --- /dev/null +++ b/test/test-suite/groups/spread/case005.json @@ -0,0 +1,8 @@ +{ + "expr": "{...Wrong.Address, \"hello\":\"world\"}", + "dataset": "dataset1", + "bindings": {}, + "result": { + "hello": "world" + } +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case006.json b/test/test-suite/groups/spread/case006.json new file mode 100644 index 0000000..cca2fd6 --- /dev/null +++ b/test/test-suite/groups/spread/case006.json @@ -0,0 +1,6 @@ +{ + "expr": "{...null, \"hello\":\"world\"}", + "dataset": "dataset1", + "bindings": {}, + "code": "T2015" +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case007.json b/test/test-suite/groups/spread/case007.json new file mode 100644 index 0000000..d936054 --- /dev/null +++ b/test/test-suite/groups/spread/case007.json @@ -0,0 +1,6 @@ +{ + "expr": "{...Phone, \"hello\":\"world\"}", + "dataset": "dataset1", + "bindings": {}, + "code": "T2015" +} \ No newline at end of file diff --git a/test/test-suite/groups/spread/case008.json b/test/test-suite/groups/spread/case008.json new file mode 100644 index 0000000..6d6b707 --- /dev/null +++ b/test/test-suite/groups/spread/case008.json @@ -0,0 +1,6 @@ +{ + "expr": "{...Surname, \"hello\":\"world\"}", + "dataset": "dataset1", + "bindings": {}, + "code": "T2015" +} \ No newline at end of file