UNPKG

@gmsoft/spel2js

Version:

Parse Spring Expression Language in JavaScript

806 lines (652 loc) 20.5 kB
import { SpelExpressionEvaluator as evaluator } from '../../src/SpelExpressionEvaluator.js'; import { StandardContext } from '../../src/StandardContext'; describe('spel expression evaluator', () => { beforeEach(() => { // add spies }); describe('compile', () => { it('should compile an expression an return an evaluator', () => { //when let compiledExpression = evaluator.compile('1234'); //then expect(compiledExpression.eval).toBeDefined(); }); }); describe('parse', () => { describe('primitives', () => { it('should evaluate a number', () => { //when let numberInt = evaluator.eval('123'); let numberFloat = evaluator.eval('123.4'); //then expect(numberInt).toBe(123); expect(numberFloat).toBe(123.4); }); it('should evaluate a string', () => { //when let stringSingle = evaluator.eval("'hello world!'"); let stringDouble = evaluator.eval('"hello world!"'); //then expect(stringSingle).toBe('hello world!'); expect(stringDouble).toBe('hello world!'); }); it('should evaluate a string with embedded escaped single quotes', () => { //when let stringSingle = evaluator.eval("'hello ''world''!'"); let stringDouble = evaluator.eval("\"hello ''world''!\""); //then expect(stringSingle).toBe("hello 'world'!"); expect(stringDouble).toBe("hello 'world'!"); }); it('should evaluate a string with embedded escaped double quotes', () => { //when let stringSingle = evaluator.eval('\'hello ""world""!\''); let stringDouble = evaluator.eval('"hello ""world""!"'); //then expect(stringSingle).toBe('hello "world"!'); expect(stringDouble).toBe('hello "world"!'); }); it('should evaluate a boolean', () => { //when let boolTrue = evaluator.eval('true'); let boolFalse = evaluator.eval('false'); //then expect(boolTrue).toBe(true); expect(boolFalse).toBe(false); }); }); describe('lookups', () => { let context; beforeEach(() => { context = { iAmANumber: 1, iAmANestedPropertyName: 'propLookup', nested: { iAmAString: 'hi', reallyNested: { iAmTrue: true, hi: 'bye', }, propLookup: 'Found!', }, }; }); it('should look up a primitive in the context', () => { //when let number = evaluator.eval('iAmANumber', context); //then expect(number).toBe(1); }); it('should look up a nested primitive in the context using dot notation', () => { //when let string = evaluator.eval('nested.iAmAString', context); //then expect(string).toBe('hi'); }); it('should look up a doubly nested primitive in the context using dot notation', () => { //when let bool = evaluator.eval('nested.reallyNested.iAmTrue', context); //then expect(bool).toBe(true); }); it('should look up a nested primitive in the context using bracket notation literal', () => { //when let string = evaluator.eval('nested["iAmAString"]', context); //then expect(string).toBe('hi'); }); it('should look up a nested primitive in the context using bracket notation', () => { //when let string = evaluator.eval('nested[iAmANestedPropertyName]', context); //then expect(string).toBe('Found!'); }); it('should look up a really nested primitive in the context using bracket notation', () => { //when let string = evaluator.eval('nested.reallyNested[nested.iAmAString]', context); //then expect(string).toBe('bye'); }); it('should return null instead of throw error when using safe navigation', () => { //when let willThrow = () => { evaluator.eval('nested.doesNotExist'); }; let willBeNull = evaluator.eval('nested?.doesNotExist', context); let willAlsoBeNull = evaluator.eval( 'nested?.doesNotExist?.definitelyDoesNotExist', context ); //then expect(willThrow).toThrow(); expect(willBeNull).toBe(null); expect(willAlsoBeNull).toBe(null); }); }); describe('comparisons', () => { it('should evaluate an equality', () => { //when let comp1 = evaluator.eval('1 == 1'); let comp2 = evaluator.eval('1 == 2'); //then expect(comp1).toBe(true); expect(comp2).toBe(false); }); it('should evaluate an equality with lookups', () => { //given let context = { left: 1, right: 1, }; //when let comp = evaluator.eval('left == right', context); //then expect(comp).toBe(true); }); it('should evaluate an inequality (not equal)', () => { //when let comp1 = evaluator.eval('1 != 2'); let comp2 = evaluator.eval('1 != 1'); //then expect(comp1).toBe(true); expect(comp2).toBe(false); }); it('should evaluate an inequality (greater than)', () => { //when let comp1 = evaluator.eval('2 > 1'); let comp2 = evaluator.eval('1 > 1'); // let comp3 = evaluator.eval('-1 > 1'); //then expect(comp1).toBe(true); expect(comp2).toBe(false); // expect(comp3).toBe(false); }); it('should evaluate an inequality (greater than or equal to)', () => { //when let comp1 = evaluator.eval('1 >= 1'); let comp2 = evaluator.eval('2 >= 1'); let comp3 = evaluator.eval('1 >= 2'); //then expect(comp1).toBe(true); expect(comp2).toBe(true); expect(comp3).toBe(false); }); it('should evaluate an inequality (less than)', () => { //when let comp1 = evaluator.eval('1 < 2'); let comp2 = evaluator.eval('1 < 1'); //then expect(comp1).toBe(true); expect(comp2).toBe(false); }); it('should evaluate an inequality (less than or equal to)', () => { //when let comp1 = evaluator.eval('1 <= 2'); let comp2 = evaluator.eval('1 <= 2'); let comp3 = evaluator.eval('2 <= 1'); //then expect(comp1).toBe(true); expect(comp2).toBe(true); expect(comp3).toBe(false); }); it('should evaluate a complex inequality', () => { //when let comp = evaluator.eval('"abc".length <= "abcde".length'); //then expect(comp).toBe(true); }); }); describe('method invocation', () => { let context = { funky: () => { return 'fresh'; }, argumentative: arg => { return arg; }, name: 'ben', }; it('should look up and invoke a function', () => { //when let ret = evaluator.eval('funky()', context); //then expect(ret).toBe('fresh'); }); it('should look up and invoke a function with arguments', () => { //when let ret = evaluator.eval('argumentative("i disagree!")', context); //then expect(ret).toBe('i disagree!'); }); it('should use a property if getter not available', () => { //when let ret = evaluator.eval('getName()', context); //then expect(ret).toBe('ben'); }); it('should set a property if setter not available', () => { //given evaluator.eval('setName("steve")', context); //when let ret = evaluator.eval('getName()', context); //then expect(ret).toBe('steve'); }); }); describe('locals', () => { it('should refer to a local variable', () => { //given let context = { myString: 'global context', }, locals = { myString: 'hello world!', }; //when let local = evaluator.eval('#myString == "hello world!"', context, locals); //then expect(local).toBe(true); }); it('should refer to the root context', () => { //given let context = { myString: 'global context', }, locals = { myString: 'hello world!', }; //when let root = evaluator.eval('#root', context, locals); //then expect(root).toBe(context); }); it('should refer the "this" context', () => { //given let context = { myString: 'global context', }, locals = { myString: 'hello world!', }; //when let that = evaluator.eval('#this', context, locals); //then expect(that).toBe(context); }); it('should call a local function', () => { //given let context = {}; let locals = { foo(echo) { return echo; }, }; //when const result = evaluator.eval('#foo("123") == "123"', context, locals); //then expect(result).toEqual(true); }); }); describe('math', () => { it('should add 2 numbers', () => { //when let sum = evaluator.eval('1 + 1'); //then expect(sum).toBe(2); }); it('should add 3 numbers', () => { //when let sum = evaluator.eval('1 + 1 + 1'); //then expect(sum).toBe(3); }); it('should subtract 2 numbers', () => { //when let difference = evaluator.eval('1 + 1'); //then expect(difference).toBe(2); }); it('should multiply 2 numbers', () => { //when let product = evaluator.eval('1 + 1'); //then expect(product).toBe(2); }); it('should divide 2 numbers', () => { //when let quotient = evaluator.eval('1 + 1'); //then expect(quotient).toBe(2); }); it('should find the modulus of 2 numbers', () => { //when let mod = evaluator.eval('10 % 8'); //then expect(mod).toBe(2); }); it('should evaluate an exponent', () => { //when let mod = evaluator.eval('10^2'); //then expect(mod).toBe(100); }); it('should honor standard order of operations', () => { //when let math = evaluator.eval('8 + 4 * 6 - 2 * 3 / 2'); //8+(4*6)-(2*3/2) = 29 //then expect(math).toBe(29); }); }); describe('matches', () => { it('should return true if the left side matches the regexp string on the right side', () => { //when let matches = evaluator.eval('"the quick brown fox" matches "^the.*fox$"'); //then expect(matches).toBe(true); }); it('should return false if the left side does not match the regexp string on the right side', () => { //when let matches = evaluator.eval('"the quick brown dog" matches "^the.*fox$"'); //then expect(matches).toBe(false); }); it('should throw if the regexp is invalid', () => { //when let willthrow = () => evaluator.eval('"foo" matches "["'); //then expect(willthrow).toThrow(); }); }); describe('ternary', () => { it('should return first argument if true', () => { //when let tern = evaluator.eval('true ? "yes" : "no"'); //then expect(tern).toBe('yes'); }); it('should return second argument if false', () => { //when let tern = evaluator.eval('false ? "yes" : "no"'); //then expect(tern).toBe('no'); }); it('should return expression if truthy, or ifFalseExpression if null ', () => { //when let elvisTruthy = evaluator.eval('"Thank you." ?: "Thank you very much."'); let elvisFalsy = evaluator.eval('null ?: "Thank you very much."'); //then expect(elvisTruthy).toBe('Thank you.'); expect(elvisFalsy).toBe('Thank you very much.'); }); }); describe('assignment', () => { it('should assign a value to the proper context with the specified property name', () => { //given let context = { name: 'Nikola Tesla', heritage: 'Serbian', }; let locals = { newName: 'Mike Tesla', }; //when evaluator.eval('name = #newName', context, locals); //then expect(context.name).toBe('Mike Tesla'); }); it('should assign to a nested context', () => { //given let context = { nested: { name: 'Nikola Tesla', }, }; let locals = { newName: 'Mike Tesla', }; //when evaluator.eval('nested.name = #newName', context, locals); //then expect(context.nested.name).toBe('Mike Tesla'); }); }); describe('complex literals', () => { it('should create an array', () => { //when let arr = evaluator.eval('{1, 2, 3, 4}'); //then expect(arr).toEqual([1, 2, 3, 4]); }); it('should get the size of an array', () => { //when let size = evaluator.eval('{1, 2, 3, 4}.size()'); //then expect(size).toEqual(4); }); it('should check whether an array contains an element', () => { //given let context = { classification: 'PHONE', }; //when let shouldBeTrue = evaluator.eval( `{'PHONE','EMPLOYMENT_PHONE','WORK_PHONE'}.contains(classification)`, context ); //then expect(shouldBeTrue).toEqual(true); }); it('should create a map', () => { //when let map = evaluator.eval('{name:"Nikola",dob:"10-July-1856"}'); //then expect(map).toEqual({ name: 'Nikola', dob: '10-July-1856' }); }); }); describe('unary', () => { it('should increment an integer but return original value', () => { //given let parsed = evaluator.compile('123++'); //when let inc1 = parsed.eval(); let inc2 = parsed.eval(); //then expect(inc1).toBe(123); expect(inc2).toBe(124); }); it('should decrement an integer but return original value', () => { //given let parsed = evaluator.compile('123--'); //when let dec1 = parsed.eval(); let dec2 = parsed.eval(); //then expect(dec1).toBe(123); expect(dec2).toBe(122); }); it('should increment an integer and return new value', () => { //given let parsed = evaluator.compile('++123'); //when let inc1 = parsed.eval(); let inc2 = parsed.eval(); //then expect(inc1).toBe(124); expect(inc2).toBe(125); }); it('should decrement an integer and return new value', () => { //given let parsed = evaluator.compile('--123'); //when let dec1 = parsed.eval(); let dec2 = parsed.eval(); //then expect(dec1).toBe(122); expect(dec2).toBe(121); }); it('should increment a property on the context', () => { //given let context = { int: 123, }; //when evaluator.eval('int++', context); //then expect(context.int).toBe(124); }); it('should increment a local variable', () => { //given let context = { int: 123, }; let locals = { int: 321, }; //when evaluator.eval('#int++', context, locals); //then expect(locals.int).toBe(322); }); it('should invert a boolean', () => { //when let bool = evaluator.eval('!true'); //then expect(bool).toBe(false); }); }); describe('logical operators', () => { it('should evaluate "and" expressions', () => { //when let and1 = evaluator.eval('true && true'); let and2 = evaluator.eval('true && false'); let and3 = evaluator.eval('false && true'); let and4 = evaluator.eval('false && false'); //then expect(and1).toBe(true); expect(and2).toBe(false); expect(and3).toBe(false); expect(and4).toBe(false); }); it('should evaluate "and" expressions', () => { //when let or1 = evaluator.eval('true || true'); let or2 = evaluator.eval('true || false'); let or3 = evaluator.eval('false || true'); let or4 = evaluator.eval('false || false'); //then expect(or1).toBe(true); expect(or2).toBe(true); expect(or3).toBe(true); expect(or4).toBe(false); }); }); describe('selection/projection', () => { it('should return a new list based on selection expression', () => { //given let context = { collection: [1, 2, 3, 4, 5, 6], }; //when let newCollection = evaluator.eval('collection.?[#this <= 3]', context); //then expect(newCollection).toEqual([1, 2, 3]); }); it('should return a new map based on selection expression', () => { //given let context = { collection: { a: 1, b: 2, c: 3, d: 4, e: 5, }, }; //when let newCollection1 = evaluator.eval('collection.?[value <= 3]', context); let newCollection2 = evaluator.eval('collection.?[key == "a"]', context); //then expect(newCollection1).toEqual({ a: 1, b: 2, c: 3 }); expect(newCollection2).toEqual({ a: 1 }); }); it('should return the first element of list or map', () => { //given let context = { list: [1, 2, 3, 4, 5, 6], map: { a: 1, b: 2, c: 3, d: 4, e: 5, }, }; //when let listFirst = evaluator.eval('list.^[#this <= 3]', context); let mapFirst = evaluator.eval('map.^[value <= 3]', context); //then expect(listFirst).toEqual(1); expect(mapFirst).toEqual({ a: 1 }); }); it('should return the last element of list or map', () => { //given let context = { list: [1, 2, 3, 4, 5, 6], map: { a: 1, b: 2, c: 3, d: 4, e: 5, }, }; //when let listFirst = evaluator.eval('list.$[#this <= 3]', context); let mapFirst = evaluator.eval('map.$[value <= 3]', context); //then expect(listFirst).toEqual(3); expect(mapFirst).toEqual({ c: 3 }); }); it('should return a list of projected values from a list of objects', () => { //given let context = { list: [ { name: 'Ben', }, { name: 'Kris', }, { name: 'Ansy', }, ], }; //when let names = evaluator.eval('list.![name]', context); //then expect(names).toEqual(['Ben', 'Kris', 'Ansy']); }); it('should return a list of entries from a map (not quite like in Java because key must be a string)', () => { //given let context = { map: { ben: { hometown: 'Newton', }, kris: { hometown: 'Peabody', }, ansy: { hometown: 'Brockton', }, }, }; //when let hometowns = evaluator.eval('map.![hometown]', context); //then expect(hometowns).toEqual(['Newton', 'Peabody', 'Brockton']); }); }); }); });