@akala/core
Version:
253 lines (215 loc) • 10.1 kB
text/typescript
import { Parser, ParsedString, StringCursor, ParsedNumber, ParsedBoolean } from '../parser/parser.js';
import { BinaryOperator } from '../parser/expressions/binary-operator.js';
import { type Formatter, formatters } from '../formatters/index.js';
import { EvaluatorAsFunction } from '../parser/evaluator-as-function.js';
import { ObservableArray } from '../observables/array.js';
import { it, describe } from 'node:test'
import assert from 'assert/strict'
import { delay } from '../promiseHelpers.js';
import { BinaryExpression } from '../parser/expressions/binary-expression.js';
import type { Expressions } from '../parser/expressions/index.js';
//b*(c+d) ==> (b*c)+d
const parser = new Parser();
const evaluator = new EvaluatorAsFunction();
describe('parser tests', () =>
{
[['b*c+d', 10] as const, ['b*(c+d)', 14] as const, ['b*(c+d)+1', 15] as const, ['(b*c+d)+1', 11] as const].forEach(([operation, expectedResult]) =>
it('should do math ' + operation, () =>
{
const cursor = new StringCursor(operation);
const result = parser.parseAny(cursor, false);
console.log(result.toString());
assert.strictEqual(operation.length, cursor.offset);
assert.strictEqual((evaluator.eval(result))({ b: 2, c: 3, d: 4 }), expectedResult);
})
);
it('should apply precedence', () =>
{
const test = new BinaryExpression<Expressions>(new ParsedString('b'), BinaryOperator.Times, new BinaryExpression(new ParsedString('c'), BinaryOperator.Plus, new ParsedString('d')));
BinaryExpression.applyPrecedence(test);
assert.strictEqual(test.toString(), '( b * ( c + d ) )');
})
it('should parse complex expressions', () =>
{
assert.strictEqual((evaluator.eval(parser.parse("template || '/' + deviceType + '/new.html'")))({ template: '/devices/virtualstate.html' }), '/devices/virtualstate.html');
assert.strictEqual((evaluator.eval(parser.parse("template || '/' + deviceType + '/new.html'")))({ deviceType: 'pioneer' }), '/pioneer/new.html');
})
class DummyHttpFormatter implements Formatter<string>
{
public format(value: unknown)
{
return `$http on '${value}'`;
}
}
formatters.register('http', DummyHttpFormatter, true);
it('should return number', () =>
{
assert.strictEqual((evaluator.eval(parser.parse("2000")))(), 2000);
});
it('should format', () =>
{
const formatter = new DummyHttpFormatter();
assert.strictEqual((evaluator.eval(parser.parse("'/my/url' # http")))(), formatter.format('/my/url'));
assert.strictEqual((evaluator.eval(parser.parse("'2018-03-12' # toDate")))({}).valueOf(), new Date(Date.UTC(2018, 2, 12, 0, 0, 0, 0)).valueOf());
assert.strictEqual((evaluator.eval(parser.parse("'12/03/18' #toDate:'dd/MM/yy'")))({}).valueOf(), new Date(Date.UTC(2018, 2, 12)).valueOf());
assert.strictEqual((evaluator.eval(parser.parse("'03/12/18' #toDate:'MM/dd/yy'")))({}).valueOf(), new Date(Date.UTC(2018, 2, 12)).valueOf());
assert.strictEqual((evaluator.eval(parser.parse("'3/12/18' #toDate:'MM/dd/yy'")))({}).valueOf(), new Date(Date.UTC(2018, 2, 12)).valueOf());
assert.strictEqual((evaluator.eval(parser.parse("'2018-03-12' #toDate #date:'dd'")))({}), '12');
assert.deepStrictEqual((evaluator.eval(parser.parse("{options:{in:'/api/@domojs/zigate/pending' # http, text:internalName, value:address}}"))({})), { options: { in: formatter.format('/api/@domojs/zigate/pending'), text: undefined, value: undefined } });
assert.deepStrictEqual((evaluator.eval(parser.parse("{options:{in:'/api/@domojs/zigate/pending' # http, text:internalName, value:address}}")))({})['options'], { in: formatter.format('/api/@domojs/zigate/pending'), text: undefined, value: undefined });
})
it('evals', async () =>
{
console.log(evaluator.eval(parser.parse('columns[0].title'))({ columns: [{ title: 'pwet' }] }))
const args = {
controller: {
async fakeServer(sort, page)
{
const result = await Promise.resolve(['x', 'y', 'z']);
return result.map(x => x + sort + page)
}
}, table: { sort: 'a', page: 1 }
};
await delay(10)
const array = evaluator.eval<ObservableArray<string>>(parser.parse('controller.fakeServer(table.sort, table.page)#asyncArray'))(args);
array.addListener(ev => console.log(array.array));
})
describe('StringCursor', () =>
{
it('advances offset and freezes', () =>
{
const cursor = new StringCursor('abc');
assert.strictEqual(cursor.char, 'a');
cursor.offset = 1;
assert.strictEqual(cursor.char, 'b');
const frozen = cursor.freeze();
assert.strictEqual(frozen.offset, 1);
});
it('throws when offset beyond string', () =>
{
const cursor = new StringCursor('a');
assert.throws(() => { cursor.offset = 5 }, e => (e as Error).message === 'Cursor cannot go beyond the string limit');
});
it('exec matches and advances offset', () =>
{
const cursor = new StringCursor('123abc');
const match = cursor.exec(/\d+/);
assert.strictEqual(match?.[0], '123');
assert.strictEqual(cursor.offset, 3);
});
it('exec returns null if not at offset', () =>
{
const cursor = new StringCursor('abc');
cursor.offset = 1;
const result = cursor.exec(/abc/);
assert.strictEqual(result, null);
});
});
describe('Parser basics', () =>
{
const parser = new Parser();
it('parses numbers', () =>
{
const expr = parser.parse('123');
assert.ok(expr instanceof ParsedNumber);
});
it('throws on invalid number', () =>
{
assert.throws(() => parser.parse('.x'));
});
it('parses booleans', () =>
{
assert.ok(parser.parse('true') instanceof ParsedBoolean);
assert.ok(parser.parse('false') instanceof ParsedBoolean);
});
it('parses strings', () =>
{
assert.ok(parser.parse('"hello"') instanceof ParsedString);
assert.ok(parser.parse("'hi'") instanceof ParsedString);
});
it('throws on invalid string', () =>
{
assert.throws(() => parser.parse('"unclosed'));
});
it('parses arrays', () =>
{
const expr = parser.parse('[1,2]');
assert.ok(expr.type);
});
it('parses objects', () =>
{
const expr = parser.parse('{ "a": 1, b: 2 }');
assert.ok(expr.type);
});
it('parses ternary', () =>
{
const expr = parser.parse('true ? 1 : 2');
assert.ok(expr.type);
});
it('throws on invalid ternary', () =>
{
assert.throws(() => parser.parse('true ? 1'));
});
it('parses function calls', () =>
{
assert.strictEqual(3, evaluator.eval(parser.parse('foo(1,2)'))({ foo(a: number, b: number) { return a + b; } }));
assert.strictEqual(undefined, evaluator.eval(parser.parse('foo?.(1,2)'))({}));
});
it('parses member access', () =>
{
assert.ok(parser.parse('foo.bar'));
assert.ok(parser.parse('foo?.bar'));
});
it('parses formatter expression', () =>
{
// fake formatter
const expr = parser.parse('1 #identity');
assert.ok(expr.type);
});
it('throws on invalid operator', () =>
{
assert.throws(() => parser.parse('1 @@ 2'))
});
});
describe('Parser.operate', () =>
{
it('evaluates binary operators correctly', () =>
{
assert.strictEqual(Parser.operate(BinaryOperator.Plus, 1, 2), 3);
assert.strictEqual(Parser.operate(BinaryOperator.Minus, 3, 1), 2);
assert.strictEqual(Parser.operate(BinaryOperator.Div, 6, 2), 3);
assert.strictEqual(Parser.operate(BinaryOperator.Times, 2, 3), 6);
assert.strictEqual(Parser.operate(BinaryOperator.Equal, 1, '1'), true);
assert.strictEqual(Parser.operate(BinaryOperator.StrictEqual, 1, 1), true);
assert.strictEqual(Parser.operate(BinaryOperator.LessThan, 1, 2), true);
assert.strictEqual(Parser.operate(BinaryOperator.LessThanOrEqual, 2, 2), true);
assert.strictEqual(Parser.operate(BinaryOperator.GreaterThan, 2, 1), true);
assert.strictEqual(Parser.operate(BinaryOperator.GreaterThanOrEqual, 2, 2), true);
assert.strictEqual(Parser.operate(BinaryOperator.NotEqual, 1, 2), true);
assert.strictEqual(Parser.operate(BinaryOperator.StrictNotEqual, 1, '1'), true);
assert.strictEqual(Parser.operate(BinaryOperator.Or, 0, 5), 5);
assert.strictEqual(Parser.operate(BinaryOperator.And, 1, 5), 5);
assert.strictEqual(Parser.operate(BinaryOperator.Dot, { foo: 42 }, 'foo'), 42);
assert.strictEqual(Parser.operate(BinaryOperator.Dot, 2, (x: number) => x + 1), 3);
});
it('throws on invalid operator', () =>
{
assert.throws(() => Parser.operate('invalid' as any, 1, 2));
});
});
describe('parseCSV', () =>
{
it('parses empty array/object correctly', () =>
{
const cursor = new StringCursor('[]');
const parser = new Parser();
parser.parseArray(cursor, true);
});
it('throws when missing comma or end', () =>
{
const cursor = new StringCursor('[1 2]');
const parser = new Parser();
assert.throws(() => parser.parseArray(cursor, true));
});
});
})