From 1f9c8c808cc4651dae00fbc1bcabf2b1fa4e9965 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Wed, 27 Sep 2023 19:56:35 +0530 Subject: [PATCH] feat(INT-723): add support for loops --- src/lexer.ts | 37 ++- src/operators.ts | 4 - src/parser.ts | 211 ++++++++++++++++-- src/translator.ts | 105 ++++++--- src/types.ts | 36 ++- test/scenario.test.ts | 2 +- test/scenarios/arrays/template.jt | 2 +- test/scenarios/assignments/data.ts | 2 +- test/scenarios/assignments/template.jt | 12 +- test/scenarios/comparisons/data.ts | 3 - test/scenarios/comparisons/template.jt | 3 - test/scenarios/conditions/data.ts | 53 ++++- test/scenarios/conditions/empty_if.jt | 1 + test/scenarios/conditions/empty_then.jt | 1 + test/scenarios/conditions/if_block.jt | 10 + .../conditions/{if-then.jt => if_then.jt} | 0 ...ined-arr-cond.jt => undefined_arr_cond.jt} | 0 test/scenarios/context_variables/filter.jt | 2 +- test/scenarios/filters/array_filters.jt | 2 +- test/scenarios/functions/data.ts | 2 +- test/scenarios/increment_statements/data.ts | 28 +++ .../postfix_decrement_on_literal.jt | 1 + .../postfix_decrement_on_non_id.jt | 1 + .../postfix_increment_on_literal.jt | 1 + .../postfix_increment_on_non_id.jt | 1 + .../prefix_decrement_on_literal.jt | 1 + .../prefix_increment_on_literal.jt | 1 + .../loops/break_without_condition.jt | 1 + test/scenarios/loops/break_without_loop.jt | 2 + test/scenarios/loops/complex_loop.jt | 5 + test/scenarios/loops/continue.jt | 7 + .../loops/continue_without_condition.jt | 1 + test/scenarios/loops/continue_without_loop.jt | 2 + test/scenarios/loops/data.ts | 81 +++++++ test/scenarios/loops/empty_loop.jt | 1 + test/scenarios/loops/just_for.jt | 8 + test/scenarios/loops/no_init.jt | 6 + test/scenarios/loops/no_test.jt | 6 + test/scenarios/loops/no_update.jt | 6 + test/scenarios/loops/statement_after_break.jt | 4 + .../loops/statement_after_continue.jt | 4 + test/scenarios/loops/template.jt | 5 + test/scenarios/paths/data.ts | 3 + test/scenarios/return/data.ts | 10 + .../return/return_without_condition.jt | 1 + .../return/statement_after_return.jt | 4 + test/scenarios/return/template.jt | 4 +- test/scenarios/selectors/template.jt | 2 +- test/scenarios/throw/data.ts | 10 + test/scenarios/throw/statement_after_throw.jt | 4 + .../throw/throw_without_condition.jt | 1 + test/utils/scenario.ts | 4 +- 52 files changed, 615 insertions(+), 89 deletions(-) create mode 100644 test/scenarios/conditions/empty_if.jt create mode 100644 test/scenarios/conditions/empty_then.jt create mode 100644 test/scenarios/conditions/if_block.jt rename test/scenarios/conditions/{if-then.jt => if_then.jt} (100%) rename test/scenarios/conditions/{undefined-arr-cond.jt => undefined_arr_cond.jt} (100%) create mode 100644 test/scenarios/increment_statements/data.ts create mode 100644 test/scenarios/increment_statements/postfix_decrement_on_literal.jt create mode 100644 test/scenarios/increment_statements/postfix_decrement_on_non_id.jt create mode 100644 test/scenarios/increment_statements/postfix_increment_on_literal.jt create mode 100644 test/scenarios/increment_statements/postfix_increment_on_non_id.jt create mode 100644 test/scenarios/increment_statements/prefix_decrement_on_literal.jt create mode 100644 test/scenarios/increment_statements/prefix_increment_on_literal.jt create mode 100644 test/scenarios/loops/break_without_condition.jt create mode 100644 test/scenarios/loops/break_without_loop.jt create mode 100644 test/scenarios/loops/complex_loop.jt create mode 100644 test/scenarios/loops/continue.jt create mode 100644 test/scenarios/loops/continue_without_condition.jt create mode 100644 test/scenarios/loops/continue_without_loop.jt create mode 100644 test/scenarios/loops/data.ts create mode 100644 test/scenarios/loops/empty_loop.jt create mode 100644 test/scenarios/loops/just_for.jt create mode 100644 test/scenarios/loops/no_init.jt create mode 100644 test/scenarios/loops/no_test.jt create mode 100644 test/scenarios/loops/no_update.jt create mode 100644 test/scenarios/loops/statement_after_break.jt create mode 100644 test/scenarios/loops/statement_after_continue.jt create mode 100644 test/scenarios/loops/template.jt create mode 100644 test/scenarios/return/return_without_condition.jt create mode 100644 test/scenarios/return/statement_after_return.jt create mode 100644 test/scenarios/throw/statement_after_throw.jt create mode 100644 test/scenarios/throw/throw_without_condition.jt diff --git a/src/lexer.ts b/src/lexer.ts index d50139e..e8a6881 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -41,6 +41,16 @@ export class JsonTemplateLexer { return token.type === TokenType.PUNCT && token.value === value; } + matchAssignment(): boolean { + return ( + this.match('=') || + this.match('+=') || + this.match('-=') || + this.match('*=') || + this.match('/=') + ); + } + matchLiteral(): boolean { return JsonTemplateLexer.isLiteralToken(this.lookahead()); } @@ -77,6 +87,14 @@ export class JsonTemplateLexer { return this.match('...'); } + matchIncrement(): boolean { + return this.match('++'); + } + + matchDecrement(): boolean { + return this.match('--'); + } + matchPathPartSelector(): boolean { let token = this.lookahead(); if (token.type === TokenType.PUNCT) { @@ -488,7 +506,7 @@ export class JsonTemplateLexer { range: [start, this.idx], }; } - } else if ('=!^$*><'.indexOf(ch1) >= 0) { + } else if ('=!^$><'.indexOf(ch1) >= 0) { this.idx += 2; return { type: TokenType.PUNCT, @@ -581,14 +599,29 @@ export class JsonTemplateLexer { } } + private scanPunctuatorForArithmeticAssignment(): Token | undefined { + let start = this.idx, + ch1 = this.codeChars[this.idx], + ch2 = this.codeChars[this.idx + 1]; + if ('+-/*'.includes(ch1) && ch2 === '=') { + this.idx += 2; + return { + type: TokenType.PUNCT, + value: ch1 + ch2, + range: [start, this.idx], + }; + } + } + private scanPunctuator(): Token | undefined { return ( this.scanPunctuatorForDots() || this.scanPunctuatorForQuestionMarks() || + this.scanPunctuatorForArithmeticAssignment() || this.scanPunctuatorForEquality() || this.scanPunctuatorForPaths() || this.scanPunctuatorForRepeatedTokens('?', 3) || - this.scanPunctuatorForRepeatedTokens('|&*.=>?<', 2) || + this.scanPunctuatorForRepeatedTokens('|&*.=>?<+-', 2) || this.scanSingleCharPunctuators() ); } diff --git a/src/operators.ts b/src/operators.ts index 375bae5..f216ff9 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -97,8 +97,6 @@ export const binaryOperators = { return endsWith(val2, val1); }, - '*==': containsStrict, - '==*': function (val1, val2): string { return containsStrict(val2, val1); }, @@ -107,8 +105,6 @@ export const binaryOperators = { return contains(val2, val1); }, - '*=': contains, - '+': function (val1, val2): string { return `${val1}+${val2}`; }, diff --git a/src/parser.ts b/src/parser.ts index c0f4e01..d79229f 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -30,9 +30,13 @@ import { PathType, ReturnExpression, ThrowExpression, + LoopControlExpression, + BlockExpressionOptions, + LoopExpression, + IncrementExpression, } from './types'; import { JsonTemplateParserError } from './errors'; -import { DATA_PARAM_KEY } from './constants'; +import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { CommonUtils } from './utils'; import { JsonTemplateEngine } from './engine'; @@ -79,10 +83,48 @@ export class JsonTemplateParser { return statements; } - private parseStatementsExpr(blockEnd?: string): StatementsExpression { + private validateStatements(statements: Expression[], options?: BlockExpressionOptions): void { + if (!statements.length) { + if ( + options?.parentType === SyntaxType.CONDITIONAL_EXPR || + options?.parentType === SyntaxType.LOOP_EXPR + ) { + throw new JsonTemplateParserError( + 'Empty statements are not allowed in loop and condtional expressions', + ); + } + return; + } + for (let i = 0; i < statements.length; i++) { + const currStatement = statements[i]; + if ( + currStatement.type === SyntaxType.RETURN_EXPR || + currStatement.type === SyntaxType.THROW_EXPR || + currStatement.type === SyntaxType.LOOP_CONTROL_EXPR + ) { + if (options?.parentType !== SyntaxType.CONDITIONAL_EXPR || i !== statements.length - 1) { + throw new JsonTemplateParserError( + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + ); + } + } + + if (this.containsLoopControlExpr(currStatement)) { + if (options?.parentType !== SyntaxType.LOOP_EXPR) { + throw new JsonTemplateParserError( + 'continue and break statements are only allowed inside loop expressions', + ); + } + } + } + } + + private parseStatementsExpr(options?: BlockExpressionOptions): StatementsExpression { + const statements = this.parseStatements(options?.blockEnd); + this.validateStatements(statements, options); return { type: SyntaxType.STATEMENTS_EXPR, - statements: this.parseStatements(blockEnd), + statements, }; } @@ -92,8 +134,8 @@ export class JsonTemplateParser { private parseAssignmentExpr(): AssignmentExpression | Expression { const expr = this.parseNextExpr(OperatorType.ASSIGNMENT); - if (expr.type === SyntaxType.PATH && this.lexer.match('=')) { - this.lexer.ignoreTokens(1); + if (expr.type === SyntaxType.PATH && this.lexer.matchAssignment()) { + const op = this.lexer.value(); const path = expr as PathExpression; if (!path.root || typeof path.root === 'object' || path.root === DATA_PARAM_KEY) { throw new JsonTemplateParserError('Invalid assignment path'); @@ -105,6 +147,7 @@ export class JsonTemplateParser { return { type: SyntaxType.ASSIGNMENT_EXPR, value: this.parseBaseExpr(), + op, path, } as AssignmentExpression; } @@ -150,6 +193,10 @@ export class JsonTemplateParser { case OperatorType.POWER: return this.parseUnaryExpr(); case OperatorType.UNARY: + return this.parsePrefixIncreamentExpr(); + case OperatorType.PREFIX_INCREMENT: + return this.parsePostfixIncreamentExpr(); + case OperatorType.POSTFIX_INCREMENT: return this.parsePathAfterExpr(); default: return this.parseConditionalExpr(); @@ -260,6 +307,7 @@ export class JsonTemplateParser { pathType, }; if (!expr.parts.length) { + expr.pathType = PathType.SIMPLE; return expr; } return JsonTemplateParser.updatePathExpr(expr); @@ -410,36 +458,96 @@ export class JsonTemplateParser { return [objectFilter, ...indexFilters]; } + private parseLoopControlExpr(): LoopControlExpression { + return { + type: SyntaxType.LOOP_CONTROL_EXPR, + control: this.lexer.value(), + }; + } + + private containsLoopControlExpr(expr: Expression | undefined): boolean { + if (!expr) { + return false; + } + if (expr.type === SyntaxType.LOOP_CONTROL_EXPR) { + return true; + } + if (expr.type === SyntaxType.CONDITIONAL_EXPR) { + return (expr as ConditionalExpression).containsLoopControls; + } + if (expr.type === SyntaxType.STATEMENTS_EXPR) { + return (expr as StatementsExpression).statements.some((s) => this.containsLoopControlExpr(s)); + } + return false; + } + + private parseCurlyBlockExpr(options?: BlockExpressionOptions): StatementsExpression { + this.lexer.expect('{'); + const expr = this.parseStatementsExpr(options); + this.lexer.expect('}'); + return expr; + } + + private parseConditionalBodyExpr(): Expression { + if (this.lexer.match('{')) { + return this.parseCurlyBlockExpr({ blockEnd: '}', parentType: SyntaxType.CONDITIONAL_EXPR }); + } + return this.parseBaseExpr(); + } + private parseConditionalExpr(): ConditionalExpression | Expression { const ifExpr = this.parseNextExpr(OperatorType.CONDITIONAL); if (this.lexer.match('?')) { this.lexer.ignoreTokens(1); - const thenExpr = this.parseConditionalExpr(); + const thenExpr = this.parseConditionalBodyExpr(); + let elseExpr: Expression | undefined; if (this.lexer.match(':')) { this.lexer.ignoreTokens(1); - const elseExpr = this.parseConditionalExpr(); - return { - type: SyntaxType.CONDITIONAL_EXPR, - if: ifExpr, - then: thenExpr, - else: elseExpr, - }; + elseExpr = this.parseConditionalBodyExpr(); } return { type: SyntaxType.CONDITIONAL_EXPR, if: ifExpr, then: thenExpr, - else: { - type: SyntaxType.LITERAL, - tokenType: TokenType.UNDEFINED, - }, + else: elseExpr, + containsLoopControls: + this.containsLoopControlExpr(thenExpr) || this.containsLoopControlExpr(elseExpr), }; } return ifExpr; } + private parseLoopExpr(): LoopExpression { + this.lexer.ignoreTokens(1); + let init: Expression | undefined; + let test: Expression | undefined; + let update: Expression | undefined; + if (!this.lexer.match('{')) { + this.lexer.expect('('); + if (!this.lexer.match(';')) { + init = this.parseAssignmentExpr(); + } + this.lexer.expect(';'); + if (!this.lexer.match(';')) { + test = this.parseLogicalORExpr(); + } + this.lexer.expect(';'); + if (!this.lexer.match(')')) { + update = this.parseAssignmentExpr(); + } + this.lexer.expect(')'); + } + return { + type: SyntaxType.LOOP_EXPR, + init, + test, + update, + body: this.parseCurlyBlockExpr({ blockEnd: '}', parentType: SyntaxType.LOOP_EXPR }), + }; + } + private parseArrayFilterExpr(): ArrayFilterExpression { this.lexer.expect('['); const filter = this.parseArrayFilter(); @@ -537,9 +645,7 @@ export class JsonTemplateParser { this.lexer.match('==$') || this.lexer.match('$=') || this.lexer.match('=$') || - this.lexer.match('*==') || this.lexer.match('==*') || - this.lexer.match('*=') || this.lexer.match('=*') ) { return { @@ -628,9 +734,59 @@ export class JsonTemplateParser { return expr; } + private parsePrefixIncreamentExpr(): IncrementExpression | Expression { + if (this.lexer.matchIncrement() || this.lexer.matchDecrement()) { + const op = this.lexer.value() as string; + if (!this.lexer.matchID()) { + throw new JsonTemplateParserError('Invalid prefix increment expression'); + } + const id = this.lexer.value(); + return { + type: SyntaxType.INCREMENT, + op, + id, + }; + } + + return this.parseNextExpr(OperatorType.PREFIX_INCREMENT); + } + + private convertToID(expr: Expression): string { + if (expr.type === SyntaxType.PATH) { + const path = expr as PathExpression; + if ( + !path.root || + typeof path.root !== 'string' || + path.parts.length !== 0 || + path.root === DATA_PARAM_KEY || + path.root === BINDINGS_PARAM_KEY + ) { + throw new JsonTemplateParserError('Invalid postfix increment expression'); + } + return path.root; + } + throw new JsonTemplateParserError('Invalid postfix increment expression'); + } + + private parsePostfixIncreamentExpr(): IncrementExpression | Expression { + let expr = this.parseNextExpr(OperatorType.POSTFIX_INCREMENT); + + if (this.lexer.matchIncrement() || this.lexer.matchDecrement()) { + return { + type: SyntaxType.INCREMENT, + op: this.lexer.value() as string, + id: this.convertToID(expr), + postfix: true, + }; + } + + return expr; + } + private parseUnaryExpr(): UnaryExpression | Expression { if ( this.lexer.match('!') || + this.lexer.match('+') || this.lexer.match('-') || this.lexer.matchTypeOf() || this.lexer.matchAwait() @@ -647,6 +803,7 @@ export class JsonTemplateParser { private shouldSkipPathParsing(expr: Expression): boolean { switch (expr.type) { + case SyntaxType.EMPTY: case SyntaxType.DEFINITION_EXPR: case SyntaxType.ASSIGNMENT_EXPR: case SyntaxType.SPREAD_EXPR: @@ -803,13 +960,10 @@ export class JsonTemplateParser { private parseFunctionExpr(asyncFn = false): FunctionExpression { this.lexer.ignoreTokens(1); const params = this.parseFunctionDefinitionParams(); - this.lexer.expect('{'); - const statements = this.parseStatementsExpr('}'); - this.lexer.expect('}'); return { type: SyntaxType.FUNCTION_EXPR, params, - body: statements, + body: this.parseCurlyBlockExpr({ blockEnd: '}' }), async: asyncFn, }; } @@ -1001,9 +1155,13 @@ export class JsonTemplateParser { private parseReturnExpr(): ReturnExpression { this.lexer.ignoreTokens(1); + let value: Expression | undefined; + if (!this.lexer.match(';')) { + value = this.parseBaseExpr(); + } return { type: SyntaxType.RETURN_EXPR, - value: this.parseBaseExpr(), + value, }; } @@ -1030,6 +1188,11 @@ export class JsonTemplateParser { return this.parseThrowExpr(); case Keyword.FUNCTION: return this.parseFunctionExpr(); + case Keyword.FOR: + return this.parseLoopExpr(); + case Keyword.CONTINUE: + case Keyword.BREAK: + return this.parseLoopControlExpr(); default: return this.parseDefinitionExpr(); } diff --git a/src/translator.ts b/src/translator.ts index 0c6152c..38c5ddd 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -36,6 +36,9 @@ import { PathType, ReturnExpression, ThrowExpression, + LoopExpression, + IncrementExpression, + LoopControlExpression, } from './types'; import { CommonUtils } from './utils'; @@ -121,6 +124,9 @@ export class JsonTemplateTranslator { case SyntaxType.SPREAD_EXPR: return this.translateSpreadExpr(expr as SpreadExpression, dest, ctx); + case SyntaxType.INCREMENT: + return this.translateIncrementExpr(expr as IncrementExpression, dest, ctx); + case SyntaxType.LITERAL: return this.translateLiteralExpr(expr as LiteralExpression, dest, ctx); @@ -133,6 +139,12 @@ export class JsonTemplateTranslator { case SyntaxType.BLOCK_EXPR: return this.translateBlockExpr(expr as BlockExpression, dest, ctx); + case SyntaxType.LOOP_EXPR: + return this.translateLoopExpr(expr as LoopExpression, dest, ctx); + + case SyntaxType.LOOP_CONTROL_EXPR: + return this.translateLoopControlExpr(expr as LoopControlExpression, dest, ctx); + case SyntaxType.FUNCTION_EXPR: return this.translateFunctionExpr(expr as FunctionExpression, dest, ctx); @@ -169,6 +181,46 @@ export class JsonTemplateTranslator { return ''; } } + translateLoopControlExpr(expr: LoopControlExpression, _dest: string, _ctx: string): string { + return `${expr.control};`; + } + + translateIncrementExpr(expr: IncrementExpression, dest: string, _ctx: string): string { + const code: string[] = []; + let incrementCode = `${expr.op}${expr.id};`; + if (expr.postfix) { + incrementCode = `${expr.id}${expr.op};`; + } + code.push(JsonTemplateTranslator.generateAssignmentCode(dest, incrementCode)); + return code.join(''); + } + + private translateLoopExpr(expr: LoopExpression, dest: string, ctx: string): string { + const code: string[] = []; + const init = this.acquireVar(); + const test = this.acquireVar(); + const update = this.acquireVar(); + const body = this.acquireVar(); + const iterator = this.acquireVar(); + if (expr.init) { + code.push(this.translateExpr(expr.init, init, ctx)); + } + code.push(`for(let ${iterator}=0;;${iterator}++){`); + if (expr.update) { + code.push(`if(${iterator} > 0) {`); + code.push(this.translateExpr(expr.update, update, ctx)); + code.push('}'); + } + if (expr.test) { + code.push(this.translateExpr(expr.test, test, ctx)); + code.push(`if(!${test}){break;}`); + } + code.push(this.translateExpr(expr.body, body, ctx)); + code.push(`}`); + JsonTemplateTranslator.generateAssignmentCode(dest, body); + this.releaseVars(iterator, body, update, test, init); + return code.join(''); + } private translateThrowExpr(expr: ThrowExpression, _dest: string, ctx: string): string { const code: string[] = []; @@ -181,29 +233,29 @@ export class JsonTemplateTranslator { private translateReturnExpr(expr: ReturnExpression, _dest: string, ctx: string): string { const code: string[] = []; - const value = this.acquireVar(); - code.push(this.translateExpr(expr.value, value, ctx)); - code.push(`return ${value};`); - this.releaseVars(value); + if (expr.value) { + const value = this.acquireVar(); + code.push(this.translateExpr(expr.value, value, ctx)); + code.push(`return ${value};`); + this.releaseVars(value); + } + code.push(`return ${ctx};`); return code.join(''); } private translateConditionalExpr(expr: ConditionalExpression, dest: string, ctx: string): string { const code: string[] = []; const ifVar = this.acquireVar(); - const thenVar = this.acquireVar(); - const elseVar = this.acquireVar(); code.push(this.translateExpr(expr.if, ifVar, ctx)); code.push(`if(${ifVar}){`); - code.push(this.translateExpr(expr.then, thenVar, ctx)); - - code.push(`${dest} = ${thenVar};`); + code.push(this.translateExpr(expr.then, dest, ctx)); code.push('} else {'); - code.push(this.translateExpr(expr.else, elseVar, ctx)); - code.push(`${dest} = ${elseVar};`); + if (expr.else) { + code.push(this.translateExpr(expr.else, dest, ctx)); + } else { + code.push(`${dest} = undefined;`); + } code.push('}'); - this.releaseVars(elseVar); - this.releaseVars(thenVar); this.releaseVars(ifVar); return code.join(''); } @@ -551,7 +603,7 @@ export class JsonTemplateTranslator { return simplePath.join(''); } - private translateAssignmentExpr(expr: AssignmentExpression, dest: string, ctx: string): string { + private translateAssignmentExpr(expr: AssignmentExpression, _dest: string, ctx: string): string { const code: string[] = []; const valueVar = this.acquireVar(); code.push(this.translateExpr(expr.value, valueVar, ctx)); @@ -562,8 +614,7 @@ export class JsonTemplateTranslator { true, ); JsonTemplateTranslator.ValidateAssignmentPath(assignmentPath); - code.push(JsonTemplateTranslator.generateAssignmentCode(assignmentPath, valueVar)); - code.push(JsonTemplateTranslator.generateAssignmentCode(dest, valueVar)); + code.push(JsonTemplateTranslator.generateAssignmentCode(assignmentPath, valueVar, expr.op)); this.releaseVars(valueVar); return code.join(''); } @@ -765,15 +816,7 @@ export class JsonTemplateTranslator { code.push(this.translateExpr(args[0], val1, ctx)); code.push(this.translateExpr(args[1], val2, ctx)); - code.push( - dest, - '=', - binaryOperators[expr.op]( - JsonTemplateTranslator.returnSingleValue(args[0], val1), - JsonTemplateTranslator.returnSingleValue(args[1], val2), - ), - ';', - ); + code.push(dest, '=', binaryOperators[expr.op](val1, val2), ';'); this.releaseVars(val1, val2); return code.join(''); @@ -809,14 +852,6 @@ export class JsonTemplateTranslator { return `Object.values(${varName}).filter(v => v !== null && v !== undefined)`; } - private static returnSingleValue(arg: Expression, varName: string): string { - if (arg.type === SyntaxType.LITERAL) { - return varName; - } - - return `(Array.isArray(${varName}) ? ${varName}[0] : ${varName})`; - } - private static convertToSingleValueIfSafe(varName: string): string { return `${varName} = ${varName}.length < 2 ? ${varName}[0] : ${varName};`; } @@ -829,7 +864,7 @@ export class JsonTemplateTranslator { return code.join(''); } - private static generateAssignmentCode(key: string, val: string): string { - return `${key}=${val};`; + private static generateAssignmentCode(key: string, val: string, op: string = '='): string { + return `${key}${op}${val};`; } } diff --git a/src/types.ts b/src/types.ts index b2490da..bafa18e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,9 @@ export enum Keyword { NOT = 'not', RETURN = 'return', THROW = 'throw', + CONTINUE = 'continue', + BREAK = 'break', + FOR = 'for', } export enum TokenType { @@ -30,6 +33,7 @@ export enum TokenType { } // In the order of precedence + export enum OperatorType { BASE, CONDITIONAL, @@ -44,6 +48,8 @@ export enum OperatorType { MULTIPLICATION, POWER, UNARY, + PREFIX_INCREMENT, + POSTFIX_INCREMENT, } export enum SyntaxType { @@ -52,6 +58,7 @@ export enum SyntaxType { PATH_OPTIONS, SELECTOR, LAMBDA_ARG, + INCREMENT, LITERAL, LOGICAL_COALESCE_EXPR, LOGICAL_OR_EXPR, @@ -79,6 +86,8 @@ export enum SyntaxType { RETURN_EXPR, THROW_EXPR, STATEMENTS_EXPR, + LOOP_CONTROL_EXPR, + LOOP_EXPR, } export enum PathType { @@ -158,6 +167,7 @@ export interface ConcatExpression extends Expression { export interface AssignmentExpression extends Expression { path: PathExpression; value: Expression; + op: string; } export interface DefinitionExpression extends Expression { @@ -195,6 +205,12 @@ export interface PathExpression extends Expression { pathType: PathType; } +export interface IncrementExpression extends Expression { + id: string; + op: string; + postfix?: boolean; +} + export interface SelectorExpression extends Expression { selector: string; prop?: Token; @@ -213,11 +229,27 @@ export interface FunctionCallExpression extends Expression { export interface ConditionalExpression extends Expression { if: Expression; then: Expression; - else: Expression; + else?: Expression; + containsLoopControls: boolean; } +export type BlockExpressionOptions = { + blockEnd?: string; + parentType?: SyntaxType; +}; + export interface ReturnExpression extends Expression { - value: Expression; + value?: Expression; +} + +export interface LoopControlExpression extends Expression { + control: string; +} +export interface LoopExpression extends Expression { + init?: Expression; + test?: Expression; + update?: Expression; + body: StatementsExpression; } export interface ThrowExpression extends Expression { diff --git a/test/scenario.test.ts b/test/scenario.test.ts index 32e9e62..1aeca0e 100644 --- a/test/scenario.test.ts +++ b/test/scenario.test.ts @@ -36,7 +36,7 @@ describe(`${scenarioName}:`, () => { } catch (error: any) { console.log('Actual result', JSON.stringify(result, null, 2)); console.log('Expected result', JSON.stringify(scenario.output, null, 2)); - expect(error.message).toEqual(scenario.error); + expect(error.message).toContain(scenario.error); } }); }); diff --git a/test/scenarios/arrays/template.jt b/test/scenarios/arrays/template.jt index 53340ec..3104f83 100644 --- a/test/scenarios/arrays/template.jt +++ b/test/scenarios/arrays/template.jt @@ -2,5 +2,5 @@ let a = [ "string1", `string2`, 'string3', "aa\"a", true, false, undefined, null, 20.02, .22, [1., 2, 3], {"b": [1, 2]} ]; -[...a[-2], a[0,8], a[1:6], a[-1].b[1]] +[...~r a[-2], a[0,8], a[1:6],~r a[-1].b[1]] diff --git a/test/scenarios/assignments/data.ts b/test/scenarios/assignments/data.ts index 9d712eb..9b7f1de 100644 --- a/test/scenarios/assignments/data.ts +++ b/test/scenarios/assignments/data.ts @@ -7,7 +7,7 @@ export const data: Scenario[] = [ }, output: { a: { - b: [12, 22], + b: [11, 13], 'c key': 4, }, }, diff --git a/test/scenarios/assignments/template.jt b/test/scenarios/assignments/template.jt index f16c553..2824683 100644 --- a/test/scenarios/assignments/template.jt +++ b/test/scenarios/assignments/template.jt @@ -1,5 +1,13 @@ -let a = 10 -let b = -a + 30 +let a = 3; +a*=3; +--a; +++a; +let b = -a + 30; +b-=1 +b/=2 +b+=1; +b--; +b++; let cKey = "c key"; let c = { a: { b: [a, b], [cKey]: 2 } } let {d, e, f} = {d: 2, e: 2, f: 1} diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts index 68b4186..0974cfa 100644 --- a/test/scenarios/comparisons/data.ts +++ b/test/scenarios/comparisons/data.ts @@ -23,9 +23,6 @@ export const data: Scenario[] = [ true, true, true, - true, - true, - true, ], }, ]; diff --git a/test/scenarios/comparisons/template.jt b/test/scenarios/comparisons/template.jt index 567f744..945a949 100644 --- a/test/scenarios/comparisons/template.jt +++ b/test/scenarios/comparisons/template.jt @@ -6,10 +6,7 @@ 'IgnoreCase' == 'ignorecase', 'CompareWithCase' !== 'comparewithCase', 'CompareWithCase' === 'CompareWithCase', -'I contain' *= 'i', -'I contain' *== 'I', 'i' =* 'I contain', -'I contain' *== 'I', 'I' ==* 'I contain', 'I end with' $= 'With', 'I end with' $== 'with', diff --git a/test/scenarios/conditions/data.ts b/test/scenarios/conditions/data.ts index fec1cb4..ddfaa35 100644 --- a/test/scenarios/conditions/data.ts +++ b/test/scenarios/conditions/data.ts @@ -2,14 +2,57 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ { - templatePath: 'if-then.jt', + templatePath: 'empty_if.jt', + error: 'Empty statements are not allowed in loop and condtional expressions', + }, + { + templatePath: 'empty_then.jt', + error: 'Empty statements are not allowed in loop and condtional expressions', + }, + { + templatePath: 'if_block.jt', + input: { + a: -5, + }, + output: 'a <= 1', + }, + { + templatePath: 'if_block.jt', + input: { + a: 1, + }, + output: 'a <= 1', + }, + { + templatePath: 'if_block.jt', + input: { + a: 2, + }, + output: 'a > 1', + }, + { + templatePath: 'if_block.jt', + input: { + a: 3, + }, + output: 'a > 2', + }, + { + templatePath: 'if_block.jt', + input: { + a: 10, + }, + output: 'a > 3', + }, + { + templatePath: 'if_then.jt', input: { a: -5, }, output: 0, }, { - templatePath: 'if-then.jt', + templatePath: 'if_then.jt', input: { a: 5, }, @@ -40,21 +83,21 @@ export const data: Scenario[] = [ output: 15, }, { - templatePath: 'undefined-arr-cond.jt', + templatePath: 'undefined_arr_cond.jt', input: { products: [{ a: 1 }, { a: 2 }], }, output: 'no', }, { - templatePath: 'undefined-arr-cond.jt', + templatePath: 'undefined_arr_cond.jt', input: { products: [{ objectID: 1 }, { objectID: 2 }], }, output: 'yes', }, { - templatePath: 'undefined-arr-cond.jt', + templatePath: 'undefined_arr_cond.jt', input: { otherProperty: [{ objectID: 1 }, { objectID: 2 }], }, diff --git a/test/scenarios/conditions/empty_if.jt b/test/scenarios/conditions/empty_if.jt new file mode 100644 index 0000000..1fdbebc --- /dev/null +++ b/test/scenarios/conditions/empty_if.jt @@ -0,0 +1 @@ +.num > 0 ? {} \ No newline at end of file diff --git a/test/scenarios/conditions/empty_then.jt b/test/scenarios/conditions/empty_then.jt new file mode 100644 index 0000000..33ed3a4 --- /dev/null +++ b/test/scenarios/conditions/empty_then.jt @@ -0,0 +1 @@ +.num > 0 ? let a = .num : {} \ No newline at end of file diff --git a/test/scenarios/conditions/if_block.jt b/test/scenarios/conditions/if_block.jt new file mode 100644 index 0000000..384c1d0 --- /dev/null +++ b/test/scenarios/conditions/if_block.jt @@ -0,0 +1,10 @@ +(.a > 1) ? { + (.a > 2) ? { + (.a > 3) ? { + return "a > 3"; + } + return "a > 2"; + } + return "a > 1"; +} +"a <= 1" \ No newline at end of file diff --git a/test/scenarios/conditions/if-then.jt b/test/scenarios/conditions/if_then.jt similarity index 100% rename from test/scenarios/conditions/if-then.jt rename to test/scenarios/conditions/if_then.jt diff --git a/test/scenarios/conditions/undefined-arr-cond.jt b/test/scenarios/conditions/undefined_arr_cond.jt similarity index 100% rename from test/scenarios/conditions/undefined-arr-cond.jt rename to test/scenarios/conditions/undefined_arr_cond.jt diff --git a/test/scenarios/context_variables/filter.jt b/test/scenarios/context_variables/filter.jt index ee5a5db..5fb62bd 100644 --- a/test/scenarios/context_variables/filter.jt +++ b/test/scenarios/context_variables/filter.jt @@ -3,6 +3,6 @@ and then on the result rest of the path will be executed */ .{.[].length > 1}.().@item#idx.({ - a: item.a, + a: ~r item.a, idx: idx }) \ No newline at end of file diff --git a/test/scenarios/filters/array_filters.jt b/test/scenarios/filters/array_filters.jt index 6a2369d..0bcfce1 100644 --- a/test/scenarios/filters/array_filters.jt +++ b/test/scenarios/filters/array_filters.jt @@ -1,5 +1,5 @@ let a = [1, 2, 3, 4, 5, {"a": 1, "b": 2, "c": 3}]; [ a[2:], a[:3], a[3:5], a[...[1, 3]].[0, 1], - a[-2], a[1], a[:-2], a[-2:], a[-1]["a", "b"] + ~r a[-2], a[1], a[:-2], a[-2:], a[-1]["a", "b"] ] \ No newline at end of file diff --git a/test/scenarios/functions/data.ts b/test/scenarios/functions/data.ts index 8ec97ad..0aa3a1a 100644 --- a/test/scenarios/functions/data.ts +++ b/test/scenarios/functions/data.ts @@ -3,7 +3,7 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ { templatePath: 'function_calls.jt', - output: ['abc', undefined, undefined], + output: ['abc', null, undefined], }, { templatePath: 'js_date_function.jt', diff --git a/test/scenarios/increment_statements/data.ts b/test/scenarios/increment_statements/data.ts new file mode 100644 index 0000000..157386c --- /dev/null +++ b/test/scenarios/increment_statements/data.ts @@ -0,0 +1,28 @@ +import { Scenario } from '../../types'; + +export const data: Scenario[] = [ + { + templatePath: 'postfix_decrement_on_literal.jt', + error: 'Invalid postfix increment expression', + }, + { + templatePath: 'postfix_decrement_on_non_id.jt', + error: 'Invalid postfix increment expression', + }, + { + templatePath: 'postfix_increment_on_literal.jt', + error: 'Invalid postfix increment expression', + }, + { + templatePath: 'postfix_increment_on_non_id.jt', + error: 'Invalid postfix increment expression', + }, + { + templatePath: 'prefix_decrement_on_literal.jt', + error: 'Invalid prefix increment expression', + }, + { + templatePath: 'prefix_increment_on_literal.jt', + error: 'Invalid prefix increment expression', + }, +]; diff --git a/test/scenarios/increment_statements/postfix_decrement_on_literal.jt b/test/scenarios/increment_statements/postfix_decrement_on_literal.jt new file mode 100644 index 0000000..6a5ca8a --- /dev/null +++ b/test/scenarios/increment_statements/postfix_decrement_on_literal.jt @@ -0,0 +1 @@ +1-- \ No newline at end of file diff --git a/test/scenarios/increment_statements/postfix_decrement_on_non_id.jt b/test/scenarios/increment_statements/postfix_decrement_on_non_id.jt new file mode 100644 index 0000000..99e2dcb --- /dev/null +++ b/test/scenarios/increment_statements/postfix_decrement_on_non_id.jt @@ -0,0 +1 @@ +.a-- \ No newline at end of file diff --git a/test/scenarios/increment_statements/postfix_increment_on_literal.jt b/test/scenarios/increment_statements/postfix_increment_on_literal.jt new file mode 100644 index 0000000..1c8af92 --- /dev/null +++ b/test/scenarios/increment_statements/postfix_increment_on_literal.jt @@ -0,0 +1 @@ +1++ \ No newline at end of file diff --git a/test/scenarios/increment_statements/postfix_increment_on_non_id.jt b/test/scenarios/increment_statements/postfix_increment_on_non_id.jt new file mode 100644 index 0000000..d7118d4 --- /dev/null +++ b/test/scenarios/increment_statements/postfix_increment_on_non_id.jt @@ -0,0 +1 @@ +.a++ \ No newline at end of file diff --git a/test/scenarios/increment_statements/prefix_decrement_on_literal.jt b/test/scenarios/increment_statements/prefix_decrement_on_literal.jt new file mode 100644 index 0000000..bfbbe2b --- /dev/null +++ b/test/scenarios/increment_statements/prefix_decrement_on_literal.jt @@ -0,0 +1 @@ +--1 \ No newline at end of file diff --git a/test/scenarios/increment_statements/prefix_increment_on_literal.jt b/test/scenarios/increment_statements/prefix_increment_on_literal.jt new file mode 100644 index 0000000..e3bc4bd --- /dev/null +++ b/test/scenarios/increment_statements/prefix_increment_on_literal.jt @@ -0,0 +1 @@ +++1 \ No newline at end of file diff --git a/test/scenarios/loops/break_without_condition.jt b/test/scenarios/loops/break_without_condition.jt new file mode 100644 index 0000000..45127c0 --- /dev/null +++ b/test/scenarios/loops/break_without_condition.jt @@ -0,0 +1 @@ +continue; \ No newline at end of file diff --git a/test/scenarios/loops/break_without_loop.jt b/test/scenarios/loops/break_without_loop.jt new file mode 100644 index 0000000..0fac5a1 --- /dev/null +++ b/test/scenarios/loops/break_without_loop.jt @@ -0,0 +1,2 @@ + +(.num < 0) ? break; \ No newline at end of file diff --git a/test/scenarios/loops/complex_loop.jt b/test/scenarios/loops/complex_loop.jt new file mode 100644 index 0000000..ceecf57 --- /dev/null +++ b/test/scenarios/loops/complex_loop.jt @@ -0,0 +1,5 @@ +let count = 0; +for (let i = 0; i < 10; i++) { + i % 2 === 0 ? count+=2 : count+=1 +} +count; \ No newline at end of file diff --git a/test/scenarios/loops/continue.jt b/test/scenarios/loops/continue.jt new file mode 100644 index 0000000..8f0bb42 --- /dev/null +++ b/test/scenarios/loops/continue.jt @@ -0,0 +1,7 @@ +let count = 0; +for (let i=0;i <= ^.num;i++) { + console.log(i); + i % 2 === 0 ? continue; + count+=i; +} +count \ No newline at end of file diff --git a/test/scenarios/loops/continue_without_condition.jt b/test/scenarios/loops/continue_without_condition.jt new file mode 100644 index 0000000..45127c0 --- /dev/null +++ b/test/scenarios/loops/continue_without_condition.jt @@ -0,0 +1 @@ +continue; \ No newline at end of file diff --git a/test/scenarios/loops/continue_without_loop.jt b/test/scenarios/loops/continue_without_loop.jt new file mode 100644 index 0000000..ff7c8c0 --- /dev/null +++ b/test/scenarios/loops/continue_without_loop.jt @@ -0,0 +1,2 @@ + +(.num < 0) ? continue; \ No newline at end of file diff --git a/test/scenarios/loops/data.ts b/test/scenarios/loops/data.ts new file mode 100644 index 0000000..ac9acbc --- /dev/null +++ b/test/scenarios/loops/data.ts @@ -0,0 +1,81 @@ +import { Scenario } from '../../types'; + +export const data: Scenario[] = [ + { + templatePath: 'break_without_condition.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, + { + templatePath: 'break_without_loop.jt', + error: 'continue and break statements are only allowed inside loop expressions', + }, + { + templatePath: 'complex_loop.jt', + output: 15, + }, + { + templatePath: 'continue_without_condition.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, + { + templatePath: 'continue_without_loop.jt', + error: 'continue and break statements are only allowed inside loop expressions', + }, + { + templatePath: 'continue.jt', + input: { + num: 10, + }, + output: 25, + }, + { + templatePath: 'empty_loop.jt', + error: 'Empty statements are not allowed in loop and condtional expressions', + }, + { + templatePath: 'just_for.jt', + input: { + num: 10, + }, + output: 55, + }, + { + input: { + num: 10, + }, + output: 55, + templatePath: 'no_init.jt', + }, + { + input: { + num: 10, + }, + output: 55, + templatePath: 'no_test.jt', + }, + { + input: { + num: 10, + }, + output: 55, + templatePath: 'no_update.jt', + }, + { + templatePath: 'statement_after_break.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, + { + templatePath: 'statement_after_continue.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, + { + input: { + num: 10, + }, + output: 55, + }, +]; diff --git a/test/scenarios/loops/empty_loop.jt b/test/scenarios/loops/empty_loop.jt new file mode 100644 index 0000000..e001913 --- /dev/null +++ b/test/scenarios/loops/empty_loop.jt @@ -0,0 +1 @@ +for {} \ No newline at end of file diff --git a/test/scenarios/loops/just_for.jt b/test/scenarios/loops/just_for.jt new file mode 100644 index 0000000..18f7caf --- /dev/null +++ b/test/scenarios/loops/just_for.jt @@ -0,0 +1,8 @@ +let count = 0; +let i = 0; +for { + i > ^.num ? break; + count+=i; + i++; +} +count \ No newline at end of file diff --git a/test/scenarios/loops/no_init.jt b/test/scenarios/loops/no_init.jt new file mode 100644 index 0000000..93dce73 --- /dev/null +++ b/test/scenarios/loops/no_init.jt @@ -0,0 +1,6 @@ +let count = 0; +let i = 0; +for (;i <= ^.num;i++) { + count+=i; +} +count \ No newline at end of file diff --git a/test/scenarios/loops/no_test.jt b/test/scenarios/loops/no_test.jt new file mode 100644 index 0000000..6546357 --- /dev/null +++ b/test/scenarios/loops/no_test.jt @@ -0,0 +1,6 @@ +let count = 0; +for (let i=0;;i++) { + i > ^.num ? break; + count+=i; +} +count \ No newline at end of file diff --git a/test/scenarios/loops/no_update.jt b/test/scenarios/loops/no_update.jt new file mode 100644 index 0000000..f4296fd --- /dev/null +++ b/test/scenarios/loops/no_update.jt @@ -0,0 +1,6 @@ +let count = 0; +for (let i=0;i <= ^.num;) { + count+=i; + i++; +} +count \ No newline at end of file diff --git a/test/scenarios/loops/statement_after_break.jt b/test/scenarios/loops/statement_after_break.jt new file mode 100644 index 0000000..e436239 --- /dev/null +++ b/test/scenarios/loops/statement_after_break.jt @@ -0,0 +1,4 @@ +.num > 1 ? { + continue; + let count = 0; +} \ No newline at end of file diff --git a/test/scenarios/loops/statement_after_continue.jt b/test/scenarios/loops/statement_after_continue.jt new file mode 100644 index 0000000..e436239 --- /dev/null +++ b/test/scenarios/loops/statement_after_continue.jt @@ -0,0 +1,4 @@ +.num > 1 ? { + continue; + let count = 0; +} \ No newline at end of file diff --git a/test/scenarios/loops/template.jt b/test/scenarios/loops/template.jt new file mode 100644 index 0000000..1ecfcce --- /dev/null +++ b/test/scenarios/loops/template.jt @@ -0,0 +1,5 @@ +let count = 0; +for (let i=0;i <= ^.num;i++) { + count+=i; +} +count \ No newline at end of file diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts index 03ac6a0..6012d2a 100644 --- a/test/scenarios/paths/data.ts +++ b/test/scenarios/paths/data.ts @@ -164,5 +164,8 @@ export const data: Scenario[] = [ 4, 1, ], + options: { + defaultPathType: PathType.RICH, + }, }, ]; diff --git a/test/scenarios/return/data.ts b/test/scenarios/return/data.ts index a516931..5487785 100644 --- a/test/scenarios/return/data.ts +++ b/test/scenarios/return/data.ts @@ -1,6 +1,16 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ + { + templatePath: 'return_without_condition.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, + { + templatePath: 'statement_after_return.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, { input: 3, output: 1, diff --git a/test/scenarios/return/return_without_condition.jt b/test/scenarios/return/return_without_condition.jt new file mode 100644 index 0000000..699755f --- /dev/null +++ b/test/scenarios/return/return_without_condition.jt @@ -0,0 +1 @@ +return; \ No newline at end of file diff --git a/test/scenarios/return/statement_after_return.jt b/test/scenarios/return/statement_after_return.jt new file mode 100644 index 0000000..f12ef19 --- /dev/null +++ b/test/scenarios/return/statement_after_return.jt @@ -0,0 +1,4 @@ +.num > 1 ? { + return; + let count = 0; +} \ No newline at end of file diff --git a/test/scenarios/return/template.jt b/test/scenarios/return/template.jt index ca2e91b..7c63cf9 100644 --- a/test/scenarios/return/template.jt +++ b/test/scenarios/return/template.jt @@ -1,2 +1,4 @@ -(. % 2 === 0) ? return ./2; +(. % 2 === 0) ? { + return ./2; +} (. - 1)/2; \ No newline at end of file diff --git a/test/scenarios/selectors/template.jt b/test/scenarios/selectors/template.jt index bd2dd8e..baa88d0 100644 --- a/test/scenarios/selectors/template.jt +++ b/test/scenarios/selectors/template.jt @@ -1 +1 @@ -..d + .a * .b \ No newline at end of file +..d[0] + .a * .b \ No newline at end of file diff --git a/test/scenarios/throw/data.ts b/test/scenarios/throw/data.ts index 3faeeae..3a8db54 100644 --- a/test/scenarios/throw/data.ts +++ b/test/scenarios/throw/data.ts @@ -1,6 +1,11 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ + { + templatePath: 'statement_after_throw.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, { input: 3, error: 'num must be even', @@ -9,4 +14,9 @@ export const data: Scenario[] = [ input: 2, output: 1, }, + { + templatePath: 'throw_without_condition.jt', + error: + 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', + }, ]; diff --git a/test/scenarios/throw/statement_after_throw.jt b/test/scenarios/throw/statement_after_throw.jt new file mode 100644 index 0000000..5b497ce --- /dev/null +++ b/test/scenarios/throw/statement_after_throw.jt @@ -0,0 +1,4 @@ +.num % 2 !== 0 ? { + throw new Error("num must be even"); + let count = 0; +} \ No newline at end of file diff --git a/test/scenarios/throw/throw_without_condition.jt b/test/scenarios/throw/throw_without_condition.jt new file mode 100644 index 0000000..566a389 --- /dev/null +++ b/test/scenarios/throw/throw_without_condition.jt @@ -0,0 +1 @@ +throw new Error("num must be even"); \ No newline at end of file diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts index 4ee3a9f..6187fbc 100644 --- a/test/utils/scenario.ts +++ b/test/utils/scenario.ts @@ -1,12 +1,14 @@ import { readFileSync } from 'fs'; import { join } from 'path'; -import { JsonTemplateEngine } from '../../src'; +import { JsonTemplateEngine, PathType } from '../../src'; import { Scenario } from '../types'; export class ScenarioUtils { static createTemplateEngine(scenarioDir: string, scenario: Scenario): JsonTemplateEngine { const templatePath = join(scenarioDir, Scenario.getTemplatePath(scenario)); const template = readFileSync(templatePath, 'utf-8'); + scenario.options = scenario.options || {}; + scenario.options.defaultPathType = scenario.options.defaultPathType || PathType.SIMPLE; return JsonTemplateEngine.create(template, scenario.options); }