UNPKG

expression-language

Version:

Javascript implementation of symfony/expression-language

650 lines (644 loc) 19.2 kB
"use strict"; var _ExpressionLanguage = _interopRequireDefault(require("../ExpressionLanguage")); var _ExpressionFunction = _interopRequireDefault(require("../ExpressionFunction")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } test('short circuit evaluate', () => { let obj = { foo: () => { throw new Error("This method should not be called due to short circuiting."); } }; let shortCircuits = [['false && object.foo()', { object: obj }, false], ['false and object.foo()', { object: obj }, false], ['true || object.foo()', { object: obj }, true], ['true or object.foo()', { object: obj }, true]]; for (let shortCircuit of shortCircuits) { //console.log("Testing: ", shortCircuit[0]); let exprLang = new _ExpressionLanguage.default(); expect(exprLang.evaluate(shortCircuit[0], shortCircuit[1])).toBe(shortCircuit[2]); } }); test('short circuit compile', () => { let shortCircuits = [['false && foo', [{ foo: 'foo' }], false], ['false and foo', [{ foo: 'foo' }], false], ['true || foo', [{ foo: 'foo' }], true], ['true or foo', [{ foo: 'foo' }], true]]; for (let shortCircuit of shortCircuits) { let exprLang = new _ExpressionLanguage.default(); let compiled = exprLang.compile(shortCircuit[0], shortCircuit[1]); expect(eval(compiled)).toBe(shortCircuit[2]); } }); test('caching for overridden variable names', () => { let expressionLanguage = new _ExpressionLanguage.default(), expression = 'a + b'; expressionLanguage.evaluate(expression, { a: 1, b: 1 }); let result = expressionLanguage.compile(expression, ['a', { 'B': 'b' }]); expect(result).toBe("(a + B)"); }); test('bitwise ~', () => { const el = new _ExpressionLanguage.default(); const res = el.evaluate("~4"); expect(res).toBe(-5); }); describe('supports all literals', () => { const el = new _ExpressionLanguage.default(); test("strings", () => { const res = el.evaluate("'testing 1234'"); expect(res).toBe("testing 1234"); const res2 = el.evaluate('"testing 1234"'); expect(res2).toBe("testing 1234"); }); test("numbers", () => { expect(el.evaluate('123')).toBe(123); expect(el.evaluate('0.787')).toBeCloseTo(0.787); expect(el.evaluate('.1234')).toBeCloseTo(0.1234); expect(el.evaluate('1_000_000')).toBe(1000000); }); test("arrays", () => { const arr = el.evaluate('[1, 2, 3]'); expect(Array.isArray(arr)).toBe(true); expect(arr).toEqual([1, 2, 3]); expect(el.evaluate('[1, 2, 3][1]')).toBe(2); // comments inside arrays should be ignored const arrWithComments = el.evaluate('[1 /* a */, 2, /* b */ 3]'); expect(arrWithComments).toEqual([1, 2, 3]); }); test('hashes', () => { const res = el.evaluate("({foo: 'bar'}).foo"); expect(res).toBe('bar'); }); test("booleans", () => { expect(el.evaluate('true')).toBe(true); expect(el.evaluate('false')).toBe(false); }); test('null', () => { expect(el.evaluate('null')).toBeNull(); }); test('exponential', () => { expect(el.evaluate('1e-2')).toBeCloseTo(0.01); expect(el.evaluate('-.7_189e+10')).toBeCloseTo(-7189000000); }); test('comments', () => { expect(el.evaluate('/* ignored */ 1 + 2')).toBe(3); }); }); test('strict equality', () => { let expressionLanguage = new _ExpressionLanguage.default(), expression = '123 === a'; let result = expressionLanguage.compile(expression, ['a']); expect(result).toBe("(123 === a)"); }); // New tests adapted from Symfony ExpressionLanguageTest (PHP) test('cached parse returns same instance', () => { const el = new _ExpressionLanguage.default(); const first = el.parse('1 + 1', []); const second = el.parse('1 + 1', []); expect(second).toBe(first); }); test('parse returns same object when already parsed', () => { const el = new _ExpressionLanguage.default(); const parsed = el.parse('1 + 1', []); const again = el.parse(parsed, []); expect(again).toBe(parsed); }); test('caching with different names order yields same parsed object', () => { const el = new _ExpressionLanguage.default(); const expr = 'a + b'; const first = el.parse(expr, ['a', { B: 'b' }]); const second = el.parse(expr, [{ B: 'b' }, 'a']); expect(second).toBe(first); }); test('register after parse', () => { let callbacks = getRegisterCallbacks(); for (let callback of callbacks) { try { let expressionLanguage = new _ExpressionLanguage.default(); expressionLanguage.parse("1 + 1", []); callback[0](expressionLanguage); console.log("Shouldn't get to this point."); expect(true).toBe(false); } catch (err) { //console.log(err); expect(err.name).toBe('LogicException'); } } }); test('register after eval', () => { let callbacks = getRegisterCallbacks(); for (let callback of callbacks) { try { let expressionLanguage = new _ExpressionLanguage.default(); expressionLanguage.evaluate("1 + 1"); callback[0](expressionLanguage); console.log("Shouldn't get to this point."); expect(true).toBe(false); } catch (err) { //console.log(err); expect(err.name).toBe('LogicException'); } } }); test('register after compile', () => { let callbacks = getRegisterCallbacks(); for (let callback of callbacks) { try { let expressionLanguage = new _ExpressionLanguage.default(); expressionLanguage.compile("1 + 1"); callback[0](expressionLanguage); console.log("Shouldn't get to this point."); expect(true).toBe(false); } catch (err) { //console.log(err); expect(err.name).toBe('LogicException'); } } }); test('bad callable', () => { try { let expressionLanguage = new _ExpressionLanguage.default(); expressionLanguage.evaluate("foo.myfunction()", { foo: {} }); console.log("Shouldn't get to this point."); expect(true).toBe(false); } catch (err) { //console.log(err); expect(err.toString()).toBe('Error: Method "myfunction" is undefined on object.'); } }); test('built-in min function', () => { const el = new _ExpressionLanguage.default(); const expr = 'min(1,2,3)'; const compiled = el.compile(expr, []); expect(compiled).toBe('Math.min(1, 2, 3)'); const result = el.evaluate(expr, {}); expect(result).toBe(1); }); test('built-in max function', () => { const el = new _ExpressionLanguage.default(); const expr = 'max(1,2,3)'; const compiled = el.compile(expr, []); expect(compiled).toBe('Math.max(1, 2, 3)'); const result = el.evaluate(expr, {}); expect(result).toBe(3); }); test('built-in constant function evaluates globals and dotted paths', () => { const el = new _ExpressionLanguage.default(); expect(el.evaluate('constant("Math.PI")')).toBe(Math.PI); // also via compile+eval const code = el.compile('constant("Math.E")', []); expect(eval(code)).toBe(Math.E); }); test('built-in constant function falls back to values map', () => { const el = new _ExpressionLanguage.default(); const values = { FOO: 42 }; expect(el.evaluate('constant("FOO")', values)).toBe(42); }); test('built-in constant returns undefined for unknown or invalid names', () => { const el = new _ExpressionLanguage.default(); expect(el.evaluate('constant("This.Does.Not.Exist")')).toBeUndefined(); expect(el.evaluate('constant(123)')).toBeUndefined(); expect(el.evaluate('constant("")')).toBeUndefined(); }); // enum() tests test('built-in enum evaluates and compiles using PHP-like FQN string', () => { const el = new _ExpressionLanguage.default(); // prepare a global-like namespace with an enum-like object const root = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : global; root.App = root.App || {}; root.App.SomeNamespace = root.App.SomeNamespace || {}; root.App.SomeNamespace.Foo = { Bar: { kind: 'Foo.Bar' } }; // PHP-like input with backslashes and :: const expr = 'enum("App\\\\SomeNamespace\\\\Foo::Bar")'; const value = el.evaluate(expr); expect(value).toMatchObject({ kind: 'Foo.Bar' }); const code = el.compile(expr, []); // ensure global is visible to eval expect(eval(code)).toMatchObject({ kind: 'Foo.Bar' }); }); test('built-in enum supports dotted path as well', () => { const el = new _ExpressionLanguage.default(); const root = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : global; root.App = root.App || {}; root.App.Other = { Enum: { CaseA: { ok: true } } }; expect(el.evaluate('enum("App.Other.Enum.CaseA")')).toMatchObject({ ok: true }); }); test('built-in enum returns undefined on invalid input or missing members', () => { const el = new _ExpressionLanguage.default(); expect(el.evaluate('enum(123)')).toBeUndefined(); expect(el.evaluate('enum("")')).toBeUndefined(); expect(el.evaluate('enum("Not.Exist::Nope")')).toBeUndefined(); }); test('operator collisions evaluate and compile', () => { const el = new _ExpressionLanguage.default(); const expr = 'foo.not in [bar]'; const compiled = el.compile(expr, ['foo', 'bar']); // compiled code should be evaluable and return true expect(compiled).toBe("includes(foo.not, [bar])"); const resultEvaluated = el.evaluate(expr, { foo: { not: 'test' }, bar: 'test' }); expect(resultEvaluated).toBe(true); }); test('parse throws on incomplete expression (node.)', () => { const el = new _ExpressionLanguage.default(); expect(() => el.parse('node.', ['node'])).toThrow(); }); test('comments ignored in evaluate and compile', () => { const el = new _ExpressionLanguage.default(); expect(el.evaluate('1 /* foo */ + 2')).toBe(3); expect(el.compile('1 /* foo */ + 2')).toBe('(1 + 2)'); }); test('providers evaluate and compile via constructor (array and generator)', () => { const makeProvider = () => ({ getFunctions: () => [new _ExpressionFunction.default('identity', x => `${x}`, (values, x) => x), new _ExpressionFunction.default('strtoupper', x => `${x}.toUpperCase()`, (values, x) => (x ?? '').toString().toUpperCase()), new _ExpressionFunction.default('strtolower', x => `${x}.toLowerCase()`, (values, x) => (x ?? '').toString().toLowerCase()), new _ExpressionFunction.default('fn_namespaced', () => 'true', () => true)] }); const provider = makeProvider(); const cases = [[[provider]], [function* () { yield provider; }()]]; for (const [providers] of cases) { const el = new _ExpressionLanguage.default(null, providers); expect(el.evaluate('identity("foo")')).toBe('foo'); expect(el.compile('identity("foo")')).toBe('"foo"'); expect(el.evaluate('strtoupper("foo")')).toBe('FOO'); expect(el.compile('strtoupper("foo")')).toBe('"foo".toUpperCase()'); expect(el.evaluate('strtolower("FOO")')).toBe('foo'); expect(el.compile('strtolower("FOO")')).toBe('"FOO".toLowerCase()'); expect(el.evaluate('fn_namespaced()')).toBe(true); expect(el.compile('fn_namespaced()')).toBe('true'); } }); function getRegisterCallbacks() { let provider = { getFunctions: () => { return [new _ExpressionFunction.default('fn', () => {}, () => {})]; } }; return [[expressionLanguage => { expressionLanguage.register('fn', () => {}, () => {}); }], [expressionLanguage => { expressionLanguage.addFunction(new _ExpressionFunction.default('fn', () => {}, () => {})); }], [expressionLanguage => { expressionLanguage.registerProvider(provider); }]]; } test('backslashes properly escaped and handled', () => { const el = new _ExpressionLanguage.default(); const res = el.evaluate('"a\\\\b" matches "/^a\\\\\\\\b$/"'); expect(res).toBe(true); const res2 = el.evaluate('"\\\\"'); expect(res2).toBe("\\"); }); test('ternary operator supported', () => { let el = new _ExpressionLanguage.default(); for (const [expr, variables, expectedResult, expectedExceptionMessage = null] of getTernary()) { if (expectedExceptionMessage) { try { const res = el.evaluate(expr, variables); console.log("This expression should have caused an error: " + expr, { res }); expect(true).toBe(false); } catch (err) { expect(err.message).toBe(expectedExceptionMessage); } } else { const res = el.evaluate(expr, variables); expect(res).toBe(expectedResult); } } }); function getTernary() { return [["a ? 'yes' : 'no'", { a: true }, 'yes'], ['a ? "yes" : "no"', { a: true }, 'yes'], ['a ? \'yes\' : \'no\'', { a: false }, 'no'], ['a ? \'yes\' : \'no\'', { a: null }, 'no'], ['a ? \'yes\' : \'no\'', {}, 'no', 'Variable "a" is not valid'], ['a ?: "short-hand"', { a: "find me" }, "find me"], ['a ?: "short-hand"', { a: false }, "short-hand"], ['a ? b', { a: false, b: 'yay' }, 'yay'], ['a ? b', { a: 'yay', b: false }, 'yay']]; } test('null safe compile', () => { let el = new _ExpressionLanguage.default(); for (let oneNullSafe of getNullSafe()) { let result = el.compile(oneNullSafe[0], ['foo']); const foo = oneNullSafe[1]; expect(eval(result)).toBeFalsy(); } }); test('null safe evaluate', () => { let el = new _ExpressionLanguage.default(); for (let oneNullSafe of getNullSafe()) { let result = el.evaluate(oneNullSafe[0], { foo: oneNullSafe[1] }); expect(result).toBeNull(); } }); test('null coalescing evaluate returns default', () => { const el = new _ExpressionLanguage.default(); for (const [expr, foo] of getNullCoalescing()) { expect(el.evaluate(expr, { foo })).toBe('default'); } }); test('null coalescing compile returns default', () => { const el = new _ExpressionLanguage.default(); for (const [expr, foo] of getNullCoalescing()) { const res = el.evaluate(expr, { foo }); expect(res).toBe("default"); } }); function getNullSafe() { let foo = { bar: () => { return null; } }; return [['foo?.bar', null], ['foo?.bar()', null], ['foo.bar?.baz', { bar: null }], ['foo.bar?.baz()', { bar: null }], ['foo["bar"]?.baz', { bar: null }], ['foo["bar"]?.baz()', { bar: null }], ['foo.bar()?.baz', foo], ['foo.bar()?.baz()', foo], ['foo?.bar.baz', null], ['foo?.bar["baz"]', null], ['foo?.bar["baz"]["qux"]', null], ['foo?.bar["baz"]["qux"].quux', null], ['foo?.bar["baz"]["qux"].quux()', null], ['foo?.bar().baz', null], ['foo?.bar()["baz"]', null], ['foo?.bar()["baz"]["qux"]', null], ['foo?.bar()["baz"]["qux"].quux', null], ['foo?.bar()["baz"]["qux"].quux()', null]]; } function getNullCoalescing() { const foo = { bar: () => null }; return [['bar ?? "default"', null], ['foo.bar ?? "default"', null], ['foo.bar.baz ?? "default"', { bar: null }], ['foo.bar ?? foo.baz ?? "default"', null], ['foo[0] ?? "default"', []], ['foo["bar"] ?? "default"', { bar: null }], ['foo["baz"] ?? "default"', { bar: null }], ['foo["bar"]["baz"] ?? "default"', { bar: null }], ['foo["bar"].baz ?? "default"', { bar: null }], ['foo.bar().baz ?? "default"', foo], ['foo.bar.baz.bam ?? "default"', { bar: null }], ['foo?.bar?.baz?.qux ?? "default"', { bar: null }], ['foo[123][456][789] ?? "default"', { 123: [] }]]; } test('evaluate', () => { let evaluateData = getEvaluateData(); for (let evaluateDatum of evaluateData) { let expressionLanguage = new _ExpressionLanguage.default(), provider = evaluateDatum[3], expression = evaluateDatum[0], values = evaluateDatum[1], expectedOutcome = evaluateDatum[2]; if (provider) { expressionLanguage.registerProvider(provider); } let result = expressionLanguage.evaluate(expression, values); if (expectedOutcome !== null && typeof expectedOutcome === "object") { expect(result).toMatchObject(expectedOutcome); } else { expect(result).toBe(expectedOutcome); } } }); function getEvaluateData() { return [[ // Expression '1.0', // Values {}, // Expected Outcome 1, // Provider null], [ // Expression '1 + 1', // Values {}, // Expected Outcome 2, // Provider null], [ // Expression '2 ** 3', // Values {}, // Expected Outcome 8, // Provider null], [ // Expression 'a > 0', // Values { a: 1 }, // Expected Outcome true, // Provider null], [ // Expression 'a >= 0', // Values { a: 1 }, // Expected Outcome true, // Provider null], [ // Expression 'a <= 0', // Values { a: 1 }, // Expected Outcome false, // Provider null], [ // Expression 'a != 0', // Values { a: 1 }, // Expected Outcome true, // Provider null], [ // Expression 'a == 1', // Values { a: 1 }, // Expected Outcome true, // Provider null], [ // Expression 'a === 1', // Values { a: 1 }, // Expected Outcome true, // Provider null], [ // Expression 'a !== 1', // Values { a: 1 }, // Expected Outcome false, // Provider null], ['foo.getFirst() + bar.getSecond()', { foo: { getFirst: () => { return 7; } }, bar: { getSecond: () => { return 100; } } }, 107, null], ['(foo.getFirst() + bar.getSecond()) / foo.second', { foo: { second: 4, getFirst: () => { return 7; } }, bar: { getSecond: () => { return 9; } } }, 4, null], ['foo.getFirst() + bar.getSecond() / foo.second', { foo: { second: 4, getFirst: () => { return 7; } }, bar: { getSecond: () => { return 8; } } }, 9, null], ['(foo.getFirst() + bar.getSecond() / foo.second) + bar.first[3]', { foo: { getFirst: () => { return 7; }, second: 4 }, bar: { first: [1, 2, 3, 4, 5], getSecond: () => { return 8; } } }, 13, null], ['b.myMethod(a[1])', { a: ["one", "two", "three"], b: { myProperty: "foo", myMethod: word => { return "bar " + word; } } }, "bar two", null], ['a[2] === "three" and b.myMethod(a[1]) === "bar two" and (b.myProperty == "foo" or b["myProperty"] == "foo") and b["property with spaces and &*()*%$##@% characters"] == "fun"', { a: ["one", "two", "three"], b: { myProperty: "foo", myMethod: word => { return "bar " + word; }, ["property with spaces and &*()*%$##@% characters"]: 'fun' } }, true, null], ['a and !b', { a: true, b: false }, true, null], ['a in b', { a: "Dogs", b: ["Cats", "Dogs"] }, true, null], ['a in outputs["typesOfAnimalsAllowed"]', { a: "Dogs", outputs: { typesOfAnimalsAllowed: ["Dogs", "Other"] } }, true, null], ['"Other" in inputs["typesOfAnimalsAllowed"]', { inputs: { typesOfAnimalsAllowed: ["Dogs", "Other"] } }, true], ['a not in b', { a: "Dogs", b: ["Cats", "Bags"] }, true, null]]; }