expression-language
Version:
Javascript implementation of symfony/expression-language
432 lines (428 loc) • 14.3 kB
JavaScript
"use strict";
var _ConstantNode = _interopRequireDefault(require("../Node/ConstantNode"));
var _Node = _interopRequireDefault(require("../Node/Node"));
var _GetAttrNode = _interopRequireDefault(require("../Node/GetAttrNode"));
var _ParsedExpression = _interopRequireDefault(require("../ParsedExpression"));
var _ExpressionLanguage = _interopRequireDefault(require("../ExpressionLanguage"));
var _index = require("../index");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
test('serialize ParsedExpression', () => {
let expression = new _ParsedExpression.default('25', new _ConstantNode.default(25));
let serialized = JSON.stringify(expression);
let unserialized = _ParsedExpression.default.fromJSON(serialized);
expect(unserialized.expression).toEqual(expression.expression);
expect(unserialized.nodes.name).toEqual(expression.nodes.name);
expect(unserialized.nodes.attributes).toMatchObject(expression.nodes.attributes);
});
test('serialize more complex ParsedExpression', () => {
let expressionString = "25 + 30";
let parser = new _index.Parser();
let nodes = parser.parse((0, _index.tokenize)(expressionString));
let expression = new _ParsedExpression.default(expressionString, nodes);
let serialized = JSON.stringify(expression);
let unserialized = _ParsedExpression.default.fromJSON(serialized);
expect(unserialized.expression).toEqual(expressionString);
expect(unserialized.nodes.name).toEqual(nodes.name);
expect(unserialized.nodes.attributes).toMatchObject(nodes.attributes);
});
/**
* Round-trips `expressionString` through parse -> JSON.stringify -> fromJSON, then asserts
* that the deserialized tree evaluates and compiles identically to the original. This avoids
* brittle deep-equality on Node instances, whose class-field methods are separate closures
* per instance and would never be reference-equal across the original/restored trees.
*/
function expectRoundTrips(el, expressionString, values = {}) {
let names = Object.keys(values);
let parsed = el.parse(expressionString, names);
let originalResult = el.evaluate(parsed, values);
let originalCompiled = el.compile(parsed, names);
let restored = _ParsedExpression.default.fromJSON(JSON.stringify(parsed));
expect(restored).toBeInstanceOf(_ParsedExpression.default);
expect(restored.expression).toBe(expressionString);
let restoredResult = el.evaluate(restored, values);
let restoredCompiled = el.compile(restored, names);
expect(restoredResult).toEqual(originalResult);
expect(restoredCompiled).toEqual(originalCompiled);
return {
parsed,
restored
};
}
test('fromJSON round-trips a NameNode', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo', {
foo: 42
});
});
test('fromJSON round-trips ConstantNode string/number/boolean/null values', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, '"hello"');
expectRoundTrips(el, '42');
expectRoundTrips(el, 'true');
expectRoundTrips(el, 'false');
expectRoundTrips(el, 'null');
});
test('fromJSON round-trips a UnaryNode', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, '-foo', {
foo: 5
});
expectRoundTrips(el, '!foo', {
foo: true
});
});
test('fromJSON round-trips a BinaryNode', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo + bar', {
foo: 10,
bar: 5
});
});
test('fromJSON round-trips a ConditionalNode', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo ? bar : baz', {
foo: true,
bar: 1,
baz: 2
});
expectRoundTrips(el, 'foo ? bar : baz', {
foo: false,
bar: 1,
baz: 2
});
});
test('fromJSON round-trips a NullCoalesceNode', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo ?? bar', {
foo: null,
bar: 'default'
});
expectRoundTrips(el, 'foo ?? bar', {
foo: 'set',
bar: 'default'
});
});
test('fromJSON round-trips a NullCoalescedNameNode for an undeclared variable', () => {
let parser = new _index.Parser();
let expressionString = 'undeclared ?? "fallback"';
let nodes = parser.parse((0, _index.tokenize)(expressionString), []);
let parsed = new _ParsedExpression.default(expressionString, nodes);
let restored = _ParsedExpression.default.fromJSON(JSON.stringify(parsed));
expect(restored.getNodes().nodes.expr1.name).toBe('NullCoalescedNameNode');
expect(restored.getNodes().evaluate({}, {})).toBe('fallback');
});
test('fromJSON round-trips a FunctionNode with multiple ArgumentsNode arguments (regression: fnArguments key)', () => {
let el = new _ExpressionLanguage.default();
let {
restored
} = expectRoundTrips(el, 'min(foo, bar, 3)', {
foo: 10,
bar: 5
});
// Directly assert the restored FunctionNode's argument list actually reconstructed
// (this is the exact shape that was broken by the n.nodes.arguments/fnArguments key mismatch).
let fnNode = restored.getNodes();
expect(fnNode.name).toBe('FunctionNode');
expect(fnNode.nodes.fnArguments).toBeDefined();
expect(fnNode.nodes.fnArguments.nodes.length).toBe(3);
});
test('fromJSON round-trips an ArrayNode (array literal)', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, '[1, 2, foo]', {
foo: 3
});
});
test('fromJSON round-trips an ArrayNode (hash/object literal)', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, '{x: 1, y: foo}', {
foo: 2
});
});
test('fromJSON round-trips a GetAttrNode property call', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo.bar', {
foo: {
bar: 'baz'
}
});
});
test('fromJSON round-trips a GetAttrNode method call with no arguments', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo.bar()', {
foo: {
bar: () => 'called'
}
});
});
test('fromJSON round-trips a GetAttrNode method call with ArgumentsNode arguments', () => {
let el = new _ExpressionLanguage.default();
let {
restored
} = expectRoundTrips(el, 'foo.bar(1, 2)', {
foo: {
bar: (a, b) => a + b
}
});
let getAttr = restored.getNodes();
expect(getAttr.nodes.fnArguments.name).toBe('ArgumentsNode');
// ArgumentsNode stores interleaved key/value pairs, so 2 arguments -> 4 entries.
expect(Object.keys(getAttr.nodes.fnArguments.nodes)).toHaveLength(4);
});
test('fromJSON round-trips a GetAttrNode array call', () => {
let el = new _ExpressionLanguage.default();
expectRoundTrips(el, 'foo[1]', {
foo: [10, 20, 30]
});
});
test('fromJSON restores the is_null_coalesce flag on a nested GetAttrNode', () => {
let el = new _ExpressionLanguage.default();
let values = {
a: {}
};
let parsed = el.parse('a.foo ?? "default"', Object.keys(values));
// Evaluating mutates the nested GetAttrNode's is_null_coalesce attribute in place.
let originalResult = el.evaluate(parsed, values);
expect(originalResult).toBe('default');
let originalGetAttr = parsed.getNodes().nodes.expr1;
expect(originalGetAttr.attributes.is_null_coalesce).toBe(true);
let restored = _ParsedExpression.default.fromJSON(JSON.stringify(parsed));
let restoredGetAttr = restored.getNodes().nodes.expr1;
expect(restoredGetAttr).toBeInstanceOf(_GetAttrNode.default);
expect(restoredGetAttr.attributes.is_null_coalesce).toBe(true);
expect(restoredGetAttr.evaluate(el.functions, values)).toBeNull();
});
test('fromJSON restores the is_short_circuited flag and preserves short-circuit chaining', () => {
let el = new _ExpressionLanguage.default();
let values = {
a: null
};
let parsed = el.parse('a?.b.c', Object.keys(values));
let originalResult = el.evaluate(parsed, values);
expect(originalResult).toBeNull();
let outerOriginal = parsed.getNodes();
let innerOriginal = outerOriginal.nodes.node;
expect(innerOriginal.attributes.is_short_circuited).toBe(true);
let restored = _ParsedExpression.default.fromJSON(JSON.stringify(parsed));
let outerRestored = restored.getNodes();
let innerRestored = outerRestored.nodes.node;
expect(innerRestored).toBeInstanceOf(_GetAttrNode.default);
expect(innerRestored.attributes.is_short_circuited).toBe(true);
expect(el.evaluate(restored, values)).toBeNull();
});
test('fromJSON accepts a plain object in addition to a JSON string', () => {
let expression = new _ParsedExpression.default('25', new _ConstantNode.default(25));
let plainObject = JSON.parse(JSON.stringify(expression));
let restored = _ParsedExpression.default.fromJSON(plainObject);
expect(restored.expression).toBe('25');
expect(restored.nodes.attributes.value).toBe(25);
});
test('fromJSON preserves a null or undefined nodes container', () => {
expect(_ParsedExpression.default.fromJSON({
expression: 'x',
nodes: null
}).getNodes()).toBeNull();
expect(_ParsedExpression.default.fromJSON({
expression: 'x'
}).getNodes()).toBeUndefined();
});
test('fromJSON reconstructs a raw array of top-level nodes', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: [{
name: 'ConstantNode',
attributes: {
value: 1
}
}, {
name: 'ConstantNode',
attributes: {
value: 2
}
}]
});
let nodes = restored.getNodes();
expect(Array.isArray(nodes)).toBe(true);
expect(nodes).toHaveLength(2);
expect(nodes[0]).toBeInstanceOf(_ConstantNode.default);
expect(nodes[0].attributes.value).toBe(1);
expect(nodes[1].attributes.value).toBe(2);
});
test('fromJSON reconstructs an object map of nodes without an outer name', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
left: {
name: 'ConstantNode',
attributes: {
value: 1
}
},
right: {
name: 'ConstantNode',
attributes: {
value: 2
}
}
}
});
let nodes = restored.getNodes();
expect(nodes.left).toBeInstanceOf(_ConstantNode.default);
expect(nodes.left.attributes.value).toBe(1);
expect(nodes.right.attributes.value).toBe(2);
});
test('fromJSON reconstructs a generic Node container (e.g. a raw argument list)', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'Node',
nodes: [{
name: 'ConstantNode',
attributes: {
value: 1
}
}, {
name: 'ConstantNode',
attributes: {
value: 2
}
}],
attributes: {}
}
});
let container = restored.getNodes();
expect(container).toBeInstanceOf(_Node.default);
expect(container.name).toBe('Node');
expect(Array.isArray(container.nodes)).toBe(true);
expect(container.nodes).toHaveLength(2);
});
test('fromJSON falls back to a generic Node for an unrecognized node name', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'MysteryNode',
attributes: {
foo: 'bar'
},
nodes: {
0: {
name: 'ConstantNode',
attributes: {
value: 1
}
}
}
}
});
let node = restored.getNodes();
expect(node).toBeInstanceOf(_Node.default);
expect(node.name).toBe('MysteryNode');
expect(node.attributes).toEqual({
foo: 'bar'
});
expect(node.nodes['0']).toBeInstanceOf(_ConstantNode.default);
});
test('fromJSON passes through values that are not node-shaped objects', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'ConstantNode',
attributes: {
value: {
plain: 'object without a name'
}
}
}
});
expect(restored.getNodes().attributes.value).toEqual({
plain: 'object without a name'
});
});
test('fromJSON falls back to a generic Node for an unrecognized node name with array-style children', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'MysteryNode',
attributes: {},
nodes: [{
name: 'ConstantNode',
attributes: {
value: 1
}
}, {
name: 'ConstantNode',
attributes: {
value: 2
}
}]
}
});
let node = restored.getNodes();
expect(Array.isArray(node.nodes)).toBe(true);
expect(node.nodes).toHaveLength(2);
});
test('fromJSON reconstructs a generic Node container with object-style (non-array) children', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'Node',
attributes: {},
nodes: {
left: {
name: 'ConstantNode',
attributes: {
value: 1
}
}
}
}
});
let node = restored.getNodes();
expect(node.nodes.left).toBeInstanceOf(_ConstantNode.default);
expect(node.nodes.left.attributes.value).toBe(1);
});
test('buildNode passes through a value that is already a Node instance', () => {
let already = new _ConstantNode.default(99);
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'UnaryNode',
attributes: {
operator: '-'
},
nodes: {
node: already
}
}
});
expect(restored.getNodes().nodes.node).toBe(already);
});
test('buildNode passes through null/primitive entries found within a node map', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: {
name: 'ArgumentsNode',
type: 'Array',
nodes: {
'0': null,
'1': 42,
'2': {
name: 'ConstantNode',
attributes: {
value: 1
}
}
}
}
});
let argsNode = restored.getNodes();
expect(argsNode.nodes['0']).toBeNull();
expect(argsNode.nodes['1']).toBe(42);
expect(argsNode.nodes['2']).toBeInstanceOf(_ConstantNode.default);
});
test('fromJSON preserves a primitive (non-node, non-array, non-object) nodes container', () => {
let restored = _ParsedExpression.default.fromJSON({
expression: 'x',
nodes: 'not-a-node'
});
expect(restored.getNodes()).toBe('not-a-node');
});