1207 lines
44 KiB
JavaScript
1207 lines
44 KiB
JavaScript
/**
|
|
* © Copyright IBM Corp. 2016 All Rights Reserved
|
|
* Project name: JSONata
|
|
* This project is licensed under the MIT License, see LICENSE
|
|
*
|
|
* The files in this directory are tests that aren't really portable
|
|
* to other implementations for various reasons but they are included
|
|
* in order to achieve 100% coverage for this implementation.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var jsonata = require("../src/jsonata");
|
|
var chai = require("chai");
|
|
var chaiAsPromised = require("chai-as-promised");
|
|
chai.use(chaiAsPromised);
|
|
var expect = chai.expect;
|
|
|
|
var testdata1 = {
|
|
"foo": {
|
|
"bar": 42,
|
|
"blah": [{"baz": {"fud": "hello"}}, {"baz": {"fud": "world"}}, {"bazz": "gotcha"}],
|
|
"blah.baz": "here"
|
|
}, "bar": 98
|
|
};
|
|
|
|
var testdata2 = {
|
|
Account: {
|
|
"Account Name": "Firefly",
|
|
Order: [
|
|
{
|
|
OrderID: "order103",
|
|
Product: [
|
|
{
|
|
"Product Name": "Bowler Hat",
|
|
ProductID: 858383,
|
|
SKU: "0406654608",
|
|
Description: {
|
|
Colour: "Purple",
|
|
Width: 300,
|
|
Height: 200,
|
|
Depth: 210,
|
|
Weight: 0.75
|
|
},
|
|
Price: 34.45,
|
|
Quantity: 2
|
|
},
|
|
{
|
|
"Product Name": "Trilby hat",
|
|
ProductID: 858236,
|
|
SKU: "0406634348",
|
|
Description: {
|
|
Colour: "Orange",
|
|
Width: 300,
|
|
Height: 200,
|
|
Depth: 210,
|
|
Weight: 0.6
|
|
},
|
|
Price: 21.67,
|
|
Quantity: 1
|
|
}
|
|
]
|
|
},
|
|
{
|
|
OrderID: "order104",
|
|
Product: [
|
|
{
|
|
"Product Name": "Bowler Hat",
|
|
ProductID: 858383,
|
|
SKU: "040657863",
|
|
Description: {
|
|
Colour: "Purple",
|
|
Width: 300,
|
|
Height: 200,
|
|
Depth: 210,
|
|
Weight: 0.75
|
|
},
|
|
Price: 34.45,
|
|
Quantity: 4
|
|
},
|
|
{
|
|
ProductID: 345664,
|
|
SKU: "0406654603",
|
|
"Product Name": "Cloak",
|
|
Description: {
|
|
Colour: "Black",
|
|
Width: 30,
|
|
Height: 20,
|
|
Depth: 210,
|
|
Weight: 2.0
|
|
},
|
|
Price: 107.99,
|
|
Quantity: 1
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
describe("Functions with side-effects", () => {
|
|
describe("Evaluator - function: millis", function() {
|
|
describe("$millis() returns milliseconds since the epoch", function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata("$millis()");
|
|
// 27 Sep 2016, first commit to JSONata
|
|
expect(expr.evaluate(testdata2)).to.eventually.be.above(
|
|
1474934400
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("$millis() always returns same value within an expression", function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata(
|
|
'{"now": $millis(), "delay": $sum([1..10000]), "later": $millis()}.(now = later)'
|
|
);
|
|
expect(expr.evaluate(testdata2)).to.eventually.equal(true);
|
|
});
|
|
});
|
|
|
|
describe("$millis() returns different timestamp for subsequent evaluate() calls", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("($sum([1..10000]); $millis())");
|
|
var result = await expr.evaluate(testdata2);
|
|
var result2 = await expr.evaluate(testdata2);
|
|
expect(result).to.not.equal(result2);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("$now() returns timestamp", function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata("$now()");
|
|
var result = expr.evaluate(testdata2);
|
|
// follows this pattern - "2017-05-09T10:10:16.918Z"
|
|
expect(result).to.eventually.match(
|
|
/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("$now() returns timestamp with defined format", function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata("$now('[h]:[M01][P] [z]')");
|
|
var result = expr.evaluate(testdata2);
|
|
// follows this pattern - "10:23am GMT+00:00"
|
|
expect(result).to.eventually.match(/^\d?\d:\d\d[ap]m GMT\+00:00$/);
|
|
});
|
|
});
|
|
|
|
describe("$now() returns timestamp with defined format and timezone", function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata("$now('[h]:[M01][P] [z]', '-0500')");
|
|
var result = expr.evaluate(testdata2);
|
|
// follows this pattern - "10:23am GMT-05:00"
|
|
expect(result).to.eventually.match(/^\d?\d:\d\d[ap]m GMT-05:00$/);
|
|
});
|
|
});
|
|
|
|
describe("$now() always returns same value within an expression", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('{"now": $now(), "delay": $sum([1..10000]), "later": $now()}.(now = later)');
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = true;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("$now() returns different timestamp for subsequent evaluate() calls", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("($sum([1..100000]); $now())");
|
|
var result = await expr.evaluate(testdata2);
|
|
var result2 = await expr.evaluate(testdata2);
|
|
expect(result).to.not.equal(result2);
|
|
});
|
|
});
|
|
|
|
describe("$millis() returns milliseconds since the epoch", function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata("$millis()");
|
|
// 27 Sep 2016, first commit to JSONata
|
|
expect(expr.evaluate(testdata2)).to.eventually.be.above(1474934400);
|
|
});
|
|
});
|
|
|
|
describe("Evaluator - functions: random", function() {
|
|
describe('random number")', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$random()");
|
|
var result = await expr.evaluate();
|
|
var expected = result >= 0 && result < 1;
|
|
expect(expected).to.equal(true);
|
|
});
|
|
});
|
|
|
|
describe('consequetive random numbers should be different")', function() {
|
|
it("should return result object", function() {
|
|
var expr = jsonata("$random() = $random()");
|
|
var expected = false;
|
|
expect(expr.evaluate()).to.eventually.deep.equal(expected);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests that rely on JavaScript-style object traversal", () => {
|
|
// A JSON object is an unordered list of key-value pairs.
|
|
// When traversing an object, the entries may be returned
|
|
// in a non-deterministic order (depending on the language).
|
|
// The following tests assume a traversal order which works
|
|
// in JavaScript but may not apply to other languages.
|
|
// See https://github.com/jsonata-js/jsonata/issues/179.
|
|
describe('foo.*[0]', function () {
|
|
it('should return result object', async function () {
|
|
var expr = jsonata("foo.*[0]");
|
|
var result = await expr.evaluate(testdata1);
|
|
var expected = 42;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('**[2]', function () {
|
|
it('should return result object', async function () {
|
|
var expr = jsonata("**[2]");
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = "Firefly";
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests that use the $clone() function", () => {
|
|
// $clone() allows jsonata-js to play nicely with Node-RED.
|
|
// It's not part of the JSONata standard.
|
|
// See https://github.com/jsonata-js/jsonata/issues/207.
|
|
describe('clone undefined', function () {
|
|
it('should return undefined', async function () {
|
|
var expr = jsonata('$clone(foo)');
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = undefined;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('clone empty object', function () {
|
|
it('should return empty object', async function () {
|
|
var expr = jsonata('$clone({})');
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = {};
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('clone object', function () {
|
|
it('should return same object', async function () {
|
|
var expr = jsonata('$clone({"a": 1})');
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = {"a": 1};
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("transform expression with overridden $clone function", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('Account ~> |Order|{"Product":"blah"},nomatch|');
|
|
var count = 0;
|
|
expr.registerFunction("clone", function(arg) {
|
|
count++;
|
|
return JSON.parse(JSON.stringify(arg));
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = {
|
|
"Account Name": "Firefly",
|
|
Order: [
|
|
{
|
|
OrderID: "order103",
|
|
Product: "blah"
|
|
},
|
|
{
|
|
OrderID: "order104",
|
|
Product: "blah"
|
|
}
|
|
]
|
|
};
|
|
expect(result).to.deep.equal(expected);
|
|
expect(count).to.equal(1);
|
|
});
|
|
});
|
|
|
|
describe('transform expression with overridden $clone value', function () {
|
|
it('should throw error', async function () {
|
|
var expr = jsonata('( $clone := 5; $ ~> |Account.Order.Product|{"blah":"foo"}| )');
|
|
expect(
|
|
expr.evaluate(testdata2)
|
|
).to.eventually.be.rejected.to.deep.contain({ code: "T2013" });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests that bind Javascript functions", () => {
|
|
// These involve binding of functions
|
|
describe("Override implementation of $now()", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$now()");
|
|
expr.registerFunction("now", function() {
|
|
return "time for tea";
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
expect(result).to.equal("time for tea");
|
|
});
|
|
});
|
|
|
|
// Issue #261. Previously we would attempt to assign to the read-only `message` property,
|
|
// causing an unrelated `TypeError` to be thrown instead
|
|
describe("function throws a `DOMException` with a read-only `message` property", function() {
|
|
/**
|
|
* `DOMException` is not available in our testing environment. Additionally, we can't
|
|
* just import the `domexception` module since it doesn't work on Node.js v4, which
|
|
* we still support. So, here's a fake skeleton implementation which has the relevant
|
|
* qualities we need to reproduce the bug, most importantly a read-only `message`
|
|
* property
|
|
* @param {string} message - Error message
|
|
* @constructor
|
|
*/
|
|
function DOMException (message) {
|
|
Object.defineProperty(this, "message", {
|
|
get() {
|
|
return message;
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
Object.setPrototypeOf(DOMException.prototype, Error.prototype);
|
|
|
|
it("rethrows correctly", function() {
|
|
var expr = jsonata("$throwDomEx()");
|
|
expr.registerFunction("throwDomEx", function() {
|
|
throw new DOMException('Here is my message');
|
|
});
|
|
expect(expr.evaluate({}))
|
|
.to.eventually.be.rejectedWith(DOMException)
|
|
.to.deep.contain({
|
|
message: "Here is my message",
|
|
position: 12,
|
|
token: "throwDomEx",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("map a user-defined Javascript function with signature", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$map([1,4,9,16], $squareroot)");
|
|
expr.registerFunction(
|
|
"squareroot",
|
|
function(num) {
|
|
return Math.sqrt(num);
|
|
},
|
|
"<n:n>"
|
|
);
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = [1, 2, 3, 4];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
describe("map a user-defined Javascript function with undefined signature", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$map([1,4,9,16], $squareroot)");
|
|
expr.registerFunction("squareroot", function(num) {
|
|
return Math.sqrt(num);
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = [1, 2, 3, 4];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("map a user-defined Javascript function", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$map([1,4,9,16], $squareroot)");
|
|
expr.assign("squareroot", function(num) {
|
|
return Math.sqrt(num);
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = [1, 2, 3, 4];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("$filter with a user-defined Javascript function", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$filter([1,4,9,16], $even)");
|
|
expr.assign("even", function(num) {
|
|
return num % 2 === 0;
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = [4, 16];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("$sift with a user-defined Javascript function", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$sift({'one': 1, 'four': 4, 'nine': 9, 'sixteen': 16}, $even)");
|
|
expr.assign("even", function(num) {
|
|
return num % 2 === 0;
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = {'four': 4, 'sixteen': 16};
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("$each with a user-defined Javascript function", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$each({'one': 1, 'four': 4, 'nine': 9, 'sixteen': 16}, $squareroot)");
|
|
expr.assign("squareroot", function(num) {
|
|
return Math.sqrt(num);
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = [1, 2, 3, 4];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("Partially apply user-defined Javascript function", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata(
|
|
"(" +
|
|
" $firstn := $substr(?, 0, ?);" +
|
|
" $first5 := $firstn(?, 5);" +
|
|
' $first5("Hello World")' +
|
|
")"
|
|
);
|
|
expr.assign("substr", function(str, start, len) {
|
|
return str.substr(start, len);
|
|
});
|
|
var result = await expr.evaluate(testdata2);
|
|
var expected = "Hello";
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("User defined matchers", function() {
|
|
var repeatingLetters = function(char, repeat) {
|
|
// custom matcher to match `repeat` contiguous occurrences of `char`
|
|
var chars = char.repeat(repeat);
|
|
var match = function(str, offset) {
|
|
var pos = str.indexOf(chars, (offset || 0));
|
|
if (pos === -1) {
|
|
return;
|
|
} else {
|
|
return {
|
|
match: chars,
|
|
start: pos,
|
|
end: pos + chars.length,
|
|
groups: [],
|
|
next: function () {
|
|
return match(str, pos + chars.length);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return match;
|
|
};
|
|
|
|
it("should match using a custom matcher", async function() {
|
|
var expr = jsonata("$match('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2))");
|
|
expr.registerFunction("repeatingLetters", repeatingLetters);
|
|
var result = await expr.evaluate();
|
|
var expected = [
|
|
{"match": "LL", "index": 0, "groups": []},
|
|
{"match": "LL", "index": 10, "groups": []},
|
|
{"match": "LL", "index": 18, "groups": []},
|
|
{"match": "LL", "index": 37, "groups": []},
|
|
{"match": "LL", "index": 39, "groups": []}
|
|
];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
it("should split using a custom matcher", async function() {
|
|
var expr = jsonata("$split('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2))");
|
|
expr.registerFunction("repeatingLetters", repeatingLetters);
|
|
var result = await expr.evaluate();
|
|
var expected = ["","ANFAIRPW","GWYNGY","GOGERYCHWYRNDROBW","","ANTYSILIOGOGOGOCH"];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
it("should replace using a custom matcher", async function() {
|
|
var expr = jsonata("$replace('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2), 'Ỻ')");
|
|
expr.registerFunction("repeatingLetters", repeatingLetters);
|
|
var result = await expr.evaluate();
|
|
var expected = "ỺANFAIRPWỺGWYNGYỺGOGERYCHWYRNDROBWỺỺANTYSILIOGOGOGOCH";
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
it("should test inclusion using a custom matcher", async function() {
|
|
var expr = jsonata("$contains('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 4))");
|
|
expr.registerFunction("repeatingLetters", repeatingLetters);
|
|
var result = await expr.evaluate();
|
|
var expected = true;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
});
|
|
|
|
describe('User defined higher-order functions', () => {
|
|
var myfunc = async (arr, fn) => 2 * (await fn(arr));
|
|
|
|
var startsWith = function(str) {
|
|
// returns a function that returns true if its argument starts with the string `str`
|
|
return (arg) => {
|
|
return arg.startsWith(str);
|
|
};
|
|
};
|
|
|
|
it('should be able to invoke a built-in function passed as an argument', async () => {
|
|
var expr = jsonata("$myfunc([1,2,3], $sum)");
|
|
expr.registerFunction('myfunc', myfunc);
|
|
var result = await expr.evaluate();
|
|
var expected = 12;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
it('should be able to invoke a lambda function passed as an argument', async () => {
|
|
var expr = jsonata("$myfunc([1,2,3], λ($arr) { $arr[1] + $arr[2] })");
|
|
expr.registerFunction('myfunc', myfunc);
|
|
var result = await expr.evaluate();
|
|
var expected = 10;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
it('should be able to invoke a user-defined function passed as an argument', async () => {
|
|
var expr = jsonata("$myfunc([1,2,3], $myfunc2)");
|
|
expr.registerFunction('myfunc', myfunc);
|
|
expr.registerFunction('myfunc2', (arr) => {
|
|
return 2 * arr[1];
|
|
});
|
|
var result = await expr.evaluate();
|
|
var expected = 8;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
|
|
it('should be able to return a function from a user-defined function', async () => {
|
|
var expr = jsonata(`
|
|
(
|
|
$startsWithHello := $startsWith("Hello");
|
|
[$startsWithHello("Hello, Bob"), $startsWithHello("Goodbye, Bill")]
|
|
)`);
|
|
expr.registerFunction('startsWith', startsWith);
|
|
var result = await expr.evaluate();
|
|
var expected = [true, false];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('User defined generator function', () => {
|
|
var myAddFunc = function*(val) {
|
|
yield val + 10;
|
|
};
|
|
|
|
var myArrayFunc = function *() {
|
|
yield [1,2,3];
|
|
};
|
|
|
|
var myObjectFunc = function* () {
|
|
yield {
|
|
downloads: [
|
|
{
|
|
downloads: 1,
|
|
day: "2016-09-01",
|
|
},
|
|
{
|
|
downloads: 2,
|
|
day: "2016-09-02",
|
|
},
|
|
{
|
|
downloads: 3,
|
|
day: "2016-09-03",
|
|
},
|
|
{
|
|
downloads: 1453,
|
|
day: "2017-03-10",
|
|
},
|
|
{
|
|
downloads: 1194,
|
|
day: "2017-03-11",
|
|
},
|
|
{
|
|
downloads: 988,
|
|
day: "2017-03-12",
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
it('should be able to invoke a generator function returning a simple value', async () => {
|
|
var expr = jsonata("$myAddFunc(1)");
|
|
expr.registerFunction('myAddFunc', myAddFunc);
|
|
|
|
var result = await expr.evaluate();
|
|
|
|
expect(result).to.equal(11);
|
|
});
|
|
|
|
it('should be able to invoke a generator function and map over its return array value', async () => {
|
|
var expr = jsonata("$myArrayFunc().{\"foo\": \"bar\"}");
|
|
expr.registerFunction('myArrayFunc', myArrayFunc);
|
|
|
|
var result = await expr.evaluate();
|
|
|
|
expect(result).to.deep.equal([
|
|
{
|
|
"foo": "bar"
|
|
},
|
|
{
|
|
"foo": "bar"
|
|
},
|
|
{
|
|
"foo": "bar"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it('should be able to invoke a generator function and map over its return object value', async () => {
|
|
var expr = jsonata("$myObjectFunc().downloads{ $substring(day, 0, 7): $sum(downloads) }");
|
|
expr.registerFunction('myObjectFunc', myObjectFunc);
|
|
|
|
var result = await expr.evaluate();
|
|
|
|
expect(result).to.deep.equal({ '2016-09': 6, '2017-03': 3635 });
|
|
});
|
|
});
|
|
|
|
describe('User defined higher-order generator functions', () => {
|
|
var myfunc = function*(arr, fn) {
|
|
const val = yield* fn(arr);
|
|
return 2 * val;
|
|
};
|
|
|
|
// FIXME:
|
|
it('a higher-order generator function will not work', async () => {
|
|
var expr = jsonata("$myfunc([1,2,3], $sum)");
|
|
expr.registerFunction('myfunc', myfunc);
|
|
try {
|
|
await expr.evaluate();
|
|
} catch (e) {
|
|
expect(e.message).to.equal("yield* (intermediate value)(intermediate value) is not iterable");
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests that are specific to a Javascript runtime", () => {
|
|
// Javascript specific
|
|
describe('/ab/ ("ab")', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/ab/ ("ab")');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "ab", start: 0, end: 2, groups: [] };
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe("/ab/ ()", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("/ab/ ()");
|
|
var result = await expr.evaluate();
|
|
var expected = undefined;
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe('/ab+/ ("ababbabbcc")', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/ab+/ ("ababbabbcc")');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "ab", start: 0, end: 2, groups: [] };
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe('/a(b+)/ ("ababbabbcc")', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/a(b+)/ ("ababbabbcc")');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "ab", start: 0, end: 2, groups: ["b"] };
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe('/a(b+)/ ("ababbabbcc").next()', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/a(b+)/ ("ababbabbcc").next()');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "abb", start: 2, end: 5, groups: ["bb"] };
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe('/a(b+)/ ("ababbabbcc").next().next()', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/a(b+)/ ("ababbabbcc").next().next()');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "abb", start: 5, end: 8, groups: ["bb"] };
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe('/a(b+)/ ("ababbabbcc").next().next().next()', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/a(b+)/ ("ababbabbcc").next().next().next()');
|
|
var result = await expr.evaluate();
|
|
var expected = undefined;
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe('/a(b+)/i ("Ababbabbcc")', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('/a(b+)/i ("Ababbabbcc")');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "Ab", start: 0, end: 2, groups: ["b"] };
|
|
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
|
|
});
|
|
});
|
|
|
|
describe("empty regex", function() {
|
|
it("should throw error", function() {
|
|
expect(function() {
|
|
var expr = jsonata("//");
|
|
expr.evaluate();
|
|
})
|
|
.to.throw()
|
|
.to.deep.contain({ position: 1, code: "S0301" });
|
|
});
|
|
});
|
|
|
|
describe("empty regex", function() {
|
|
it("should throw error", function() {
|
|
expect(function() {
|
|
var expr = jsonata("/");
|
|
expr.evaluate();
|
|
})
|
|
.to.throw()
|
|
.to.deep.contain({ position: 1, code: "S0302" });
|
|
});
|
|
});
|
|
|
|
describe("empty regex: Escaped termination", function() {
|
|
it("should throw error", function() {
|
|
expect(function() {
|
|
var expr = jsonata("/\\/");
|
|
expr.evaluate();
|
|
})
|
|
.to.throw()
|
|
.to.deep.contain({ position: 3, code: "S0302" });
|
|
});
|
|
});
|
|
|
|
describe("empty regex: Escaped termination", function() {
|
|
it("should throw error", function() {
|
|
expect(function() {
|
|
var expr = jsonata("/\\\\\\/");
|
|
expr.evaluate();
|
|
})
|
|
.to.throw()
|
|
.to.deep.contain({ position: 5, code: "S0302" });
|
|
});
|
|
});
|
|
|
|
describe("Functions - $match", function() {
|
|
describe('$match("test escape \\\\", /\\\\/)', function() {
|
|
it("should find \\", async function() {
|
|
var expr = jsonata('$match("test escape \\\\", /\\\\/)');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "\\", index: 12, groups: []};
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('$match("ababbabbcc",/ab/)', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('$match("ababbabbcc",/ab/)');
|
|
var result = await expr.evaluate();
|
|
var expected = [
|
|
{ match: "ab", index: 0, groups: [] },
|
|
{
|
|
match: "ab",
|
|
index: 2,
|
|
groups: []
|
|
},
|
|
{ match: "ab", index: 5, groups: [] }
|
|
];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('$match("ababbabbcc",/a(b+)/)', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('$match("ababbabbcc",/a(b+)/)');
|
|
var result = await expr.evaluate();
|
|
var expected = [
|
|
{ match: "ab", index: 0, groups: ["b"] },
|
|
{
|
|
match: "abb",
|
|
index: 2,
|
|
groups: ["bb"]
|
|
},
|
|
{ match: "abb", index: 5, groups: ["bb"] }
|
|
];
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('$match("ababbabbcc",/a(b+)/, 1)', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('$match("ababbabbcc",/a(b+)/, 1)');
|
|
var result = await expr.evaluate();
|
|
var expected = { match: "ab", index: 0, groups: ["b"] };
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('$match("ababbabbcc",/a(b+)/, 0)', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('$match("ababbabbcc",/a(b+)/, 0)');
|
|
var result = await expr.evaluate();
|
|
var expected = undefined;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe("$match(nothing,/a(xb+)/)", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$match(nothing,/a(xb+)/)");
|
|
var result = await expr.evaluate();
|
|
var expected = undefined;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('$match("ababbabbcc",/a(xb+)/)', function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata('$match("ababbabbcc",/a(xb+)/)');
|
|
var result = await expr.evaluate();
|
|
var expected = undefined;
|
|
expect(result).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
describe('$match("a, b, c, d", /ab/, -3)', function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata('$match("a, b, c, d", /ab/, -3)');
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "D3040",
|
|
token: "match",
|
|
index: 3,
|
|
value: -3,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('$match("a, b, c, d", /ab/, null)', function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata('$match("a, b, c, d", /ab/, null)');
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "T0410",
|
|
token: "match",
|
|
index: 3,
|
|
value: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('$match("a, b, c, d", /ab/, "2")', function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata('$match("a, b, c, d", /ab/, "2")');
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "T0410",
|
|
token: "match",
|
|
index: 3,
|
|
value: "2",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('$match("a, b, c, d", "ab")', function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata('$match("a, b, c, d", "ab")');
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "T0410",
|
|
token: "match",
|
|
index: 2,
|
|
value: "ab",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('$match("a, b, c, d", true)', function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata('$match("a, b, c, d", true)');
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "T0410",
|
|
token: "match",
|
|
index: 2,
|
|
value: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("$match(12345, 3)", function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata("$match(12345, 3)");
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "T0410",
|
|
token: "match",
|
|
index: 1,
|
|
value: 12345,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("$match(12345)", function() {
|
|
it("should throw error", function() {
|
|
var expr = jsonata("$match(12345)");
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 7,
|
|
code: "T0410",
|
|
token: "match",
|
|
index: 1,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Expressions that attempt to access the object prototype", function() {
|
|
const data = {"foo": {"bar": "baz"}};
|
|
it("should ignore __proto__", async function() {
|
|
const expr = jsonata('foo.__proto__');
|
|
const result = await expr.evaluate(data);
|
|
expect(result).to.deep.equal(undefined);
|
|
});
|
|
|
|
it("should throw an error trying to invoke toString()", async function() {
|
|
const expr = jsonata('foo.toString()');
|
|
expect(
|
|
expr.evaluate(data)
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 13,
|
|
code: "T1006",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Expressions that attempt to pollute the object prototype", function() {
|
|
it("should ignore __proto__", async function() {
|
|
const expr = jsonata('{} ~> | __proto__ | {"is_admin": true} |');
|
|
const result = await expr.evaluate();
|
|
expect(result).to.deep.equal({});
|
|
});
|
|
it("should throw an error with __lookupGetter__", async function() {
|
|
const expr = jsonata('{} ~> | __lookupGetter__("__proto__")() | {"is_admin": true} |');
|
|
expect(
|
|
expr.evaluate()
|
|
).to.eventually.be.rejected.to.deep.contain({
|
|
position: 25,
|
|
code: "T1006",
|
|
});
|
|
});
|
|
it("should ignore constructor", async function() {
|
|
const expr = jsonata('{} ~> | constructor | {"is_admin": true} |');
|
|
const result = await expr.evaluate();
|
|
expect(result).to.deep.equal({});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Test that yield platform specific results", () => {
|
|
// Platform specific
|
|
describe("$sqrt(10) * $sqrt(10)", function() {
|
|
it("should return result object", async function() {
|
|
var expr = jsonata("$sqrt(10) * $sqrt(10)");
|
|
var result = await expr.evaluate();
|
|
var expected = 10;
|
|
expect(result).to.be.closeTo(expected, 1e-13);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests that include infinite recursion", () => {
|
|
describe("stack overflow - infinite recursive function - non-tail call", function() {
|
|
it("should throw error", function() {
|
|
const options = {
|
|
'timeout': 1000,
|
|
'stack': 300
|
|
}
|
|
const expr = jsonata("($inf := function($n){$n+$inf($n-1)}; $inf(5))", options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
token: "inf",
|
|
position: 30,
|
|
code: "D1011",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("stack overflow - infinite recursive function - tail call", function() {
|
|
this.timeout(5000);
|
|
it("should throw error", function() {
|
|
const options = {
|
|
'timeout': 1000,
|
|
'stack': 500
|
|
}
|
|
const expr = jsonata("( $inf := function(){$inf()}; $inf())", options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
token: "inf",
|
|
code: "D1012",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("stack overflow - infinite recursive function - tail call (no stack guardrail)", function() {
|
|
this.timeout(5000);
|
|
it("should throw error", function() {
|
|
const options = {
|
|
'timeout': 1000
|
|
}
|
|
const expr = jsonata("( $inf := function(){$inf()}; $inf())", options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
token: "inf",
|
|
code: "D1012",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("guardrails on Ackermann function", function() {
|
|
this.timeout(5000);
|
|
const ackermann = (m, n) => `
|
|
(
|
|
$ack := function($m, $n) {
|
|
$m = 0 ? $n + 1 :
|
|
$n = 0 ? $ack($m - 1, 1) :
|
|
$ack($m - 1, $ack($m, $n - 1))
|
|
};
|
|
|
|
$ack(${m}, ${n})
|
|
)`;
|
|
|
|
it("should complete for small parameters", async function() {
|
|
const options = {
|
|
'timeout': 1000,
|
|
'stack': 500
|
|
}
|
|
const expr = jsonata(ackermann(3, 4), options);
|
|
const result = await expr.evaluate();
|
|
expect(result).to.equal(125);
|
|
});
|
|
|
|
it("larger inputs cause stack overflow", function() {
|
|
const options = {
|
|
'stack': 500
|
|
}
|
|
const expr = jsonata(ackermann(4, 4), options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
token: "ack",
|
|
code: "D1011",
|
|
});
|
|
});
|
|
|
|
it("larger inputs cause stack overflow", function() {
|
|
const options = {
|
|
'stack': 500
|
|
}
|
|
const expr = jsonata(ackermann(4, 4), options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
token: "ack",
|
|
code: "D1011",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("guardrails on sequence length", function() {
|
|
it("prevents large ranges", function() {
|
|
const options = {
|
|
'sequence': 1000
|
|
}
|
|
const expr = jsonata('[0..1001]', options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
code: "D2015",
|
|
});
|
|
});
|
|
|
|
it("prevents large intermediate sequences", function() {
|
|
const options = {
|
|
'sequence': 1000
|
|
}
|
|
const expr = jsonata('[0..100].([0..100]) ~> count()', options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
code: "D2015",
|
|
});
|
|
});
|
|
|
|
it("prevents appending large sequences", function() {
|
|
const options = {
|
|
'sequence': 1000
|
|
}
|
|
const expr = jsonata('$append([0..600], [0..600]) ~> $count()', options);
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
code: "D2015",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests invalid object creation", () => {
|
|
it("prevents creating an object mimicking a lambda", () => {
|
|
const expr = jsonata('($lambda = {"_jsonata_lambda": true}; $lambda())');
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
code: "D1013",
|
|
});
|
|
})
|
|
it("prevents creating an object mimicking a function", () => {
|
|
const expr = jsonata('($fn = {"_jsonata_function": true}; $fn())');
|
|
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
|
|
code: "D1013",
|
|
});
|
|
})
|
|
})
|
|
|
|
describe("Tests that use internal frame push callbacks", () => {
|
|
describe("frame push callback bound to expression", function() {
|
|
it("calls callback when new frame created", function(done) {
|
|
var expr = jsonata("( )");
|
|
// gets called twice
|
|
let count = 0;
|
|
expr.assign(Symbol.for('jsonata.__createFrame_push'), function(parentEnv, newEnv) {
|
|
expect(parentEnv).to.not.equal(newEnv);
|
|
expect(parentEnv).to.include.keys(['lookup', 'bind']);
|
|
expect(newEnv).to.include.keys(['lookup', 'bind']);
|
|
count++;
|
|
if (count == 2) done();
|
|
});
|
|
expr.evaluate();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Concurrency tests", () => {
|
|
it("invokes a single compiled expression multiple times concurrently", async () => {
|
|
const expression = `
|
|
{
|
|
"id": $$.id,
|
|
"delayed": $slow($$.id)
|
|
}`;
|
|
|
|
|
|
const compiled = jsonata(expression);
|
|
|
|
// Async user function — long-running on purpose.
|
|
compiled.registerFunction(
|
|
"slow",
|
|
async (id) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
return `done:${id}`;
|
|
},
|
|
"<s:s>",
|
|
);
|
|
|
|
const inputs = [
|
|
{ id: "a" },
|
|
{ id: "b" },
|
|
{ id: "c" },
|
|
];
|
|
|
|
const results = await Promise.all(
|
|
inputs.map((input) => compiled.evaluate(input)),
|
|
);
|
|
|
|
expect(results).to.deep.equal([
|
|
{"id": "a", "delayed": "done:a"},
|
|
{"id": "b", "delayed": "done:b"},
|
|
{"id": "c", "delayed": "done:c"}
|
|
])
|
|
})
|
|
}) |