diff --git a/src/operators.ts b/src/operators.ts index eb29417..c5cc633 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -107,3 +107,66 @@ export const binaryOperators = { '**': (val1, val2): string => `${val1}**${val2}`, }; + +export const standardFunctions = { + sum: `function sum(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return arr.reduce((a, b) => a + b, 0); + }`, + max: `function max(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return Math.max(...arr); + }`, + min: `function min(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return Math.min(...arr); + }`, + avg: `function avg(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return sum(arr) / arr.length; + }`, + length: `function length(arr) { + if(!Array.isArray(arr) && typeof arr !== 'string') { + throw new Error('Expected an array or string'); + } + return arr.length; + }`, + stddev: `function stddev(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + const mu = avg(arr); + const diffSq = arr.map((el) => (el - mu) ** 2); + return Math.sqrt(avg(diffSq)); + }`, + first: `function first(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return arr[0]; + }`, + last: `function last(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return arr[arr.length - 1]; + }`, + index: `function index(arr, i) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + if (i < 0) { + return arr[arr.length + i]; + } + return arr[i]; + }`, + keys: `function keys(obj) { return Object.keys(obj); }`, +}; diff --git a/src/parser.ts b/src/parser.ts index 979ca3a..78d4609 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -366,6 +366,9 @@ export class JsonTemplateParser { this.lexer.matchTokenType(TokenType.STR) ) { prop = this.lexer.lex(); + if (prop.type === TokenType.KEYWORD) { + prop.type = TokenType.ID; + } } return { type: SyntaxType.SELECTOR, @@ -1346,10 +1349,6 @@ export class JsonTemplateParser { }; } - private static prependFunctionID(prefix: string, id?: string): string { - return id ? `${prefix}.${id}` : prefix; - } - private static ignoreEmptySelectors(parts: Expression[]): Expression[] { return parts.filter( (part) => !(part.type === SyntaxType.SELECTOR && part.selector === '.' && !part.prop), @@ -1384,13 +1383,11 @@ export class JsonTemplateParser { if (selectorExpr.selector === '.' && selectorExpr.prop?.type === TokenType.ID) { pathExpr.parts.pop(); newFnExpr.id = selectorExpr.prop.value; - newFnExpr.dot = true; } } if (!pathExpr.parts.length && pathExpr.root && typeof pathExpr.root !== 'object') { - newFnExpr.id = this.prependFunctionID(pathExpr.root, fnExpr.id); - newFnExpr.dot = false; + newFnExpr.parent = pathExpr.root; } else { newFnExpr.object = pathExpr; } diff --git a/src/translator.ts b/src/translator.ts index b074366..09a7bc9 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -7,7 +7,7 @@ import { VARS_PREFIX, } from './constants'; import { JsonTemplateTranslatorError } from './errors'; -import { binaryOperators } from './operators'; +import { binaryOperators, standardFunctions } from './operators'; import { ArrayExpression, AssignmentExpression, @@ -39,7 +39,6 @@ import { LoopExpression, IncrementExpression, LoopControlExpression, - Keyword, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; @@ -50,6 +49,8 @@ export class JsonTemplateTranslator { private unusedVars: string[] = []; + private standardFunctions: Record = {}; + private readonly expr: Expression; constructor(expr: Expression) { @@ -91,6 +92,10 @@ export class JsonTemplateTranslator { this.init(); const code: string[] = []; const exprCode = this.translateExpr(this.expr, dest, ctx); + const functions = Object.values(this.standardFunctions); + if (functions.length > 0) { + code.push(functions.join('').replaceAll(/\s+/g, ' ')); + } code.push(`let ${dest};`); code.push(this.vars.map((elm) => `let ${elm};`).join('')); code.push(exprCode); @@ -475,7 +480,13 @@ export class JsonTemplateTranslator { } private getFunctionName(expr: FunctionCallExpression, ctx: string): string { - return expr.dot ? `${ctx}.${expr.id}` : expr.id || ctx; + if (expr.object) { + return expr.id ? `${ctx}.${expr.id}` : ctx; + } + if (expr.parent) { + return expr.id ? `${expr.parent}.${expr.id}` : expr.parent; + } + return expr.id as string; } private translateFunctionCallExpr( @@ -491,7 +502,17 @@ export class JsonTemplateTranslator { code.push(`if(${JsonTemplateTranslator.returnIsNotEmpty(result)}){`); } const functionArgsStr = this.translateSpreadableExpressions(expr.args, result, code); - code.push(result, '=', this.getFunctionName(expr, result), '(', functionArgsStr, ');'); + const functionName = this.getFunctionName(expr, result); + if (expr.id && standardFunctions[expr.id]) { + this.standardFunctions[expr.id] = standardFunctions[expr.id]; + code.push(`if(${functionName} && typeof ${functionName} === 'function'){`); + code.push(result, '=', functionName, '(', functionArgsStr, ');'); + code.push('} else {'); + code.push(result, '=', expr.id, '(', expr.parent || result, ',', functionArgsStr, ');'); + code.push('}'); + } else { + code.push(result, '=', functionName, '(', functionArgsStr, ');'); + } if (expr.object) { code.push('}'); } diff --git a/src/types.ts b/src/types.ts index 5a0b694..2592f48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -234,7 +234,7 @@ export interface FunctionCallExpression extends Expression { args: Expression[]; object?: Expression; id?: string; - dot?: boolean; + parent?: string; } export interface ConditionalExpression extends Expression { @@ -272,6 +272,6 @@ export type FlatMappingPaths = { }; export type FlatMappingAST = { - input: PathExpression; + input: PathExpression | FunctionCallExpression; output: PathExpression; }; diff --git a/test/scenarios/functions/array_functions.jt b/test/scenarios/functions/array_functions.jt new file mode 100644 index 0000000..e2b276c --- /dev/null +++ b/test/scenarios/functions/array_functions.jt @@ -0,0 +1,4 @@ +{ + map: .map(lambda ?0 * 2), + filter: .filter(lambda ?0 % 2 == 0) +} \ No newline at end of file diff --git a/test/scenarios/functions/data.ts b/test/scenarios/functions/data.ts index 0aa3a1a..e582341 100644 --- a/test/scenarios/functions/data.ts +++ b/test/scenarios/functions/data.ts @@ -1,6 +1,14 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ + { + templatePath: 'array_functions.jt', + input: [1, 2, 3, 4], + output: { + map: [2, 4, 6, 8], + filter: [2, 4], + }, + }, { templatePath: 'function_calls.jt', output: ['abc', null, undefined], diff --git a/test/scenarios/standard_functions/data.ts b/test/scenarios/standard_functions/data.ts new file mode 100644 index 0000000..3064221 --- /dev/null +++ b/test/scenarios/standard_functions/data.ts @@ -0,0 +1,27 @@ +import { Scenario } from '../../types'; + +export const data: Scenario[] = [ + { + input: { + arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + obj: { + foo: 1, + bar: 2, + baz: 3, + quux: 4, + }, + }, + output: { + sum: 55, + sum2: 55, + avg: 5.5, + min: 1, + max: 10, + stddev: 2.8722813232690143, + length: 10, + first: 1, + last: 10, + keys: ['foo', 'bar', 'baz', 'quux'], + }, + }, +]; diff --git a/test/scenarios/standard_functions/template.jt b/test/scenarios/standard_functions/template.jt new file mode 100644 index 0000000..fd6ff03 --- /dev/null +++ b/test/scenarios/standard_functions/template.jt @@ -0,0 +1,14 @@ +const arr = .arr; +const obj = .obj; +{ + sum: .arr.sum(), + sum2: (arr.index(0) + arr.index(-1)) * arr.length() / 2, + avg: arr.avg(), + min: arr.min(), + max: arr.max(), + stddev: arr.stddev(), + length: arr.length(), + first: arr.first(), + last: arr.last(), + keys: obj.keys(), +} \ No newline at end of file