Skip to content

Commit

Permalink
feat: add support for json path functions
Browse files Browse the repository at this point in the history
  • Loading branch information
koladilip committed May 26, 2024
1 parent 7789cae commit 17241c3
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 13 deletions.
63 changes: 63 additions & 0 deletions src/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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); }`,
};
11 changes: 4 additions & 7 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
}
Expand Down
29 changes: 25 additions & 4 deletions src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -39,7 +39,6 @@ import {
LoopExpression,
IncrementExpression,
LoopControlExpression,
Keyword,
} from './types';
import { convertToStatementsExpr, escapeStr } from './utils/common';

Expand All @@ -50,6 +49,8 @@ export class JsonTemplateTranslator {

private unusedVars: string[] = [];

private standardFunctions: Record<string, string> = {};

private readonly expr: Expression;

constructor(expr: Expression) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand All @@ -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('}');
}
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export interface FunctionCallExpression extends Expression {
args: Expression[];
object?: Expression;
id?: string;
dot?: boolean;
parent?: string;
}

export interface ConditionalExpression extends Expression {
Expand Down Expand Up @@ -272,6 +272,6 @@ export type FlatMappingPaths = {
};

export type FlatMappingAST = {
input: PathExpression;
input: PathExpression | FunctionCallExpression;
output: PathExpression;
};
4 changes: 4 additions & 0 deletions test/scenarios/functions/array_functions.jt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
map: .map(lambda ?0 * 2),
filter: .filter(lambda ?0 % 2 == 0)
}
8 changes: 8 additions & 0 deletions test/scenarios/functions/data.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand Down
27 changes: 27 additions & 0 deletions test/scenarios/standard_functions/data.ts
Original file line number Diff line number Diff line change
@@ -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'],
},
},
];
14 changes: 14 additions & 0 deletions test/scenarios/standard_functions/template.jt
Original file line number Diff line number Diff line change
@@ -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(),
}

0 comments on commit 17241c3

Please sign in to comment.