Skip to content

Commit

Permalink
feat: Plugable evaluators (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
valya committed Aug 31, 2023
1 parent 65e8bf6 commit 917a576
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@n8n/tournament",
"version": "1.0.0",
"version": "1.0.1",
"description": "Output compatible rewrite of riot tmpl",
"main": "dist/index.js",
"module": "src/index.ts",
Expand Down
9 changes: 9 additions & 0 deletions src/Evaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReturnValue, Tournament } from '.';

export interface ExpressionEvaluator {
evaluate(code: string, data: unknown): ReturnValue;
}

export interface ExpressionEvaluatorClass {
new (instance: Tournament): ExpressionEvaluator;
}
22 changes: 22 additions & 0 deletions src/FunctionEvaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ExpressionEvaluator, ReturnValue, Tournament } from '.';

export class FunctionEvaluator implements ExpressionEvaluator {
private _codeCache: Record<string, Function> = {};

constructor(private instance: Tournament) {}

private getFunction(expr: string): Function {
if (expr in this._codeCache) {
return this._codeCache[expr];
}
const [code] = this.instance.getExpressionCode(expr);
const func = new Function('E', code + ';');
this._codeCache[expr] = func;
return func;
}

evaluate(expr: string, data: unknown): ReturnValue {
const fn = this.getFunction(expr);
return fn.call(data, this.instance.errorHandler);
}
}
27 changes: 13 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { getExpressionCode } from './ExpressionBuilder';
import type { ExpressionAnalysis } from './ExpressionBuilder';
import { getTmplDifference } from './Analysis';
import type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';
import { FunctionEvaluator } from './FunctionEvaluator';
export type { TmplDifference } from './Analysis';
export type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';

const DATA_NODE_NAME = '___n8n_data';
export type ReturnValue = string | null | (() => unknown);

export class Tournament {
private _codeCache: Record<string, [Function, ExpressionAnalysis]> = {};
private evaluator!: ExpressionEvaluator;

constructor(
public errorHandler: (error: Error) => void = () => {},
private _dataNodeName: string = DATA_NODE_NAME,
) {}
Evaluator: ExpressionEvaluatorClass = FunctionEvaluator,
) {
this.setEvaluator(Evaluator);
}

setEvaluator(Evaluator: ExpressionEvaluatorClass) {
this.evaluator = new Evaluator(this);
}

getExpressionCode(expr: string): [string, ExpressionAnalysis] {
return getExpressionCode(expr, this._dataNodeName);
Expand All @@ -22,23 +32,12 @@ export class Tournament {
return getTmplDifference(expr, this._dataNodeName);
}

private getFunction(expr: string): [Function, ExpressionAnalysis] {
if (expr in this._codeCache) {
return this._codeCache[expr];
}
const [code, analysis] = this.getExpressionCode(expr);
const func = new Function('E', code + ';');
this._codeCache[expr] = [func, analysis];
return [func, analysis];
}

execute(expr: string, data: unknown): ReturnValue {
// This is to match tmpl. This will only really happen if
// an empty expression is passed in.
if (!expr) {
return expr;
}
const fn = this.getFunction(expr)[0];
return fn.call(data, this.errorHandler);
return this.evaluator.evaluate(expr, data);
}
}
2 changes: 1 addition & 1 deletion test/Es6Syntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('ES6 Syntax', () => {
let t: Tournament;

beforeAll(() => {
t = new Tournament(fnStub, '___n8n_data', fnStub);
t = new Tournament(fnStub, '___n8n_data');
});

test('arrow functions', () => {
Expand Down
6 changes: 6 additions & 0 deletions test/Expression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import * as tmpl from '@n8n_io/riot-tmpl';

import { Tournament } from '../src/index';
import { isDifferent } from '../src/Differ';
import { FunctionEvaluator } from '../src/FunctionEvaluator';
import { baseFixtures } from './ExpressionFixtures/base';
import { testExpressionsWithEvaluator } from './utils';

tmpl.brackets.set('{{ }}');
const evaluator = new Tournament(() => {});
Expand All @@ -21,4 +23,8 @@ describe('Expression', () => {
});
}
});

describe('Test all expression evaluation fixtures', () => {
testExpressionsWithEvaluator(FunctionEvaluator);
});
});
38 changes: 38 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ExpressionEvaluatorClass } from '../src/index';
import { Tournament } from '../src/index';
import type { ExpressionTestEvaluation } from './ExpressionFixtures/base';
import { baseFixtures } from './ExpressionFixtures/base';

export const testExpressionsWithEvaluator = (Evaluator: ExpressionEvaluatorClass) => {
const tourn = new Tournament(
(e) => {
console.error(e);
},
undefined,
Evaluator,
);
const builtins = {
String,
parseFloat,
parseInt,
};
for (const t of baseFixtures) {
if (!t.tests.some((test) => test.type === 'evaluation')) {
continue;
}
test(t.expression, () => {
for (const test of t.tests.filter(
(test_): test_ is ExpressionTestEvaluation => test_.type === 'evaluation',
)) {
expect(
tourn.execute(t.expression.slice(1), {
...builtins,
$runIndex: 0,
$json: test.input[0],
$item: (i: number) => ({ $json: test.input[i] }),
}),
).toStrictEqual(test.output);
}
});
}
};

0 comments on commit 917a576

Please sign in to comment.