diff --git a/docs/syntax.md b/docs/syntax.md index a41479e..c848be3 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -1,13 +1,13 @@ Template is a set of statements and result the last statement is the output of the template. -### Variables +## Variables ```js const a = 1 let b = a + 2 a + b ``` -### Input and Bindings +## Input and Bindings Input refers to the JSON document we would like to process using a template. Bindings refer to additional data or functions we would provide to process the data efficiently. Example: @@ -18,7 +18,7 @@ Example: * `{defaultName: 'World'}` is bindings. * `$.defaultName` refers to "defaultName" property of the bindings. Refer the [example](../test/scenarios/bindings/template.jt) for more clarity. -### Arrays +## Arrays ```js let arr = [1, 2, 3, 4] let a = arr[1, 2] // [2, 3] @@ -27,7 +27,7 @@ let c = arr[-2:] // [3, 4] ``` Refer the [example](../test/scenarios/arrays/template.jt) for more clarity. -### Objects +## Objects ```js let key = "some key" // { "a": 1, "b": 2, "c": 3, "some key": 4 } @@ -38,3 +38,148 @@ let c = obj{["a", "b"]} // { "a": 1, "b": 2} let d = obj{~["a", "b"]} // { "c": 3, "some key": 4} ``` Refer the [example](../test/scenarios/objects/template.jt) for more clarity. + +## Functions +### Normal functions +```js +let fn = function(arg1, arg2){ + arg1 + arg2 +} +``` +The result of the last statement of function will be returned as result of the function. We can also use rest params (`...args`). +### Lambda/Short functions +```js +let fn = array.map(lambda 2 * ?0); +``` +This function gets converted to: +```js +let fn = array.map(function(args) { + 2 * args[0] +}) +``` +Lambda functions are short to express the intention and it is convenient sometimes. +### Async functions +```js +let fn = async function(arg1, arg2){ + const result = await doSomethingAsync(arg1, arg2) + doSomethingSync(result) +} +``` +**Note:** When we want to use async functions then we need to create template engine using `JsonTemplateEngine.create`. If you create a template this way then it will be created as an async function so we can `await` anywhere in the template. +```js +let result = await doSomething(.a, .b) +``` +## Paths +Paths are used to access properties in `input`, `bindings` and `variables`. +### Simple Paths +Simple paths support limited path features and get translated as direct property access statements in the generate javascript code. +`a.b.c` gets translated to `a?.b?.c` so they are very fast compared to [Rich paths](#rich-paths). Simple paths are ideal when we know the object structure. + +**Supported features:** +* [Simple Selectors](#simple-selectors) +* [Single Index Filters](#single-index-or-property-filters) +Refer the [example](../test/scenarios/paths/simple_path.jt) for more clarity. +### Rich Paths +Rich paths gets converted complex code to support different variations in the data. + +If we use rich path for expression: `a.b.c` then it automatically following variations. +* `[{"a": { "b": [{"c": 2}]}}]` +* `{"a": { "b": [{"c": 2}]}}` +* `{"a": [{ "b": [{"c": 2}]}]}` +* Automatically handles selection from nested objects and arrays. + +#### Simple selectors +```js +let x = a.b.c; +let y = a."some key".c +``` +Refer the [example](../test/scenarios/selectors/template.jt) for more clarity. +#### Wildcard selectors +```js +a.*.c // selects c from any direct property of a +``` +Refer the [example](../test/scenarios/selectors/wild_cards.jt) for more clarity. +#### Descendent selectors +```js +// selects c from any child property of a +// a.b.c, a.b1.b2.c or a.b1.b2.b3.c +let x = a..c; +let y = a.."some key"; +``` +Refer the [example](../test/scenarios/selectors/template.jt) for more clarity. +#### Single Index or Property Filters +```js +let x = a[0].c; +let y = a[-1].c; // selects last element from array +let z = a["some key"].c +``` +Refer the [example](../test/scenarios/filters/array_filters.jt) for more clarity. +#### Multi Indexes or Properties Filters +```js +let x = a[0, 2, 5].c; +let y = a["some key1", "some key2"].c; +``` +Refer the [example](../test/scenarios/filters/array_filters.jt) for more clarity. +#### Range filters +```js +let x = a[2:5].c; +let y = a[:-2].c; +let z = a[2:].c; +``` +#### Object Property Filters +```js +let x = obj{["a", "b"]}; // selects a and b +let y = obj{~["a", "b"]}; // selects all properties except a and b +``` +Refer the [example](../test/scenarios/filters/object_indexes.jt) for more clarity. +#### Conditional or Object Filters +```js +let x = obj{.a > 1}; +``` +Refer the [example](../test/scenarios/filters/object_filters.jt) for more clarity. +#### Block expressions +```js +let x = obj.({ + a: .a + 1, + b: .b + 2 +}); +let x = obj.([.a+1, .b+2]); +``` +Refer the [example](../test/scenarios/paths/block.jt) for more clarity. + +### Path Options +We can mention defaultPathType while creating engine instance. +```js +// For using simple path as default path type +// a.b.c will be treated as simple path +JsonTemplateEngine.create(`a.b.c`, {defaultPathType: PathType.SIMPLE}); +// For using rich path as default path type +// a.b.c will be treated as rich path +JsonTemplateEngine.create(`a.b.c`, {defaultPathType: PathType.RICH}); +``` +We can override the default path option using tags. +```js +// Use ~s to treat a.b.c as simple path +~s a.b.c +// Use ~r to treat a.b.c as rich path +~r a.b.c +``` +**Note:** Rich paths are slower compare to the simple paths. + +## Compile time expressions +Compile time expressions are evaluated during compilation phase using compileTimeBindings option. +```js +// {{$.a.b.c}} gets translated to 1 and +// final translated code will be "let a = 1;" +JsonTemplateEngine.create(`let a = {{$.a.b.c}};`, { + compileTimeBindings: { + a: { + b: { + c: 1 + } + } + } +}); +``` +We can use compile time expressions to generate a template and then recompile it as expression. Refer the [example](../test/scenarios/compile_time_expressions/two_level_path_processing.jt). + diff --git a/readme.md b/readme.md index d21efb0..c6966a9 100644 --- a/readme.md +++ b/readme.md @@ -35,14 +35,19 @@ This library generates a javascript function code from the template and then use 1. [Objects](test/scenarios/objects/template.jt) 1. [Functions](test/scenarios/functions/template.jt) 1. [Bindings](test/scenarios/bindings/template.jt) +1. [Comments](test/scenarios/comments/template.jt) 1. [Paths](test/scenarios/paths/template.jt) * [Filters](test/scenarios/filters/template.jt) * [Selectors](test/scenarios/selectors/template.jt) - * [Context Variables](test/scenarios/selectors/context_variables.jt) + * [Context variables](test/scenarios/selectors/context_variables.jt) + * [Simple paths](test/scenarios/paths/simple_path.jt) + * [Rich paths](test/scenarios/paths/rich_path.jt) + * [Paths options](test/scenarios/paths/options.jt) 1. [Conditions](test/scenarios/conditions/template.jt) * [Comparisons](test/scenarios/comparisons/template.jt) 1. [Math operations](test/scenarios/math/template.jt) 1. [Logical operations](test/scenarios/logics/template.jt) +1. [Compile time expressions](test/scenarios/compile_time_expressions/template.jt) For more examples, refer [Scenarios](test/scenarios) diff --git a/src/engine.ts b/src/engine.ts index 99e719b..ebb03d4 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -2,7 +2,7 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; import { JsonTemplateTranslator } from './translator'; -import { Dictionary, EngineOptions, Expression } from './types'; +import { EngineOptions, Expression } from './types'; import { CommonUtils } from './utils'; export class JsonTemplateEngine { @@ -62,7 +62,7 @@ export class JsonTemplateEngine { return translator.translate(); } - evaluate(data: any, bindings: Dictionary = {}): any { + evaluate(data: any, bindings: Record = {}): any { return this.fn(data || {}, bindings); } } diff --git a/src/errors.ts b/src/errors.ts index 71ba632..9e190e2 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -10,7 +10,7 @@ export class JsonTemplateParserError extends Error { } } -export class JsosTemplateTranslatorError extends Error { +export class JsonTemplateTranslatorError extends Error { constructor(message: string) { super(message); } diff --git a/src/lexer.ts b/src/lexer.ts index 5d706e8..e4b5ea1 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -13,6 +13,8 @@ export class JsonTemplateLexer { private readonly codeChars: string[]; private buf: Token[]; private idx = 0; + private lastParsedToken?: Token; + constructor(template: string) { this.buf = []; this.codeChars = template.split(''); @@ -27,8 +29,8 @@ export class JsonTemplateLexer { return this.idx; } - getCodeChars(start: number, end: number): string[] { - return this.codeChars.slice(start, end); + getCode(start: number, end: number): string { + return this.codeChars.slice(start, end).join(''); } match(value?: string, steps = 0): boolean { @@ -250,16 +252,14 @@ export class JsonTemplateLexer { } lex(): Token { - let token; - if (this.buf[0]) { this.idx = this.buf[0].range[1]; - token = this.buf[0]; + this.lastParsedToken = this.buf[0]; this.buf = this.buf.slice(1); - return token; + return this.lastParsedToken; } - - return this.advance(); + this.lastParsedToken = this.advance(); + return this.lastParsedToken; } static isLiteralToken(token: Token) { @@ -282,15 +282,11 @@ export class JsonTemplateLexer { this.throwError(MESSAGES.UNEXP_TOKEN, token.value); } - getContext(length = 10): string { - return this.codeChars.slice(this.idx - length, this.idx + length).join(''); - } - private throwError(messageFormat: string, ...args): never { const msg = messageFormat.replace(/%(\d)/g, (_, idx) => { return args[idx]; }); - throw new JsonTemplateLexerError(msg + ' at ' + this.getContext(15)); + throw new JsonTemplateLexerError(msg); } private static isDigit(ch: string) { diff --git a/src/parser.ts b/src/parser.ts index 51641f1..0d88296 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -62,7 +62,7 @@ export class JsonTemplateParser { const currIdx = this.lexer.currentIndex(); const nextTokenStart = this.lexer.lookahead().range[0]; - const code = this.lexer.getCodeChars(currIdx, nextTokenStart); + const code = this.lexer.getCode(currIdx, nextTokenStart); if (!code.includes('\n')) { this.lexer.throwUnexpectedToken(); } @@ -110,7 +110,17 @@ export class JsonTemplateParser { } private parseBaseExpr(): Expression { - return this.parseNextExpr(OperatorType.BASE); + const startIdx = this.lexer.currentIndex(); + try { + const expr = this.parseNextExpr(OperatorType.BASE); + return expr; + } catch (error: any) { + const code = this.lexer.getCode(startIdx, this.lexer.currentIndex()); + if (error.message.includes('at')) { + throw error; + } + throw new JsonTemplateParserError(error.message + ' at ' + code); + } } private parseNextExpr(currentOperation: OperatorType): Expression { @@ -157,7 +167,7 @@ export class JsonTemplateParser { } else if (this.lexer.matchToArray()) { return this.parsePathOptions(); } else if (this.lexer.match('[')) { - return this.parseArrayFiltersExpr(); + return this.parseArrayFilterExpr(); } else if (this.lexer.match('{')) { return this.parseObjectFiltersExpr(); } else if (this.lexer.match('@') || this.lexer.match('#')) { @@ -259,7 +269,7 @@ export class JsonTemplateParser { ): ArrayFilterExpression { return { type: SyntaxType.ARRAY_FILTER_EXPR, - filters: [expr], + filter: expr, }; } @@ -328,7 +338,7 @@ export class JsonTemplateParser { }; } - private parseArrayFilterExpr(): Expression { + private parseArrayFilter(): Expression { if (this.lexer.matchSpread()) { return this.parseArrayIndexFilterExpr(); } @@ -345,7 +355,7 @@ export class JsonTemplateParser { this.lexer.ignoreTokens(1); exclude = true; } - // excluding is applicaple only for index filters + // excluding is applicable only for index filters if (exclude || this.lexer.match('[')) { return { type: SyntaxType.OBJECT_INDEX_FILTER_EXPR, @@ -419,19 +429,14 @@ export class JsonTemplateParser { return ifExpr; } - private parseArrayFiltersExpr(): ArrayFilterExpression { - const filters: Expression[] = []; - while (this.lexer.match('[') && !this.lexer.match(']', 1)) { - this.lexer.ignoreTokens(1); - filters.push(this.parseArrayFilterExpr()); - this.lexer.expect(']'); - if (this.lexer.match('.') && this.lexer.match('[', 1)) { - this.lexer.ignoreTokens(1); - } - } + private parseArrayFilterExpr(): ArrayFilterExpression { + this.lexer.expect('['); + const filter = this.parseArrayFilter(); + this.lexer.expect(']'); + return { type: SyntaxType.ARRAY_FILTER_EXPR, - filters, + filter, }; } @@ -631,7 +636,7 @@ export class JsonTemplateParser { private shouldSkipPathParsing(expr: Expression): boolean { switch (expr.type) { - case SyntaxType.DEFINTION_EXPR: + case SyntaxType.DEFINITION_EXPR: case SyntaxType.ASSIGNMENT_EXPR: case SyntaxType.SPREAD_EXPR: return true; @@ -701,7 +706,7 @@ export class JsonTemplateParser { this.lexer.expect('{'); while (!this.lexer.match('}')) { if (!this.lexer.matchID()) { - throw new JsonTemplateParserError('Invalid object vars at ' + this.lexer.getContext()); + throw new JsonTemplateParserError('Invalid object vars'); } vars.push(this.lexer.value()); if (!this.lexer.match('}')) { @@ -710,7 +715,7 @@ export class JsonTemplateParser { } this.lexer.expect('}'); if (vars.length === 0) { - throw new JsonTemplateParserError('Empty object vars at ' + this.lexer.getContext()); + throw new JsonTemplateParserError('Empty object vars'); } return vars; } @@ -718,7 +723,7 @@ export class JsonTemplateParser { private parseNormalDefVars(): string[] { const vars: string[] = []; if (!this.lexer.matchID()) { - throw new JsonTemplateParserError('Invalid normal vars at ' + this.lexer.getContext()); + throw new JsonTemplateParserError('Invalid normal vars'); } vars.push(this.lexer.value()); return vars; @@ -731,7 +736,7 @@ export class JsonTemplateParser { this.lexer.expect('='); return { - type: SyntaxType.DEFINTION_EXPR, + type: SyntaxType.DEFINITION_EXPR, value: this.parseBaseExpr(), vars, definition, @@ -1092,14 +1097,12 @@ export class JsonTemplateParser { } private static isArrayFilterExpressionSimple(expr: ArrayFilterExpression): boolean { - for (let filter of expr.filters) { - if (filter.type !== SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - return false; - } - const expr = filter as IndexFilterExpression; - if (expr.indexes.elements.length > 1) { - return false; - } + if (expr.filter.type !== SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + return false; + } + const filter = expr.filter as IndexFilterExpression; + if (filter.indexes.elements.length > 1) { + return false; } return true; } diff --git a/src/translator.ts b/src/translator.ts index 48470c7..7f077a4 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -1,5 +1,12 @@ -import { BINDINGS_CONTEXT_KEY, BINDINGS_PARAM_KEY, DATA_PARAM_KEY, FUNCTION_RESULT_KEY, RESULT_KEY, VARS_PREFIX } from './constants'; -import { JsosTemplateTranslatorError } from './errors'; +import { + BINDINGS_CONTEXT_KEY, + BINDINGS_PARAM_KEY, + DATA_PARAM_KEY, + FUNCTION_RESULT_KEY, + RESULT_KEY, + VARS_PREFIX, +} from './constants'; +import { JsonTemplateTranslatorError } from './errors'; import { binaryOperators } from './operators'; import { ArrayExpression, @@ -131,7 +138,7 @@ export class JsonTemplateTranslator { case SyntaxType.FUNCTION_CALL_EXPR: return this.translateFunctionCallExpr(expr as FunctionCallExpression, dest, ctx); - case SyntaxType.DEFINTION_EXPR: + case SyntaxType.DEFINITION_EXPR: return this.translateDefinitionExpr(expr as DefinitionExpression, dest, ctx); case SyntaxType.ASSIGNMENT_EXPR: @@ -473,7 +480,7 @@ export class JsonTemplateTranslator { return `${isAssignment ? '' : '?'}.${expr.prop?.value}`; } } - private getJSPathArrayIndex( + private getSimplePathArrayIndex( expr: ArrayFilterExpression, ctx: string, code: string[], @@ -482,12 +489,11 @@ export class JsonTemplateTranslator { ): string { const parts: string[] = []; const prefix = isAssignment ? '' : '?.'; - for (const filter of expr.filters as IndexFilterExpression[]) { - const keyVar = this.acquireVar(); - code.push(this.translateExpr(filter.indexes.elements[0], keyVar, ctx)); - parts.push(`${prefix}[${keyVar}]`); - keyVars.push(keyVar); - } + const filter = expr.filter as IndexFilterExpression; + const keyVar = this.acquireVar(); + code.push(this.translateExpr(filter.indexes.elements[0], keyVar, ctx)); + parts.push(`${prefix}[${keyVar}]`); + keyVars.push(keyVar); return parts.join(''); } @@ -505,7 +511,13 @@ export class JsonTemplateTranslator { simplePath.push(this.getSimplePathSelector(part as SelectorExpression, isAssignment)); } else { simplePath.push( - this.getJSPathArrayIndex(part as ArrayFilterExpression, ctx, code, keyVars, isAssignment), + this.getSimplePathArrayIndex( + part as ArrayFilterExpression, + ctx, + code, + keyVars, + isAssignment, + ), ); } } @@ -517,7 +529,12 @@ export class JsonTemplateTranslator { const code: string[] = []; const valueVar = this.acquireVar(); code.push(this.translateExpr(expr.value, valueVar, ctx)); - const assignmentPath = this.translateToSimplePath(expr.path, code, expr.path.root as string, true); + const assignmentPath = this.translateToSimplePath( + expr.path, + code, + expr.path.root as string, + true, + ); JsonTemplateTranslator.ValidateAssignmentPath(assignmentPath); code.push(JsonTemplateTranslator.generateAssignmentCode(assignmentPath, valueVar)); code.push(JsonTemplateTranslator.generateAssignmentCode(dest, valueVar)); @@ -615,14 +632,10 @@ export class JsonTemplateTranslator { private translateArrayFilterExpr(expr: ArrayFilterExpression, dest: string, ctx: string): string { const code: string[] = []; - code.push(JsonTemplateTranslator.generateAssignmentCode(dest, ctx)); - for (const filter of expr.filters) { - if (filter.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - code.push(this.translateIndexFilterExpr(filter as IndexFilterExpression, dest, dest)); - } else { - code.push(this.translateRangeFilterExpr(filter as RangeFilterExpression, dest, dest)); - } - code.push(`if(!${dest}){continue;}`); + if (expr.filter.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + code.push(this.translateIndexFilterExpr(expr.filter as IndexFilterExpression, dest, ctx)); + } else { + code.push(this.translateRangeFilterExpr(expr.filter as RangeFilterExpression, dest, ctx)); } return code.join(''); } @@ -741,8 +754,8 @@ export class JsonTemplateTranslator { } private static ValidateAssignmentPath(path: string) { - if(path.startsWith(BINDINGS_PARAM_KEY) && !path.startsWith(BINDINGS_CONTEXT_KEY)) { - throw new JsosTemplateTranslatorError('Invalid assignment path'); + if (path.startsWith(BINDINGS_PARAM_KEY) && !path.startsWith(BINDINGS_CONTEXT_KEY)) { + throw new JsonTemplateTranslatorError('Invalid assignment path at' + path); } } @@ -782,8 +795,12 @@ export class JsonTemplateTranslator { return `${varName} = ${varName}.length < 2 ? ${varName}[0] : ${varName};`; } - private static covertToArrayValue(varName: string) { - return `${varName} = Array.isArray(${varName}) ? ${varName} : [${varName}];`; + private static covertToArrayValue(varName: string): string { + const code: string[] = []; + code.push(`if(${JsonTemplateTranslator.returnIsNotEmpty(varName)}){`); + code.push(`${varName} = Array.isArray(${varName}) ? ${varName} : [${varName}];`); + code.push('}'); + return code.join(''); } private static generateAssignmentCode(key: string, val: string): string { diff --git a/src/types.ts b/src/types.ts index ddb9b8c..560931a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ -export type Dictionary = Record; - export enum Keyword { FUNCTION = 'function', NEW = 'new', @@ -66,7 +64,7 @@ export enum SyntaxType { RANGE_FILTER_EXPR, OBJECT_FILTER_EXPR, ARRAY_FILTER_EXPR, - DEFINTION_EXPR, + DEFINITION_EXPR, ASSIGNMENT_EXPR, OBJECT_PROP_EXPR, OBJECT_EXPR, @@ -84,7 +82,7 @@ export enum PathType { } export interface EngineOptions { - compileTimeBindings?: Dictionary; + compileTimeBindings?: Record; defaultPathType?: PathType; } @@ -179,7 +177,7 @@ export interface ObjectFilterExpression extends Expression { } export interface ArrayFilterExpression extends Expression { - filters: (RangeFilterExpression | IndexFilterExpression)[]; + filter: RangeFilterExpression | IndexFilterExpression; } export interface LiteralExpression extends Expression { value: string | number | boolean | null | undefined; diff --git a/test/e2e.test.ts b/test/e2e.test.ts index b9a9e7b..ad1cb87 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -1,7 +1,7 @@ import { readdirSync } from 'fs'; import { join } from 'path'; import { Command } from 'commander'; -import { SceanarioUtils } from './utils'; +import { ScenarioUtils } from './utils'; const rootDirName = 'scenarios'; const command = new Command(); @@ -18,12 +18,12 @@ describe('Scenarios tests', () => { scenarios.forEach((scenarioName) => { describe(`${scenarioName}`, () => { const scenarioDir = join(__dirname, rootDirName, scenarioName); - const sceanarios = SceanarioUtils.extractScenarios(scenarioDir); - sceanarios.forEach((scenario, index) => { + const scenarios = ScenarioUtils.extractScenarios(scenarioDir); + scenarios.forEach((scenario, index) => { it(`Scenario ${index}: ${scenario.templatePath || 'template.jt'}`, async () => { try { - const templateEngine = SceanarioUtils.createTemplateEngine(scenarioDir, scenario); - const result = await SceanarioUtils.evaluateScenario(templateEngine, scenario); + const templateEngine = ScenarioUtils.createTemplateEngine(scenarioDir, scenario); + const result = await ScenarioUtils.evaluateScenario(templateEngine, scenario); expect(result).toEqual(scenario.output); } catch (error: any) { expect(error.message).toContain(scenario.error); diff --git a/test/scenarios/arrays/data.ts b/test/scenarios/arrays/data.ts index b1cef87..ce32f63 100644 --- a/test/scenarios/arrays/data.ts +++ b/test/scenarios/arrays/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: [1, 2, 3, ['string1', 20.02], ['string2', 'string3', 'aa"a', true, false], 2], }, diff --git a/test/scenarios/assignments/data.ts b/test/scenarios/assignments/data.ts index 08a2342..9d712eb 100644 --- a/test/scenarios/assignments/data.ts +++ b/test/scenarios/assignments/data.ts @@ -1,9 +1,9 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { bindings: { - context: {} + context: {}, }, output: { a: { diff --git a/test/scenarios/bad_templates/data.ts b/test/scenarios/bad_templates/data.ts index 31c5a7d..97e98a3 100644 --- a/test/scenarios/bad_templates/data.ts +++ b/test/scenarios/bad_templates/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { templatePath: 'bad_array_coalese_expr.jt', error: 'expected at least 1 expression', diff --git a/test/scenarios/bindings/data.ts b/test/scenarios/bindings/data.ts index bb4d80e..bcb49fb 100644 --- a/test/scenarios/bindings/data.ts +++ b/test/scenarios/bindings/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { templatePath: 'async.jt', bindings: { diff --git a/test/scenarios/block/data.ts b/test/scenarios/block/data.ts index 6300f13..7f7391f 100644 --- a/test/scenarios/block/data.ts +++ b/test/scenarios/block/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: 15, }, diff --git a/test/scenarios/comments/data.ts b/test/scenarios/comments/data.ts index a4bafda..b080da8 100644 --- a/test/scenarios/comments/data.ts +++ b/test/scenarios/comments/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: ['////', '/*** /// */'], }, diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts index 17e0e4e..68b4186 100644 --- a/test/scenarios/comparisons/data.ts +++ b/test/scenarios/comparisons/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: [ true, diff --git a/test/scenarios/compile_time_expressions/data.ts b/test/scenarios/compile_time_expressions/data.ts index e42ac5e..4dd0b57 100644 --- a/test/scenarios/compile_time_expressions/data.ts +++ b/test/scenarios/compile_time_expressions/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { options: { compileTimeBindings: { a: 1, b: 'string', c: { c: 1.02 }, d: [null, true, false] } }, output: [1, 'string', { c: 1.02 }, [null, true, false]], diff --git a/test/scenarios/conditions/data.ts b/test/scenarios/conditions/data.ts index 4133bd3..2287550 100644 --- a/test/scenarios/conditions/data.ts +++ b/test/scenarios/conditions/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { templatePath: 'if-then.jt', input: { diff --git a/test/scenarios/context_variables/data.ts b/test/scenarios/context_variables/data.ts index 9f88019..3d739cf 100644 --- a/test/scenarios/context_variables/data.ts +++ b/test/scenarios/context_variables/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { templatePath: 'filter.jt', input: [[{ a: 1 }], [{ a: 2 }, { a: 3 }], [{ a: 4 }, { a: 5 }, { a: 6 }]], diff --git a/test/scenarios/filters/array_filters.jt b/test/scenarios/filters/array_filters.jt index cef006d..6a2369d 100644 --- a/test/scenarios/filters/array_filters.jt +++ b/test/scenarios/filters/array_filters.jt @@ -1,2 +1,5 @@ -let a = [1, 2, 3, 4, 5]; -[a[2:], a[:3], a[3:5], a[...[1, 3]].[0, 1], a[-1], a[:-1], a[-2:]] \ No newline at end of file +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"] +] \ No newline at end of file diff --git a/test/scenarios/filters/data.ts b/test/scenarios/filters/data.ts index 933567e..483eb91 100644 --- a/test/scenarios/filters/data.ts +++ b/test/scenarios/filters/data.ts @@ -1,6 +1,36 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ + { + templatePath: 'array_filters.jt', + output: [ + [ + 3, + 4, + 5, + { + a: 1, + b: 2, + c: 3, + }, + ], + [1, 2, 3], + [4, 5], + [2, 4], + 5, + 2, + [1, 2, 3, 4], + [ + 5, + { + a: 1, + b: 2, + c: 3, + }, + ], + [1, 2], + ], + }, { templatePath: 'object_filters.jt', output: { @@ -8,10 +38,6 @@ export const data: Sceanario[] = [ b: 2, }, }, - { - templatePath: 'array_filters.jt', - output: [[3, 4, 5], [1, 2, 3], [4, 5], [2, 4], 5, [1, 2, 3, 4], [4, 5]], - }, { templatePath: 'object_indexes.jt', output: { diff --git a/test/scenarios/functions/data.ts b/test/scenarios/functions/data.ts index 1686f63..6174146 100644 --- a/test/scenarios/functions/data.ts +++ b/test/scenarios/functions/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { templatePath: 'function_calls.jt', output: ['abc', undefined, undefined], diff --git a/test/scenarios/inputs/data.ts b/test/scenarios/inputs/data.ts index 4e8f562..bf5fdcb 100644 --- a/test/scenarios/inputs/data.ts +++ b/test/scenarios/inputs/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { input: { a: 10, diff --git a/test/scenarios/logics/data.ts b/test/scenarios/logics/data.ts index 53e9c07..a1c6d0b 100644 --- a/test/scenarios/logics/data.ts +++ b/test/scenarios/logics/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: [3, 0, 2, 3, 0, 3, true, true], }, diff --git a/test/scenarios/math/data.ts b/test/scenarios/math/data.ts index f7464b6..281a3d0 100644 --- a/test/scenarios/math/data.ts +++ b/test/scenarios/math/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: [12, 8, 20, 5, 100, 0, 40, 2], }, diff --git a/test/scenarios/objects/data.ts b/test/scenarios/objects/data.ts index e79ea4d..48f77c1 100644 --- a/test/scenarios/objects/data.ts +++ b/test/scenarios/objects/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: { a: 1, diff --git a/test/scenarios/paths/block.jt b/test/scenarios/paths/block.jt new file mode 100644 index 0000000..45c6aa2 --- /dev/null +++ b/test/scenarios/paths/block.jt @@ -0,0 +1,7 @@ +[ + .({ + a: .a + 1, + b: .b + 2 + }), + .([.a + 1, .b + 2]) +] \ No newline at end of file diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts index 82a8f7e..f931c65 100644 --- a/test/scenarios/paths/data.ts +++ b/test/scenarios/paths/data.ts @@ -1,6 +1,115 @@ -import { Sceanario } from '../../types'; +import { PathType } from '../../../src'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ + { + templatePath: 'block.jt', + input: { + a: 1, b :1 + }, + output: [{a: 2, b: 3}, [2, 3]] + }, + { + templatePath: 'options.jt', + options: { + defaultPathType: PathType.RICH, + }, + input: { + a: { + b: [{ c: [{ d: 1 }, { d: 2 }] }], + }, + }, + output: [ + [ + { + d: 1, + }, + { + d: 2, + }, + ], + [ + { + d: 1, + }, + { + d: 2, + }, + ], + undefined, + { + d: 2, + }, + ], + }, + { + templatePath: 'options.jt', + options: { + defaultPathType: PathType.SIMPLE, + }, + input: { + a: { + b: [{ c: [{ d: 1 }, { d: 2 }] }], + }, + }, + output: [ + undefined, + [ + { + d: 1, + }, + { + d: 2, + }, + ], + undefined, + { + d: 2, + }, + ], + }, + { + templatePath: 'options.jt', + options: { + defaultPathType: PathType.SIMPLE, + }, + input: { + a: { + b: { + c: [{ d: 1 }, { d: 2 }], + }, + }, + }, + output: [ + [ + { + d: 1, + }, + { + d: 2, + }, + ], + [ + { + d: 1, + }, + { + d: 2, + }, + ], + [ + { + d: 1, + }, + { + d: 2, + }, + ], + { + d: 2, + }, + ], + }, { templatePath: 'rich_path.jt', input: [ diff --git a/test/scenarios/paths/options.jt b/test/scenarios/paths/options.jt new file mode 100644 index 0000000..afcab03 --- /dev/null +++ b/test/scenarios/paths/options.jt @@ -0,0 +1,10 @@ +[ + // Based path options it will be treat as either simple or rich path + .a.b.c, + // this is always treated as rich path + ~r .a.b.c, + // this is always treated as simple path + ~s .a.b.c, + // this is always treated as rich path as it uses rich features like + .a.b.c{.d > 1} +] \ No newline at end of file diff --git a/test/scenarios/selectors/data.ts b/test/scenarios/selectors/data.ts index c5e5ad3..26cab2c 100644 --- a/test/scenarios/selectors/data.ts +++ b/test/scenarios/selectors/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { input: { a: 10, diff --git a/test/scenarios/statements/data.ts b/test/scenarios/statements/data.ts index 6e82a3b..1a71ac2 100644 --- a/test/scenarios/statements/data.ts +++ b/test/scenarios/statements/data.ts @@ -1,6 +1,6 @@ -import { Sceanario } from '../../types'; +import { Scenario } from '../../types'; -export const data: Sceanario[] = [ +export const data: Scenario[] = [ { output: [15], }, diff --git a/test/test_scenario.ts b/test/test_scenario.ts index 2fb0e50..9d6e1ee 100644 --- a/test/test_scenario.ts +++ b/test/test_scenario.ts @@ -1,8 +1,8 @@ import { join } from 'path'; import { deepEqual } from 'assert'; import { Command } from 'commander'; -import { Sceanario } from './types'; -import { SceanarioUtils } from './utils'; +import { Scenario } from './types'; +import { ScenarioUtils } from './utils'; const command = new Command(); command @@ -18,15 +18,15 @@ const index = +(opts.index || 0); async function createAndEvaluateTemplate() { try { const scenarioDir = join(__dirname, 'scenarios', scenarioName); - const scenarios: Sceanario[] = SceanarioUtils.extractScenarios(scenarioDir); - const scenario: Sceanario = scenarios[index] || scenarios[0]; + const scenarios: Scenario[] = ScenarioUtils.extractScenarios(scenarioDir); + const scenario: Scenario = scenarios[index] || scenarios[0]; console.log( `Executing scenario: ${scenarioName}, test: ${index}, template: ${ scenario.templatePath || 'template.jt' }`, ); - const templateEngine = SceanarioUtils.createTemplateEngine(scenarioDir, scenario); - const result = await SceanarioUtils.evaluateScenario(templateEngine, scenario); + const templateEngine = ScenarioUtils.createTemplateEngine(scenarioDir, scenario); + const result = await ScenarioUtils.evaluateScenario(templateEngine, scenario); console.log('Actual result', JSON.stringify(result, null, 2)); console.log('Expected result', JSON.stringify(scenario.output, null, 2)); deepEqual(result, scenario.output, 'matching failed'); diff --git a/test/types.ts b/test/types.ts index 1940a95..00c3b73 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,6 +1,6 @@ -import { Dictionary, EngineOptions } from '../src'; +import { EngineOptions } from '../src'; -export type Sceanario = { +export type Scenario = { description?: string; input?: any; templatePath?: string; diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts index a8c38e5..12a2704 100644 --- a/test/utils/scenario.ts +++ b/test/utils/scenario.ts @@ -1,21 +1,21 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { JsonTemplateEngine } from '../../src'; -import { Sceanario } from '../types'; +import { Scenario } from '../types'; -export class SceanarioUtils { - static createTemplateEngine(scenarioDir: string, sceanario: Sceanario): JsonTemplateEngine { +export class ScenarioUtils { + static createTemplateEngine(scenarioDir: string, sceanario: Scenario): JsonTemplateEngine { const templatePath = join(scenarioDir, sceanario.templatePath || 'template.jt'); const template = readFileSync(templatePath, 'utf-8'); return JsonTemplateEngine.create(template, sceanario.options); } - static evaluateScenario(templateEngine: JsonTemplateEngine, sceanario: Sceanario): any { + static evaluateScenario(templateEngine: JsonTemplateEngine, sceanario: Scenario): any { return templateEngine.evaluate(sceanario.input, sceanario.bindings); } - static extractScenarios(scenarioDir: string): Sceanario[] { + static extractScenarios(scenarioDir: string): Scenario[] { const { data } = require(join(scenarioDir, 'data.ts')); - return data as Sceanario[]; + return data as Scenario[]; } }