From 26d5fd814810a4972c503221d5bf9bca07720127 Mon Sep 17 00:00:00 2001 From: Dilip Kola <33080863+koladilip@users.noreply.github.com> Date: Fri, 18 Nov 2022 12:33:11 +0530 Subject: [PATCH] feat: add several features and tests (#5) * refactor(parser): create lexer to hold lexing logic Create separate class for lexing Refactor language spec for variables and functions. Add support for comments. * refactor: add support for more function features Add support for spread operators. Add support for calling prototype functions. * refactor: Fix filters and add basic support for context vars Add support for object index filters. Add basic support for context vars. * Add support for complex assignment path * Add support for object vars * Add support for lambda functions * Add support for to array * Fix issues with function calls * Fix function call expr * feat: add support for conditional expressions * Add support for context variables * Fix formating issues * Add test framework * Add test scenarios * add tests for all use cases * Update manual test files * rerefactor binary operators * Refactor object filters * Improve the performance of subpaths * Avoid using hasOwnProperty as it is effecting performance * Add example of using function in object filter * Use of hasOwnProperty is required Co-authored-by: saikumarrs --- .vscode/launch.json | 46 +- package.json | 4 +- src/constants.ts | 2 + src/engine.ts | 11 +- src/errors.ts | 10 +- src/lexer.ts | 190 ++- src/operators.ts | 183 +-- src/parser.ts | 961 ++++++++----- src/translator.ts | 1237 ++++++++--------- src/types.ts | 129 +- src/utils.ts | 31 + test/e2e.test.ts | 35 + test/main.test.ts | 5 - test/scenarios/arrays/data.ts | 7 + test/scenarios/arrays/template.jt | 6 + test/scenarios/assignments/data.ts | 12 + test/scenarios/assignments/template.jt | 10 + .../bad_templates/bad_async_usage.jt | 1 + .../bad_templates/bad_context_var.jt | 1 + .../bad_templates/bad_function_params.jt | 1 + .../bad_templates/bad_function_rest_param.jt | 1 + test/scenarios/bad_templates/bad_number.jt | 1 + test/scenarios/bad_templates/bad_string.jt | 1 + test/scenarios/bad_templates/data.ts | 93 ++ .../empty_object_vars_for_definition.jt | 1 + .../bad_templates/incomplete_statement.jt | 1 + .../invalid_new_function_call.jt | 1 + .../invalid_object_vars_for_definition.jt | 1 + .../invalid_token_after_function_def.jt | 1 + .../invalid_variable_assignment1.jt | 2 + .../invalid_variable_assignment2.jt | 2 + .../invalid_variable_assignment3.jt | 2 + .../invalid_variable_assignment4.jt | 1 + .../invalid_variable_assignment5.jt | 1 + .../invalid_variable_definition.jt | 1 + .../object_with_invalid_closing.jt | 1 + .../bad_templates/object_with_invalid_key.jt | 1 + test/scenarios/bad_templates/reserved_id.jt | 1 + test/scenarios/bad_templates/unknown_token.jt | 1 + .../bad_templates/unsupported_assignment.jt | 2 + test/scenarios/bindings/async.jt | 4 + test/scenarios/bindings/data.ts | 21 + test/scenarios/bindings/template.jt | 1 + test/scenarios/block/data.ts | 7 + test/scenarios/block/template.jt | 14 + test/scenarios/comparisons/data.ts | 31 + test/scenarios/comparisons/template.jt | 24 + test/scenarios/conditions/data.ts | 42 + test/scenarios/conditions/if-then.jt | 3 + test/scenarios/conditions/template.jt | 4 + test/scenarios/filters/array_filters.jt | 2 + test/scenarios/filters/data.ts | 24 + test/scenarios/filters/object_filters.jt | 5 + test/scenarios/filters/object_indexes.jt | 7 + test/scenarios/functions/data.ts | 28 + test/scenarios/functions/js_date_function.jt | 2 + test/scenarios/functions/new_operator.jt | 13 + test/scenarios/functions/parent_scope_vars.jt | 5 + test/scenarios/functions/template.jt | 9 + test/scenarios/inputs/data.ts | 14 + test/scenarios/inputs/template.jt | 1 + test/scenarios/logics/data.ts | 7 + test/scenarios/logics/template.jt | 5 + test/scenarios/math/data.ts | 7 + test/scenarios/math/template.jt | 1 + test/scenarios/objects/data.ts | 11 + test/scenarios/objects/template.jt | 17 + test/scenarios/paths/data.ts | 20 + test/scenarios/paths/template.jt | 7 + test/scenarios/selectors/context_variables.jt | 6 + test/scenarios/selectors/data.ts | 79 ++ test/scenarios/selectors/template.jt | 1 + test/scenarios/selectors/wild_cards.jt | 1 + test/scenarios/statements/data.ts | 7 + test/scenarios/statements/template.jt | 14 + test/test_engine.ts | 247 ++++ test/test_extractor.ts | 62 - test/test_scenario.ts | 35 + test/types.ts | 10 + test/utils/index.ts | 1 + test/utils/scenario.ts | 21 + 81 files changed, 2516 insertions(+), 1302 deletions(-) create mode 100644 src/utils.ts create mode 100644 test/e2e.test.ts delete mode 100644 test/main.test.ts create mode 100644 test/scenarios/arrays/data.ts create mode 100644 test/scenarios/arrays/template.jt create mode 100644 test/scenarios/assignments/data.ts create mode 100644 test/scenarios/assignments/template.jt create mode 100644 test/scenarios/bad_templates/bad_async_usage.jt create mode 100644 test/scenarios/bad_templates/bad_context_var.jt create mode 100644 test/scenarios/bad_templates/bad_function_params.jt create mode 100644 test/scenarios/bad_templates/bad_function_rest_param.jt create mode 100644 test/scenarios/bad_templates/bad_number.jt create mode 100644 test/scenarios/bad_templates/bad_string.jt create mode 100644 test/scenarios/bad_templates/data.ts create mode 100644 test/scenarios/bad_templates/empty_object_vars_for_definition.jt create mode 100644 test/scenarios/bad_templates/incomplete_statement.jt create mode 100644 test/scenarios/bad_templates/invalid_new_function_call.jt create mode 100644 test/scenarios/bad_templates/invalid_object_vars_for_definition.jt create mode 100644 test/scenarios/bad_templates/invalid_token_after_function_def.jt create mode 100644 test/scenarios/bad_templates/invalid_variable_assignment1.jt create mode 100644 test/scenarios/bad_templates/invalid_variable_assignment2.jt create mode 100644 test/scenarios/bad_templates/invalid_variable_assignment3.jt create mode 100644 test/scenarios/bad_templates/invalid_variable_assignment4.jt create mode 100644 test/scenarios/bad_templates/invalid_variable_assignment5.jt create mode 100644 test/scenarios/bad_templates/invalid_variable_definition.jt create mode 100644 test/scenarios/bad_templates/object_with_invalid_closing.jt create mode 100644 test/scenarios/bad_templates/object_with_invalid_key.jt create mode 100644 test/scenarios/bad_templates/reserved_id.jt create mode 100644 test/scenarios/bad_templates/unknown_token.jt create mode 100644 test/scenarios/bad_templates/unsupported_assignment.jt create mode 100644 test/scenarios/bindings/async.jt create mode 100644 test/scenarios/bindings/data.ts create mode 100644 test/scenarios/bindings/template.jt create mode 100644 test/scenarios/block/data.ts create mode 100644 test/scenarios/block/template.jt create mode 100644 test/scenarios/comparisons/data.ts create mode 100644 test/scenarios/comparisons/template.jt create mode 100644 test/scenarios/conditions/data.ts create mode 100644 test/scenarios/conditions/if-then.jt create mode 100644 test/scenarios/conditions/template.jt create mode 100644 test/scenarios/filters/array_filters.jt create mode 100644 test/scenarios/filters/data.ts create mode 100644 test/scenarios/filters/object_filters.jt create mode 100644 test/scenarios/filters/object_indexes.jt create mode 100644 test/scenarios/functions/data.ts create mode 100644 test/scenarios/functions/js_date_function.jt create mode 100644 test/scenarios/functions/new_operator.jt create mode 100644 test/scenarios/functions/parent_scope_vars.jt create mode 100644 test/scenarios/functions/template.jt create mode 100644 test/scenarios/inputs/data.ts create mode 100644 test/scenarios/inputs/template.jt create mode 100644 test/scenarios/logics/data.ts create mode 100644 test/scenarios/logics/template.jt create mode 100644 test/scenarios/math/data.ts create mode 100644 test/scenarios/math/template.jt create mode 100644 test/scenarios/objects/data.ts create mode 100644 test/scenarios/objects/template.jt create mode 100644 test/scenarios/paths/data.ts create mode 100644 test/scenarios/paths/template.jt create mode 100644 test/scenarios/selectors/context_variables.jt create mode 100644 test/scenarios/selectors/data.ts create mode 100644 test/scenarios/selectors/template.jt create mode 100644 test/scenarios/selectors/wild_cards.jt create mode 100644 test/scenarios/statements/data.ts create mode 100644 test/scenarios/statements/template.jt create mode 100644 test/test_engine.ts delete mode 100644 test/test_extractor.ts create mode 100644 test/test_scenario.ts create mode 100644 test/types.ts create mode 100644 test/utils/index.ts create mode 100644 test/utils/scenario.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index b911268..846362f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,8 +13,30 @@ "type": "node" }, { - "name": "Test Extractor", - "program": "${workspaceFolder}/test/test_extractor.ts", + "name": "Test Scenario", + "program": "${workspaceFolder}/test/test_scenario.ts", + "request": "launch", + "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], + "skipFiles": ["/**"], + "args": ["--scenario=${input:scenario}", "--index=${input:index}"], + "type": "node" + }, + { + "runtimeExecutable": "/usr/local/bin/node", + "type": "node", + "request": "launch", + "name": "Jest Scenarios", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["test/e2e.test.ts", "--config", "jest.config.ts", "--scenarios=${input:scenarios}"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + }, + { + "name": "Test Engine", + "program": "${workspaceFolder}/test/test_engine.ts", "request": "launch", "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], "skipFiles": ["/**"], @@ -33,5 +55,25 @@ "program": "${workspaceFolder}/node_modules/jest/bin/jest" } } + ], + "inputs": [ + { + "id": "scenarios", + "type": "promptString", + "description": "Enter Scenarios", + "default": "all" + }, + { + "id": "scenario", + "type": "promptString", + "description": "Enter Scenario", + "default": "assignments" + }, + { + "id": "index", + "type": "promptString", + "description": "Enter test index", + "default": "0" + } ] } diff --git a/package.json b/package.json index 8800bc6..a238185 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,9 @@ "lint:fix": "eslint . --fix", "lint:check": "eslint . || exit 1", "format": "prettier --write '**/*.ts' '**/*.js' '**/*.json'", - "prepare": "husky install" + "prepare": "husky install", + "jest:scenarios": "jest e2e.test.ts --verbose", + "test:scenario": "ts-node test/test_scenario.ts" }, "engines": { "node": ">=14.15.0 <15", diff --git a/src/constants.ts b/src/constants.ts index b92f63c..11af7c6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ export const VARS_PREFIX = '___'; export const DATA_PARAM_KEY = '___d'; export const BINDINGS_PARAM_KEY = '___b'; +export const RESULT_KEY = '___r'; +export const FUNCTION_RESULT_KEY = '___f'; diff --git a/src/engine.ts b/src/engine.ts index c073224..a70add8 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -2,20 +2,23 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; import { JsonTemplateTranslator } from './translator'; +import { CommonUtils } from './utils'; export class JsonTemplateEngine { private readonly fn: Function; constructor(template: string) { this.fn = JsonTemplateEngine.compile(template); } - private static compile(template: string) { + + private static compile(template: string): Function { const lexer = new JsonTemplateLexer(template); const parser = new JsonTemplateParser(lexer); const translator = new JsonTemplateTranslator(parser.parse()); - return new Function(DATA_PARAM_KEY, BINDINGS_PARAM_KEY, translator.translate()); + const code = translator.translate(); + return CommonUtils.CreateAsyncFunction(DATA_PARAM_KEY, BINDINGS_PARAM_KEY, code); } - evaluate(data: any, bindings: any = {}) { - return this.fn(data, bindings); + evaluate(data: any, bindings: any = {}): Promise { + return this.fn(data || {}, bindings); } } diff --git a/src/errors.ts b/src/errors.ts index c1b785a..6db3c40 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,8 +1,6 @@ export class JsonTemplateLexerError extends Error { - readonly column: number; - constructor(message: string, column: number) { + constructor(message: string) { super(message); - this.column = column; } } @@ -11,3 +9,9 @@ export class JsosTemplateParserError extends Error { super(message); } } + +export class JsosTemplateTranslatorError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/lexer.ts b/src/lexer.ts index 11a0006..e0bc57f 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -1,10 +1,11 @@ -import { VARS_PREFIX } from './constants'; +import { BINDINGS_PARAM_KEY, VARS_PREFIX } from './constants'; import { JsonTemplateLexerError } from './errors'; -import { Dictionary, Keyword, Token, TokenType } from './types'; +import { Keyword, Token, TokenType } from './types'; const MESSAGES = { RESERVED_ID: 'Reserved ID pattern "%0"', UNEXP_TOKEN: 'Unexpected token "%0"', + UNKNOWN_TOKEN: 'Unknown token', UNEXP_EOT: 'Unexpected end of template', }; @@ -23,7 +24,15 @@ export class JsonTemplateLexer { .split(''); } - match(value: string, steps = 0): boolean { + init() { + this.idx = 0; + this.buf = []; + } + + match(value?: string, steps = 0): boolean { + if (!value) { + return false; + } let token = this.lookahead(steps); return token.type === TokenType.PUNCT && token.value === value; } @@ -33,18 +42,27 @@ export class JsonTemplateLexer { } matchPath(): boolean { - const token = this.lookahead(); - return this.matchSelector() || - this.matchID() || - this.match('[') || - this.match('^'); + return this.matchPathSelector() || this.matchID(); } - matchSelector(): boolean { + matchSpread(): boolean { + return this.match('...'); + } + + matchPathPartSelector(): boolean { let token = this.lookahead(); if (token.type === TokenType.PUNCT) { let value = token.value; - return value === '.' || value === '..' || value === '...'; + return value === '.' || value === '..'; + } + return false; + } + + matchPathSelector(): boolean { + let token = this.lookahead(); + if (token.type === TokenType.PUNCT) { + let value = token.value; + return value === '.' || value === '..' || value === '^'; } return false; @@ -64,52 +82,50 @@ export class JsonTemplateLexer { } private static isOperator(id: string): boolean { - return Object.values(Keyword).some(op => op.toString() === id); + return Object.values(Keyword).some((op) => op.toString() === id); } - matchOperator(op: string): boolean { + matchKeyword(op: string): boolean { let token = this.lookahead(); - return token.type === TokenType.OPERATOR && token.value === op; + return token.type === TokenType.KEYWORD && token.value === op; + } + + matchIN(): boolean { + return this.matchKeyword(Keyword.IN); } matchFunction(): boolean { - return this.matchOperator(Keyword.FUNCTION); + return this.matchKeyword(Keyword.FUNCTION); } matchNew(): boolean { - return this.matchOperator(Keyword.NEW); + return this.matchKeyword(Keyword.NEW); } - matchReturn(): boolean { - return this.matchOperator(Keyword.RETURN); + matchTypeOf(): boolean { + return this.matchKeyword(Keyword.TYPEOF); } - matchTypeOf(): boolean { - return this.matchOperator(Keyword.TYPEOF); + matchAwait(): boolean { + return this.matchKeyword(Keyword.AWAIT); } - matchDefinition(): boolean { - return this.matchOperator(Keyword.LET) || this.matchOperator(Keyword.CONST); + matchAsync(): boolean { + return this.matchKeyword(Keyword.ASYNC); } - expectOperator(op: string) { - let token = this.lex(); - if (token.type !== TokenType.OPERATOR || token.value !== op) { - this.throwUnexpected(token); - } + matchLambda(): boolean { + return this.matchKeyword(Keyword.LAMBDA); } - expectTokenType(tokenType: TokenType) { - let token = this.lex(); - if (token.type !== tokenType) { - this.throwUnexpected(token); - } + matchDefinition(): boolean { + return this.matchKeyword(Keyword.LET) || this.matchKeyword(Keyword.CONST); } expect(value) { let token = this.lex(); if (token.type !== TokenType.PUNCT || token.value !== value) { - this.throwUnexpected(token); + this.throwUnexpectedToken(token); } } @@ -147,8 +163,11 @@ export class JsonTemplateLexer { if (token) { return token; } + this.throwError(MESSAGES.UNKNOWN_TOKEN); + } - return { range: [this.idx, this.idx], type: TokenType.UNKNOWN, value: undefined }; + value(): any { + return this.lex().value; } lex(): Token { @@ -164,29 +183,46 @@ export class JsonTemplateLexer { return this.advance(); } + nextChar(): string { + return this.codeChars[this.idx]; + } + + ignoreNextChar() { + this.idx++; + } + + matchNextChar(ch: string): boolean { + return this.nextChar() === ch; + } + static isLiteralToken(token: Token) { return ( token.type === TokenType.BOOL || token.type === TokenType.NUM || token.type === TokenType.STR || - token.type === TokenType.NULL + token.type === TokenType.NULL || + token.type === TokenType.UNDEFINED ); } - throwUnexpected(token?: Token): never { + throwUnexpectedToken(token?: Token): never { token = token || this.lookahead(); if (token.type === TokenType.EOT) { - JsonTemplateLexer.throwError(token, MESSAGES.UNEXP_EOT); + this.throwError(MESSAGES.UNEXP_EOT); } - JsonTemplateLexer.throwError(token, MESSAGES.UNEXP_TOKEN, token.value); + this.throwError(MESSAGES.UNEXP_TOKEN, token.value); } - static throwError(token: Token, messageFormat: string, ...args): never { + 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] || ''; + return args[idx]; }); - throw new JsonTemplateLexerError(msg, token.range[0]); + throw new JsonTemplateLexerError(msg + ' at ' + this.getContext(15)); } private static isDigit(ch: string) { @@ -198,18 +234,16 @@ export class JsonTemplateLexer { } private static isIdStart(ch: string) { - return ( - ch === '$' || ch === '@' || ch === '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') - ); + return ch === '$' || ch === '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } private static isIdPart(ch: string) { return this.isIdStart(ch) || (ch >= '0' && ch <= '9'); } - private static validateIDToken(token: Token) { - if (token.value.startsWith(VARS_PREFIX)) { - this.throwError(token, MESSAGES.RESERVED_ID, token.value); + private validateID(id: string) { + if (id.startsWith(VARS_PREFIX)) { + this.throwError(MESSAGES.RESERVED_ID, id); } } @@ -233,7 +267,7 @@ export class JsonTemplateLexer { if (JsonTemplateLexer.isOperator(id)) { return { - type: TokenType.OPERATOR, + type: TokenType.KEYWORD, value: id, range: [start, this.idx], }; @@ -255,19 +289,29 @@ export class JsonTemplateLexer { range: [start, this.idx], }; + case 'undefined': + return { + type: TokenType.UNDEFINED, + value: undefined, + range: [start, this.idx], + }; + default: - const token: Token = { + this.validateID(id); + return { type: TokenType.ID, - value: id, + value: id.replace(/^\$/, `${BINDINGS_PARAM_KEY}`), range: [start, this.idx], }; - JsonTemplateLexer.validateIDToken(token); - return token; } } private scanString(): Token | undefined { - if (this.codeChars[this.idx] !== '"' && this.codeChars[this.idx] !== "'") { + if ( + this.codeChars[this.idx] !== '"' && + this.codeChars[this.idx] !== '`' && + this.codeChars[this.idx] !== "'" + ) { return; } @@ -281,7 +325,7 @@ export class JsonTemplateLexer { ch = this.codeChars[this.idx++]; if (ch === '\\') { ch = this.codeChars[this.idx++]; - } else if ((ch === '"' || ch === "'") && ch === orig) { + } else if ('\'"`'.includes(ch) && ch === orig) { eosFound = true; break; } @@ -295,6 +339,7 @@ export class JsonTemplateLexer { range: [start, this.idx], }; } + this.throwUnexpectedToken(); } private scanNumeric(): Token | undefined { @@ -308,7 +353,7 @@ export class JsonTemplateLexer { ch = this.codeChars[this.idx]; if (ch === '.') { if (isFloat) { - return; + this.throwUnexpectedToken(); } isFloat = true; } else if (!JsonTemplateLexer.isDigit(ch)) { @@ -343,6 +388,13 @@ export class JsonTemplateLexer { value: '...', range: [start, this.idx], }; + } else if (ch2 === '.') { + this.idx = this.idx + 2; + return { + type: TokenType.PUNCT, + value: '..', + range: [start, this.idx], + }; } else { if (JsonTemplateLexer.isDigit(ch2)) { return; @@ -354,11 +406,13 @@ export class JsonTemplateLexer { }; } } + private scanPunctuatorForEquality(): Token | undefined { let start = this.idx, ch1 = this.codeChars[this.idx], ch2 = this.codeChars[this.idx + 1], ch3 = this.codeChars[this.idx + 2]; + if (ch2 === '=') { if (ch3 === '=') { if ('=!^$*'.indexOf(ch1) >= 0) { @@ -409,7 +463,7 @@ export class JsonTemplateLexer { ch1 = this.codeChars[this.idx], ch2 = this.codeChars[this.idx + 1]; - if (ch1 === ch2 && '|&*.=><'.includes(ch1)) { + if (ch1 === ch2 && '|&*.=>?<'.includes(ch1)) { this.idx += 2; return { type: TokenType.PUNCT, @@ -423,7 +477,7 @@ export class JsonTemplateLexer { let start = this.idx, ch1 = this.codeChars[this.idx]; - if (',;:{}()[]^+-*/%!><|='.includes(ch1)) { + if (',;:{}()[]^+-*/%!><|=@~#?\n'.includes(ch1)) { return { type: TokenType.PUNCT, value: ch1, @@ -432,20 +486,28 @@ export class JsonTemplateLexer { } } + private scanPunctuatorForQuestionMarks(): Token | undefined { + let start = this.idx, + ch1 = this.codeChars[this.idx], + ch2 = this.codeChars[this.idx + 1]; + + if (ch1 === '?' && JsonTemplateLexer.isDigit(ch2)) { + this.idx += 2; + return { + type: TokenType.LAMBDA_ARG, + value: Number(ch2), + range: [start, this.idx], + }; + } + } + private scanPunctuator(): Token | undefined { return ( this.scanPunctuatorForDots() || + this.scanPunctuatorForQuestionMarks() || this.scanPunctuatorForEquality() || this.scanPunctuatorForRepeatedTokens() || this.scanSingleCharPunctuators() ); } - - validateNoMoreTokensLeft() { - const token = this.lex(); - - if (token.type !== TokenType.EOT) { - this.throwUnexpected(token); - } - } } diff --git a/src/operators.ts b/src/operators.ts index 26e0e08..375bae5 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -1,218 +1,143 @@ -function startsWithStrict(val1, val2) { - return [ - 'typeof ', - val1, - '=== "string" && typeof ', - val2, - '=== "string" &&', - val1, - '.indexOf(', - val2, - ') === 0', - ].join(''); +function startsWithStrict(val1, val2): string { + return `(typeof ${val1} === 'string' && ${val1}.startsWith(${val2}))`; } -function startsWith(val1, val2) { - return [ - val1, - '!= null &&', - val2, - '!= null &&', - val1, - '.toString().toLowerCase().indexOf(', - val2, - '.toString().toLowerCase()) === 0', - ].join(''); +function startsWith(val1, val2): string { + const code: string[] = []; + code.push(`(typeof ${val1} === 'string' && `); + code.push(`typeof ${val2} === 'string' && `); + code.push(`${val1}.toLowerCase().startsWith(${val2}.toLowerCase()))`); + return code.join(''); } -function endsWithStrict(val1, val2) { - return [ - 'typeof ', - val1, - '=== "string" && typeof ', - val2, - '=== "string" &&', - val1, - '.length >=', - val2, - '.length &&', - val1, - '.lastIndexOf(', - val2, - ') ===', - val1, - '.length -', - val2, - '.length', - ].join(''); +function endsWithStrict(val1, val2): string { + return `(typeof ${val1} === 'string' && ${val1}.endsWith(${val2}))`; } -function endsWith(val1, val2) { - return [ - val1, - '!= null &&', - val2, - '!= null &&', - '(', - val1, - '=', - val1, - '.toString()).length >=', - '(', - val2, - '=', - val2, - '.toString()).length &&', - '(', - val1, - '.toLowerCase()).lastIndexOf(', - '(', - val2, - '.toLowerCase())) ===', - val1, - '.length -', - val2, - '.length', - ].join(''); +function endsWith(val1, val2): string { + const code: string[] = []; + code.push(`(typeof ${val1} === 'string' && `); + code.push(`typeof ${val2} === 'string' && `); + code.push(`${val1}.toLowerCase().endsWith(${val2}.toLowerCase()))`); + return code.join(''); } -function containsStrict(val1, val2) { - return [ - 'typeof ', - val1, - '=== "string" && typeof ', - val2, - '=== "string" &&', - val1, - '.indexOf(', - val2, - ') > -1', - ].join(''); +function containsStrict(val1, val2): string { + return `(typeof ${val1} === 'string' && ${val1}.includes(${val2}))`; } -function contains(val1, val2) { - return [ - val1, - '!= null && ', - val2, - '!= null &&', - val1, - '.toString().toLowerCase().indexOf(', - val2, - '.toString().toLowerCase()) > -1', - ].join(''); +function contains(val1, val2): string { + const code: string[] = []; + code.push(`(typeof ${val1} === 'string' && `); + code.push(`typeof ${val2} === 'string' && `); + code.push(`${val1}.toLowerCase().includes(${val2}.toLowerCase()))`); + return code.join(''); } export const binaryOperators = { - '===': function (val1, val2) { + '===': function (val1, val2): string { return `${val1}===${val2}`; }, - '==': function (val1, val2) { - return [ - 'typeof ', - val1, - '=== "string" && typeof ', - val2, - '=== "string"?', - val1, - '.toLowerCase() ===', - val2, - `.toLowerCase() :${val1}`, - '==', - val2, - ].join(''); + '==': function (val1, val2): string { + const code: string[] = []; + code.push(`((typeof ${val1} == 'string' && `); + code.push(`typeof ${val2} == 'string' && `); + code.push(`${val1}.toLowerCase() == ${val2}.toLowerCase()) || `); + code.push(`${val1} == ${val2})`); + return code.join(''); }, - '>=': function (val1, val2) { + '>=': function (val1, val2): string { return `${val1}>=${val2}`; }, - '>': function (val1, val2) { + '>': function (val1, val2): string { return `${val1}>${val2}`; }, - '<=': function (val1, val2) { + 'i==': {}, + '<=': function (val1, val2): string { return `${val1}<=${val2}`; }, - '<': function (val1, val2) { + '<': function (val1, val2): string { return `${val1}<${val2}`; }, - '!==': function (val1, val2) { + '!==': function (val1, val2): string { return `${val1}!==${val2}`; }, - '!=': function (val1, val2) { + '!=': function (val1, val2): string { return `${val1}!=${val2}`; }, '^==': startsWithStrict, - '==^': function (val1, val2) { + '==^': function (val1, val2): string { return startsWithStrict(val2, val1); }, '^=': startsWith, - '=^': function (val1, val2) { + '=^': function (val1, val2): string { return startsWith(val2, val1); }, '$==': endsWithStrict, - '==$': function (val1, val2) { + '==$': function (val1, val2): string { return endsWithStrict(val2, val1); }, '$=': endsWith, - '=$': function (val1, val2) { + '=$': function (val1, val2): string { return endsWith(val2, val1); }, '*==': containsStrict, - '==*': function (val1, val2) { + '==*': function (val1, val2): string { return containsStrict(val2, val1); }, - '=*': function (val1, val2) { + '=*': function (val1, val2): string { return contains(val2, val1); }, '*=': contains, - '+': function (val1, val2) { + '+': function (val1, val2): string { return `${val1}+${val2}`; }, - '-': function (val1, val2) { + '-': function (val1, val2): string { return `${val1}-${val2}`; }, - '*': function (val1, val2) { + '*': function (val1, val2): string { return `${val1}*${val2}`; }, - '/': function (val1, val2) { + '/': function (val1, val2): string { return `${val1}/${val2}`; }, - '%': function (val1, val2) { + '%': function (val1, val2): string { return `${val1}%${val2}`; }, - '>>': function (val1, val2) { + '>>': function (val1, val2): string { return `${val1}>>${val2}`; }, - '<<': function (val1, val2) { + '<<': function (val1, val2): string { return `${val1}<<${val2}`; }, - - '**': function (val1, val2) { + + '**': function (val1, val2): string { return `${val1}**${val2}`; }, }; diff --git a/src/parser.ts b/src/parser.ts index db8a39b..a2be68d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,7 +2,6 @@ import { ArrayExpression, AssignmentExpression, BinaryExpression, - ConcatExpression, Expression, FunctionCallExpression, FunctionExpression, @@ -14,259 +13,425 @@ import { SyntaxType, TokenType, UnaryExpression, - ObjectFilterExpression, - PosFilterExpression, - Keyword, - FunctionCallArgExpression, + FilterExpression, + RangeFilterExpression, + Token, + IndexFilterExpression, + DefinitionExpression, + SpreadExpression, + ObjectPropExpression, + ToArrayExpression, + ContextVariable, + ConditionalExpression, + OperatorType, } from './types'; import { JsosTemplateParserError } from './errors'; -import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; +import { DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; +import { CommonUtils } from './utils'; const EMPTY_EXPR = { type: SyntaxType.EMPTY }; export class JsonTemplateParser { private lexer: JsonTemplateLexer; - private expr?: Expression; constructor(lexer: JsonTemplateLexer) { this.lexer = lexer; } parse(): Expression { - if (!this.expr) { - const expr = this.parseStatementsExpr(); - this.lexer.validateNoMoreTokensLeft(); - this.expr = expr; + this.lexer.init(); + return this.parseStatementsExpr(); + } + + private parseEndOfStatement(blockEnd) { + if (this.lexer.matchEOT() || this.lexer.match(blockEnd)) { + return; + } + + if (this.lexer.match(';')) { + this.lexer.lex(); + } else if (this.lexer.matchNextChar('\n')) { + this.lexer.ignoreNextChar(); + } else { + this.lexer.throwUnexpectedToken(); } - return this.expr; } - private parseStatementsExpr(): StatementsExpression { + private parseStatements(blockEnd?: string): Expression[] { let statements: Expression[] = []; - while (!this.lexer.match('}')) { - let expr: Expression = this.parseStatementExpr(); - if (expr.type !== SyntaxType.EMPTY) { - statements.push(expr); - } - if (this.lexer.matchTokenType(TokenType.EOT)) { - break; - } - if (!this.lexer.match('}')) { - this.lexer.expect(';'); - } + while (!this.lexer.matchEOT() && !this.lexer.match(blockEnd)) { + statements.push(this.parseStatementExpr()); + this.parseEndOfStatement(blockEnd); } + return statements; + } + + private parseStatementsExpr(blockEnd?: string): StatementsExpression { return { type: SyntaxType.STATEMENTS_EXPR, - statements, + statements: this.parseStatements(blockEnd), }; } private parseStatementExpr(): Expression { - return this.parseAssignmentExpr(); - } - - private static getIDPath(expr: PathExpression): string { - if (!expr.root || expr.root.startsWith(DATA_PARAM_KEY) || expr.parts.length) { - throw new JsosTemplateParserError('Invalid ID path'); - } - return expr.root; + return this.parseBaseExpr(); } private parseAssignmentExpr(): AssignmentExpression | Expression { - const expr = this.parseLogicalORExpr(); + const expr = this.parseNextExpr(OperatorType.ASSIGNMENT); if (expr.type === SyntaxType.PATH && this.lexer.match('=')) { this.lexer.lex(); return { type: SyntaxType.ASSIGNMENT_EXPR, - value: this.parseLogicalORExpr(), - id: JsonTemplateParser.getIDPath(expr as PathExpression), + value: this.parseBaseExpr(), + path: expr as PathExpression, }; } return expr; } - private parsePathConcatExpr(): ConcatExpression | Expression { - const expr = this.parsePathConcatPartExpr(); - let operands: Expression[] = []; - - while (this.lexer.match('|')) { + private parseBaseExpr(): Expression { + return this.parseNextExpr(OperatorType.BASE); + } + + private parseNextExpr(currentOperation: OperatorType): Expression { + switch (currentOperation) { + case OperatorType.CONDITIONAL: + return this.parseAssignmentExpr(); + case OperatorType.ASSIGNMENT: + return this.parseCoalescingExpr(); + case OperatorType.COALESCING: + return this.parseLogicalORExpr(); + case OperatorType.OR: + return this.parseLogicalANDExpr(); + case OperatorType.AND: + return this.parseEqualityExpr(); + case OperatorType.EQUALITY: + return this.parseRelationalExpr(); + case OperatorType.RELATIONAL: + return this.parseShiftExpr(); + case OperatorType.SHIFT: + return this.parseAdditiveExpr(); + case OperatorType.ADDITION: + return this.parseMultiplicativeExpr(); + case OperatorType.MULTIPLICATION: + return this.parsePowerExpr(); + case OperatorType.POWER: + return this.parseUnaryExpr(); + case OperatorType.UNARY: + return this.parsePathAfterExpr(); + default: + return this.parseConditionalExpr(); + } + } + + private parsePathPart(): Expression | Expression[] | undefined { + if (this.lexer.match('.') && this.lexer.match('(', 1)) { this.lexer.lex(); - operands.push(this.parsePathConcatPartExpr()); + return this.parseBlockExpr(); + } else if (this.lexer.match('(')) { + return this.parseFunctionCallExpr(); + } else if (this.lexer.matchPathPartSelector()) { + return this.parseSelector(); + } else if (this.lexer.match('[') && !this.lexer.match(']', 1)) { + return this.parseArrayFiltersExpr(); + } else if (this.lexer.match('{')) { + return this.parseObjectFiltersExpr(); } + } - if (operands.length) { - operands.unshift(expr); - return { - type: SyntaxType.CONCAT_EXPR, - args: operands, + private parsePathParts(): Expression[] { + let parts: Expression[] = []; + let newParts: Expression | Expression[] | undefined; + while ((newParts = this.parsePathPart())) { + newParts = CommonUtils.toArray(newParts); + parts.push(...newParts); + if (newParts[0].type === SyntaxType.FUNCTION_CALL_EXPR) { + break; } } - - return expr; - } - - private parsePathConcatPartExpr(): Expression { - return this.lexer.match('(') ? this.parsePathGroupExpr() : this.parsePath(); + return JsonTemplateParser.combinePathParts(parts); + } + + private static prependFunctionID(prefix: string, id?: string): string { + return id ? prefix + '.' + id : prefix; + } + + private static combinePathParts(parts: Expression[]): Expression[] { + if (parts.length < 2) { + return parts; + } + let newParts: Expression[] = []; + for (let i = 0; i < parts.length; i++) { + let expr = parts[i]; + if (expr.type === SyntaxType.SELECTOR && expr.selector === '.') { + const selectorExpr = expr as SelectorExpression; + if (!selectorExpr.prop) { + continue; + } else if ( + !selectorExpr.contextVar && + selectorExpr.prop?.type === TokenType.ID && + parts[i + 1]?.type === SyntaxType.FUNCTION_CALL_EXPR + ) { + expr = parts[i + 1] as FunctionCallExpression; + expr.id = this.prependFunctionID(selectorExpr.prop.value, expr.id); + expr.dot = true; + i++; + } + } + newParts.push(expr); + } + if (newParts.length < parts.length) { + newParts = this.combinePathParts(newParts); + } + return newParts; } - private parsePathGroupExpr(): Expression { - this.lexer.expect('('); - let expr = this.parseLogicalORExpr(); - this.lexer.expect(')'); - - let parts: Expression[] = []; - let part: Expression | undefined; - while ((part = this.parsePathPart())) { - parts.push(part); + private static convertToFunctionCallExpr( + expr: PathExpression, + ): FunctionCallExpression | PathExpression | FunctionExpression { + if (expr.parts[0]?.type === SyntaxType.FUNCTION_CALL_EXPR && typeof expr.root !== 'object') { + const fnExpr = expr.parts.shift() as FunctionCallExpression; + if (expr.root) { + fnExpr.id = this.prependFunctionID(expr.root, fnExpr.id); + fnExpr.dot = false; + } + return fnExpr; } - - if (!parts.length) { - return expr; - } else if (expr.type === SyntaxType.PATH) { - expr.parts = (expr.parts || []).concat(parts); - return expr; + if (CommonUtils.getLastElement(expr.parts)?.type === SyntaxType.FUNCTION_CALL_EXPR) { + const fnExpr = expr.parts.pop() as FunctionCallExpression; + fnExpr.object = expr; + return fnExpr; } - - parts.unshift(expr); - - return { - type: SyntaxType.PATH, - parts: parts, - }; + return expr; } - private parsePath(): PathExpression { - if (!this.lexer.matchPath()) { - this.lexer.throwUnexpected(); + private parsePathRoot(root?: Expression): Expression | string | undefined { + if (root) { + return root; } - - let root: string | undefined; - let parts: Expression[] = []; - if (this.lexer.match('^')) { this.lexer.lex(); - root = DATA_PARAM_KEY; + return DATA_PARAM_KEY; } else if (this.lexer.matchID()) { - const idPath = this.parsePathVariable(); - if (this.lexer.match('(')) { - parts.push(this.parseFunctionCallExpr(idPath)) - } else { - root = idPath; - } - } - - let part: Expression | undefined; - while ((part = this.parsePathPart())) { - parts.push(part); + return this.lexer.value(); } + } - return { + private parsePath( + root?: Expression, + ): PathExpression | FunctionCallExpression | FunctionExpression { + const pathExpr = { type: SyntaxType.PATH, - root, - parts, + root: this.parsePathRoot(root), + parts: this.parsePathParts(), }; + + JsonTemplateParser.setSubpath(pathExpr.parts); + + const shouldConvertAsBlock = JsonTemplateParser.pathContainsVariables(pathExpr.parts); + const expr = JsonTemplateParser.convertToFunctionCallExpr(pathExpr); + return shouldConvertAsBlock ? JsonTemplateParser.convertToBlockExpr(expr) : expr; } - private parsePathPart(): Expression | undefined { - if (this.lexer.match('(') || - (this.lexer.match('.') && this.lexer.matchID(1) && this.lexer.match('(', 2)) - ) { - return this.parseFunctionCallExpr(); - } else if (this.lexer.matchSelector()) { - return this.parseSelector(); - } else if (this.lexer.match('[')) { - return this.parseFilterExpr(); + private parseContextVariable(expected: string): string | undefined { + if (this.lexer.match(expected)) { + this.lexer.lex(); + if (!this.lexer.matchID()) { + this.lexer.throwUnexpectedToken(); + } + return this.lexer.value(); } } private parseSelector(): SelectorExpression | Expression { - let selector = this.lexer.lex().value; - let prop: string | undefined; - - if (this.lexer.match('*') || - this.lexer.matchID() || - this.lexer.matchTokenType(TokenType.STR)) { - prop = this.lexer.lex().value; + let selector = this.lexer.value(); + let prop: Token | undefined; + let context: ContextVariable | undefined; + if (this.lexer.match('*')) { + prop = this.lexer.lex(); + } + if (this.lexer.matchID() || this.lexer.matchTokenType(TokenType.STR)) { + prop = this.lexer.lex(); + while (this.lexer.match('@') || this.lexer.match('#')) { + context = context || {}; + context.item = context.item || this.parseContextVariable('@'); + context.index = context.index || this.parseContextVariable('#'); + } } - return { type: SyntaxType.SELECTOR, selector: selector, - prop + prop, + context, }; } - private parsePosFilterExpr(): PosFilterExpression { - if (this.lexer.match(']')) { - return { - type: SyntaxType.POS_FILTER_EXPR, - empty: true, - }; - } + private parsePositionFilterExpr(): + | RangeFilterExpression + | IndexFilterExpression + | FilterExpression { if (this.lexer.match(':')) { this.lexer.lex(); return { - type: SyntaxType.POS_FILTER_EXPR, - toIdx: this.parseLogicalORExpr(), + type: SyntaxType.RANGE_FILTER_EXPR, + toIdx: this.parseBaseExpr(), }; } - let fromExpr = this.parseLogicalORExpr(); + let fromExpr = this.parseBaseExpr(); if (this.lexer.match(':')) { this.lexer.lex(); if (this.lexer.match(']')) { return { - type: SyntaxType.POS_FILTER_EXPR, + type: SyntaxType.RANGE_FILTER_EXPR, fromIdx: fromExpr, }; } return { - type: SyntaxType.POS_FILTER_EXPR, + type: SyntaxType.RANGE_FILTER_EXPR, fromIdx: fromExpr, - toIdx: this.parseLogicalORExpr(), + toIdx: this.parseBaseExpr(), }; } + if (!this.lexer.match(']')) { + this.lexer.expect(','); + } return { - type: SyntaxType.POS_FILTER_EXPR, - idx: fromExpr, + type: SyntaxType.ARRAY_INDEX_FILTER_EXPR, + indexes: { + type: SyntaxType.ARRAY_EXPR, + elements: [ + fromExpr, + ...this.parseCommaSeparatedElements(']', () => this.parseSpreadExpr()), + ], + }, + }; + } + + private parseObjectFilter(): IndexFilterExpression | FilterExpression { + let exclude = false; + if (this.lexer.match('~')) { + this.lexer.lex(); + exclude = true; + } + // excluding is applicaple only for index filters + if (exclude || this.lexer.match('[')) { + return { + type: SyntaxType.OBJECT_INDEX_FILTER_EXPR, + indexes: this.parseArrayExpr(), + exclude, + }; + } + return { + type: SyntaxType.OBJECT_FILTER_EXPR, + filter: this.parseBaseExpr(), }; } - private parseObjectFilterExpression(): ObjectFilterExpression { - const filters: Expression[] = []; + private combineObjectFilters(objectFilters: FilterExpression[]): FilterExpression[] { + if (objectFilters.length <= 1) { + return objectFilters; + } + const expr1 = objectFilters.shift() as FilterExpression; + const expr2 = this.combineObjectFilters(objectFilters); + return [ + { + type: SyntaxType.OBJECT_FILTER_EXPR, + filter: { + type: SyntaxType.LOGICAL_AND_EXPR, + op: '&&', + args: [expr1.filter, expr2[0].filter], + }, + }, + ]; + } + + private parseObjectFiltersExpr(): (FilterExpression | IndexFilterExpression)[] { + const objectFilters: FilterExpression[] = []; + const indexFilters: IndexFilterExpression[] = []; + while (this.lexer.match('{')) { this.lexer.expect('{'); - filters.push(this.parseLogicalORExpr()); + const expr = this.parseObjectFilter(); + if (expr.type === SyntaxType.OBJECT_INDEX_FILTER_EXPR) { + indexFilters.push(expr as IndexFilterExpression); + } else { + objectFilters.push(expr as FilterExpression); + } this.lexer.expect('}'); + if (this.lexer.match('.') && this.lexer.match('{', 1)) { + this.lexer.lex(); + } } - return { - type: SyntaxType.OBJECT_FILTER_EXPR, - filters, - }; + return [...this.combineObjectFilters(objectFilters), ...indexFilters]; } - private parseFilterExpr(): Expression { - this.lexer.expect('['); - let expr = this.lexer.match('{') - ? this.parseObjectFilterExpression() - : this.parsePosFilterExpr(); + private parseConditionalExpr(): ConditionalExpression | Expression { + const ifExpr = this.parseNextExpr(OperatorType.CONDITIONAL); + if (this.lexer.match('?')) { + this.lexer.lex(); + const thenExpr = this.parseConditionalExpr(); + if (this.lexer.match(':')) { + this.lexer.lex(); + const elseExpr = this.parseConditionalExpr(); + return { + type: SyntaxType.CONDITIONAL_EXPR, + if: ifExpr, + then: thenExpr, + else: elseExpr, + }; + } + return { + type: SyntaxType.CONDITIONAL_EXPR, + if: ifExpr, + then: thenExpr, + else: { + type: SyntaxType.LITERAL, + tokenType: TokenType.UNDEFINED, + }, + }; + } + + return ifExpr; + } + + private parseArrayFiltersExpr(): + | RangeFilterExpression + | IndexFilterExpression + | FilterExpression { + this.lexer.expect('['); + const expr = this.parsePositionFilterExpr(); this.lexer.expect(']'); + return expr; + } + + private parseCoalescingExpr(): BinaryExpression | Expression { + let expr = this.parseNextExpr(OperatorType.COALESCING); + + if (this.lexer.match('??')) { + return { + type: SyntaxType.LOGICAL_COALESCE_EXPR, + op: this.lexer.value(), + args: [expr, this.parseCoalescingExpr()], + }; + } return expr; } private parseLogicalORExpr(): BinaryExpression | Expression { - let expr = this.parseLogicalANDExpr(); + let expr = this.parseNextExpr(OperatorType.OR); - while (this.lexer.match('||')) { - expr = { - type: SyntaxType.LOGICAL_EXPR, - op: this.lexer.lex().value, - args: [expr, this.parseLogicalANDExpr()], + if (this.lexer.match('||')) { + return { + type: SyntaxType.LOGICAL_OR_EXPR, + op: this.lexer.value(), + args: [expr, this.parseLogicalORExpr()], }; } @@ -274,13 +439,13 @@ export class JsonTemplateParser { } private parseLogicalANDExpr(): BinaryExpression | Expression { - let expr = this.parseEqualityExpr(); + let expr = this.parseNextExpr(OperatorType.AND); - while (this.lexer.match('&&')) { - expr = { - type: SyntaxType.LOGICAL_EXPR, - op: this.lexer.lex().value, - args: [expr, this.parseEqualityExpr()], + if (this.lexer.match('&&')) { + return { + type: SyntaxType.LOGICAL_AND_EXPR, + op: this.lexer.value(), + args: [expr, this.parseLogicalANDExpr()], }; } @@ -288,9 +453,9 @@ export class JsonTemplateParser { } private parseEqualityExpr(): BinaryExpression | Expression { - let expr = this.parseRelationalExpr(); + let expr = this.parseNextExpr(OperatorType.EQUALITY); - while ( + if ( this.lexer.match('==') || this.lexer.match('!=') || this.lexer.match('===') || @@ -308,10 +473,10 @@ export class JsonTemplateParser { this.lexer.match('*=') || this.lexer.match('=*') ) { - expr = { + return { type: SyntaxType.COMPARISON_EXPR, - op: this.lexer.lex().value, - args: [expr, this.parseRelationalExpr()], + op: this.lexer.value(), + args: [expr, this.parseEqualityExpr()], }; } @@ -319,18 +484,19 @@ export class JsonTemplateParser { } private parseRelationalExpr(): BinaryExpression | Expression { - let expr = this.parseShiftExpr(); + let expr = this.parseNextExpr(OperatorType.RELATIONAL); - while ( + if ( this.lexer.match('<') || this.lexer.match('>') || this.lexer.match('<=') || - this.lexer.match('>=') + this.lexer.match('>=') || + this.lexer.matchIN() ) { - expr = { - type: SyntaxType.COMPARISON_EXPR, - op: this.lexer.lex().value, - args: [expr, this.parseShiftExpr()], + return { + type: this.lexer.matchIN() ? SyntaxType.IN_EXPR : SyntaxType.COMPARISON_EXPR, + op: this.lexer.value(), + args: [expr, this.parseRelationalExpr()], }; } @@ -338,13 +504,13 @@ export class JsonTemplateParser { } private parseShiftExpr(): BinaryExpression | Expression { - let expr = this.parseAdditiveExpr(); + let expr = this.parseNextExpr(OperatorType.SHIFT); - while (this.lexer.match('>>') || this.lexer.match('<<')) { - expr = { + if (this.lexer.match('>>') || this.lexer.match('<<')) { + return { type: SyntaxType.MATH_EXPR, - op: this.lexer.lex().value, - args: [expr, this.parseAdditiveExpr()], + op: this.lexer.value(), + args: [expr, this.parseShiftExpr()], }; } @@ -352,13 +518,13 @@ export class JsonTemplateParser { } private parseAdditiveExpr(): BinaryExpression | Expression { - let expr = this.parseMultiplicativeExpr(); + let expr = this.parseNextExpr(OperatorType.ADDITION); - while (this.lexer.match('+') || this.lexer.match('-')) { - expr = { + if (this.lexer.match('+') || this.lexer.match('-')) { + return { type: SyntaxType.MATH_EXPR, - op: this.lexer.lex().value, - args: [expr, this.parseMultiplicativeExpr()], + op: this.lexer.value(), + args: [expr, this.parseAdditiveExpr()], }; } @@ -366,13 +532,13 @@ export class JsonTemplateParser { } private parseMultiplicativeExpr(): BinaryExpression | Expression { - let expr: Expression = this.parsePowerExpr(); + let expr = this.parseNextExpr(OperatorType.MULTIPLICATION); - while (this.lexer.match('*') || this.lexer.match('/') || this.lexer.match('%')) { - expr = { + if (this.lexer.match('*') || this.lexer.match('/') || this.lexer.match('%')) { + return { type: SyntaxType.MATH_EXPR, - op: this.lexer.lex().value as string, - args: [expr, this.parsePowerExpr()], + op: this.lexer.value() as string, + args: [expr, this.parseMultiplicativeExpr()], }; } @@ -380,12 +546,12 @@ export class JsonTemplateParser { } private parsePowerExpr(): BinaryExpression | Expression { - let expr: Expression = this.parseUnaryExpr(); + let expr = this.parseNextExpr(OperatorType.POWER); - while (this.lexer.match('**')) { - expr = { + if (this.lexer.match('**')) { + return { type: SyntaxType.MATH_EXPR, - op: this.lexer.lex().value as string, + op: this.lexer.value() as string, args: [expr, this.parsePowerExpr()], }; } @@ -394,25 +560,74 @@ export class JsonTemplateParser { } private parseUnaryExpr(): UnaryExpression | Expression { - if (this.lexer.match('!') || + if ( + this.lexer.match('!') || this.lexer.match('-') || - this.lexer.matchTypeOf()) { + this.lexer.matchTypeOf() || + this.lexer.matchAwait() + ) { return { type: SyntaxType.UNARY_EXPR, - op: this.lexer.lex().value as string, - arg: this.parseComplexExpr(), + op: this.lexer.value() as string, + arg: this.parseUnaryExpr(), }; } - return this.parseComplexExpr(); + return this.parseNextExpr(OperatorType.UNARY); } - private parseComplexExpr(): PathExpression | Expression { - const expr = this.parsePrimaryExpr(); - if(this.lexer.match('.') || this.lexer.match('[')) { - const pathExpr = this.parsePath(); - pathExpr.parts.unshift(expr); - return pathExpr; + private isToArrayExpr(): boolean { + let toArray = false; + while (this.lexer.match('[') && this.lexer.match(']', 1)) { + this.lexer.lex(); + this.lexer.lex(); + toArray = true; + } + return toArray; + } + + private shouldSkipPathParsing(expr: Expression): boolean { + switch (expr.type) { + case SyntaxType.DEFINTION_EXPR: + case SyntaxType.ASSIGNMENT_EXPR: + case SyntaxType.SPREAD_EXPR: + return true; + case SyntaxType.LITERAL: + case SyntaxType.MATH_EXPR: + case SyntaxType.COMPARISON_EXPR: + if (this.lexer.match('(')) { + return true; + } + break; + case SyntaxType.FUNCTION_EXPR: + if (!this.lexer.match('(')) { + return true; + } + break; + case SyntaxType.ARRAY_EXPR: + case SyntaxType.OBJECT_EXPR: + if (this.lexer.match('(')) { + return true; + } + break; + } + return false; + } + + private parsePathAfterExpr(): PathExpression | ToArrayExpression | Expression { + let expr = this.parsePrimaryExpr(); + if (this.shouldSkipPathParsing(expr)) { + return expr; + } + while (this.lexer.matchPathPartSelector() || this.lexer.match('[') || this.lexer.match('(')) { + if (this.isToArrayExpr()) { + expr = { + type: SyntaxType.TO_ARRAY_EXPR, + value: expr, + }; + continue; + } + expr = this.parsePath(expr); } return expr; } @@ -426,166 +641,119 @@ export class JsonTemplateParser { }; } - private parsePathVariable(): string { + private parseIDPath(): string { const idParts: string[] = []; while (this.lexer.matchID()) { - idParts.push(this.lexer.lex().value); + idParts.push(this.lexer.value()); if (this.lexer.match('.') && this.lexer.matchID(1)) { this.lexer.lex(); } } if (!idParts.length) { - this.lexer.throwUnexpected(); + this.lexer.throwUnexpectedToken(); } - return idParts.join('.').replace(/^\$/, `${BINDINGS_PARAM_KEY}.`); + return idParts.join('.'); } - private parseDefinitionExpr(): AssignmentExpression { - const operator = this.lexer.lex().value; - if (!this.lexer.matchID()) { - this.lexer.throwUnexpected(); + private parseObjectDefVars(): string[] { + const vars: string[] = []; + this.lexer.expect('{'); + while (!this.lexer.match('}')) { + if (!this.lexer.matchID()) { + throw new JsosTemplateParserError('Invalid object vars at ' + this.lexer.getContext()); + } + vars.push(this.lexer.value()); + if (!this.lexer.match('}')) { + this.lexer.expect(','); + } } - const id = this.lexer.lex().value; - this.lexer.expect('='); + this.lexer.expect('}'); + if (vars.length === 0) { + throw new JsosTemplateParserError('Empty object vars at ' + this.lexer.getContext()); + } + return vars; + } - return { - type: SyntaxType.ASSIGNMENT_EXPR, - id, - value: this.parseLogicalORExpr(), - operator, - }; + private parseNormalDefVars(): string[] { + const vars: string[] = []; + if (!this.lexer.matchID()) { + throw new JsosTemplateParserError('Invalid normal vars at ' + this.lexer.getContext()); + } + vars.push(this.lexer.value()); + return vars; } - private parseFunctionCallArgExpr(): FunctionCallArgExpression { - let spread: boolean = false; - if (this.lexer.match('...')) { - this.lexer.lex(); - spread = true; + private parseDefinitionExpr(): DefinitionExpression { + const definition = this.lexer.value(); + let fromObject = this.lexer.match('{'); + let vars: string[] = fromObject ? this.parseObjectDefVars() : this.parseNormalDefVars(); + this.lexer.expect('='); - } return { - type: SyntaxType.FUNCTION_CALL_ARG_EXPR, - value: this.parseLogicalORExpr(), - spread - } + type: SyntaxType.DEFINTION_EXPR, + value: this.parseBaseExpr(), + vars, + definition, + fromObject, + }; } - private parseFunctionCallArgs(): FunctionCallArgExpression[] { - const args: FunctionCallArgExpression[] = []; + private parseFunctionCallArgs(): Expression[] { this.lexer.expect('('); - while (!this.lexer.match(')')) { - args.push(this.parseFunctionCallArgExpr()); - if (!this.lexer.match(')')) { - this.lexer.expect(','); - } - } + const args = this.parseCommaSeparatedElements(')', () => this.parseSpreadExpr()); this.lexer.expect(')'); return args; } - private parseFunctionCallExpr(id?: string): FunctionCallExpression { - let dot = this.lexer.match('.'); - let isNew = this.lexer.matchNew(); - if (dot || isNew) { + private parseFunctionCallExpr(): FunctionCallExpression { + let id: string | undefined; + + if (this.lexer.matchNew()) { this.lexer.lex(); - if (!this.lexer.matchID()) { - this.lexer.throwUnexpected(); - } - id = this.parsePathVariable(); + id = 'new ' + this.parseIDPath(); } return { type: SyntaxType.FUNCTION_CALL_EXPR, args: this.parseFunctionCallArgs(), id, - dot, - isNew }; } - private parsePrimaryExpr(): Expression { - if (this.lexer.matchEOT() || this.lexer.match(';')) { - return EMPTY_EXPR; - } - - if (this.lexer.matchNew()) { - return this.parseFunctionCallExpr(); - } - - if (this.lexer.matchDefinition()) { - return this.parseDefinitionExpr(); - } - - if (this.lexer.matchFunction()) { - return this.parseFunctionDefinitionExpr(); - } - - if (this.lexer.matchLiteral()) { - return this.parseLiteralExpr(); - } - - if (this.lexer.match('{')) { - return this.parseObjectExpr(); - } - - if (this.lexer.match('[')) { - return this.parseArrayExpr(); - } - - if (this.lexer.matchSelector()) { - return this.parsePathConcatExpr(); - } - - if (this.lexer.matchPath()) { - return this.parsePath(); - } - - if (this.lexer.match('(')) { - return this.parseGroupExpr(); - } - - return this.lexer.throwUnexpected(); - } - private parseFunctionDefinitionParam(): string { let spread: string = ''; - if (this.lexer.match('...')) { + if (this.lexer.matchSpread()) { this.lexer.lex(); spread = '...'; // rest param should be last param. if (!this.lexer.match(')', 1)) { - this.lexer.throwUnexpected(); + this.lexer.throwUnexpectedToken(); } } if (!this.lexer.matchID()) { - this.lexer.throwUnexpected(); + this.lexer.throwUnexpectedToken(); } - return `${spread}${this.lexer.lex().value}`; + return `${spread}${this.lexer.value()}`; } private parseFunctionDefinitionParams(): string[] { - const params: string[] = []; this.lexer.expect('('); - while (!this.lexer.match(')')) { - params.push(this.parseFunctionDefinitionParam()); - if (!this.lexer.match(')')) { - this.lexer.expect(','); - } - } + const params = this.parseCommaSeparatedElements(')', () => this.parseFunctionDefinitionParam()); this.lexer.expect(')'); return params; } - private parseFunctionDefinitionExpr(): FunctionExpression { - this.lexer.expectOperator(Keyword.FUNCTION); + private parseFunctionExpr(asyncFn = false): FunctionExpression { + this.lexer.lex(); const params = this.parseFunctionDefinitionParams(); this.lexer.expect('{'); - const body = this.parseStatementsExpr(); + const statements = this.parseStatementsExpr('}'); this.lexer.expect('}'); return { type: SyntaxType.FUNCTION_EXPR, params, - body, + body: statements, + async: asyncFn, }; } @@ -593,52 +761,199 @@ export class JsonTemplateParser { let key: Expression | string; if (this.lexer.match('[')) { this.lexer.lex(); - key = this.parseLogicalORExpr(); + key = this.parseBaseExpr(); this.lexer.expect(']'); + } else if (this.lexer.matchID()) { + key = this.lexer.value(); + } else if (this.lexer.matchTokenType(TokenType.STR)) { + key = this.parseLiteralExpr(); } else { - key = `${this.lexer.lex().value}`; + this.lexer.throwUnexpectedToken(); } return key; } + private parseObjectPropExpr(): ObjectPropExpression { + let key: Expression | string | undefined; + if (!this.lexer.matchSpread()) { + key = this.parseObjectKeyExpr(); + this.lexer.expect(':'); + } + const value = this.parseSpreadExpr(); + return { + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value, + }; + } + private parseObjectExpr(): ObjectExpression { this.lexer.expect('{'); - const expr: ObjectExpression = { + const props = this.parseCommaSeparatedElements('}', () => this.parseObjectPropExpr()); + this.lexer.expect('}'); + return { type: SyntaxType.OBJECT_EXPR, - props: [], + props, }; - while (!this.lexer.match('}')) { - const key = this.parseObjectKeyExpr(); - this.lexer.expect(':'); - const value = this.parseLogicalORExpr(); - expr.props.push({ key, value }); - if (!this.lexer.match('}')) { + } + + private parseCommaSeparatedElements(blockEnd: string, parseFn: () => T): T[] { + const elements: T[] = []; + while (!this.lexer.match(blockEnd)) { + elements.push(parseFn()); + if (!this.lexer.match(blockEnd)) { this.lexer.expect(','); } } - this.lexer.expect('}'); - return expr; + return elements; + } + + private parseSpreadExpr(): SpreadExpression | Expression { + if (this.lexer.matchSpread()) { + this.lexer.lex(); + return { + type: SyntaxType.SPREAD_EXPR, + value: this.parseBaseExpr(), + }; + } + return this.parseBaseExpr(); } private parseArrayExpr(): ArrayExpression { this.lexer.expect('['); - const elements: Expression[] = []; - while (!this.lexer.match(']')) { - const expr = this.parseLogicalANDExpr(); - elements.push(expr); - if (!this.lexer.match(']')) { - this.lexer.expect(','); - } - } + const elements = this.parseCommaSeparatedElements(']', () => this.parseSpreadExpr()); this.lexer.expect(']'); - return { type: SyntaxType.ARRAY_EXPR, elements }; + return { + type: SyntaxType.ARRAY_EXPR, + elements, + }; } - private parseGroupExpr(): Expression { + private parseBlockExpr(): FunctionExpression | Expression { this.lexer.expect('('); - let expr = this.parseLogicalORExpr(); + let statements: Expression[] = this.parseStatements(')'); this.lexer.expect(')'); + if (statements.length === 0) { + return EMPTY_EXPR; + } else if (statements.length === 1) { + return statements[0]; + } + return { + type: SyntaxType.FUNCTION_EXPR, + body: CommonUtils.convertToStatementsExpr(...statements), + block: true, + }; + } - return expr; + private parseAsyncFunctionExpr(): FunctionExpression { + this.lexer.lex(); + if (this.lexer.matchFunction()) { + return this.parseFunctionExpr(true); + } else if (this.lexer.matchLambda()) { + return this.parseLambdaExpr(true); + } + this.lexer.throwUnexpectedToken(); + } + + private parseLambdaExpr(asyncFn = false): FunctionExpression { + this.lexer.lex(); + const expr = this.parseBaseExpr(); + return { + type: SyntaxType.FUNCTION_EXPR, + body: CommonUtils.convertToStatementsExpr(expr), + params: ['...args'], + async: asyncFn, + }; + } + + private parsePrimaryExpr(): Expression { + if (this.lexer.match(';')) { + return EMPTY_EXPR; + } + + if (this.lexer.matchTokenType(TokenType.LAMBDA_ARG)) { + return { + type: SyntaxType.LAMBDA_ARG, + index: this.lexer.value(), + }; + } + + if (this.lexer.matchNew()) { + return this.parseFunctionCallExpr(); + } + + if (this.lexer.matchDefinition()) { + return this.parseDefinitionExpr(); + } + + if (this.lexer.matchLambda()) { + return this.parseLambdaExpr(); + } + + if (this.lexer.matchFunction()) { + return this.parseFunctionExpr(); + } + + if (this.lexer.matchAsync()) { + return this.parseAsyncFunctionExpr(); + } + + if (this.lexer.matchLiteral()) { + return this.parseLiteralExpr(); + } + + if (this.lexer.match('{')) { + return this.parseObjectExpr(); + } + + if (this.lexer.match('[')) { + return this.parseArrayExpr(); + } + + if (this.lexer.matchPath()) { + return this.parsePath(); + } + + if (this.lexer.match('(')) { + return this.parseBlockExpr(); + } + + return this.lexer.throwUnexpectedToken(); + } + + private static setSubpath(parts: any[]) { + const remainingParts = parts.slice(); + while (remainingParts.length) { + const part = remainingParts.shift(); + if (typeof part !== 'object') { + continue; + } + if (part.type === SyntaxType.PATH && Array.isArray(part.parts)) { + part.subPath = !part.root; + } else { + for (let key in part) { + if (Array.isArray(part[key])) { + remainingParts.push(...part[key].flat()); + } else if (typeof part[key] === 'object') { + remainingParts.push(part[key]); + } + } + } + } + } + + private static pathContainsVariables(parts: Expression[]): boolean { + return parts + .filter((part) => part.type === SyntaxType.SELECTOR) + .map((part) => part as SelectorExpression) + .some((part) => part.context?.index || part.context?.item); + } + + private static convertToBlockExpr(expr: Expression): FunctionExpression { + return { + type: SyntaxType.FUNCTION_EXPR, + block: true, + body: CommonUtils.convertToStatementsExpr(expr), + }; } } diff --git a/src/translator.ts b/src/translator.ts index 0880583..db0f099 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -1,39 +1,47 @@ -import { DATA_PARAM_KEY, VARS_PREFIX } from './constants'; +import { DATA_PARAM_KEY, FUNCTION_RESULT_KEY, RESULT_KEY, VARS_PREFIX } from './constants'; +import { JsosTemplateTranslatorError } from './errors'; import { binaryOperators } from './operators'; import { ArrayExpression, AssignmentExpression, BinaryExpression, - ConcatExpression, Expression, FunctionCallExpression, FunctionExpression, LiteralExpression, - ObjectFilterExpression, ObjectExpression, PathExpression, - PosFilterExpression, + RangeFilterExpression, SelectorExpression, StatementsExpression, SyntaxType, UnaryExpression, - Keyword + TokenType, + IndexFilterExpression, + DefinitionExpression, + SpreadExpression, + LambdaArgExpression, + ToArrayExpression, + ContextVariable, + FilterExpression, + ConditionalExpression, } from './types'; +import { CommonUtils } from './utils'; export class JsonTemplateTranslator { - private body: string[]; - private vars: string[]; - private lastVarId: number; - private unusedVars: string[]; + private vars: string[] = []; + private lastVarId = 0; + private unusedVars: string[] = []; private expr: Expression; - private code?: string; constructor(expr: Expression) { - this.body = []; + this.expr = expr; + } + + private init() { this.vars = []; this.lastVarId = 0; this.unusedVars = []; - this.expr = expr; } private acquireVar(): string { @@ -46,6 +54,14 @@ export class JsonTemplateTranslator { return varName; } + private acquireVars(numVars = 1): string[] { + const vars: string[] = []; + for (let i = 0; i < numVars; i++) { + vars.push(this.acquireVar()); + } + return vars; + } + private releaseVars(...args: any[]) { let i = args.length; while (i--) { @@ -53,630 +69,535 @@ export class JsonTemplateTranslator { } } - translate(): string { - if (!this.code) { - this.translateExpr(this.expr, 'result', DATA_PARAM_KEY); + translate(dest = RESULT_KEY, ctx = DATA_PARAM_KEY): string { + this.init(); + let code: string[] = []; + const exprCode = this.translateExpr(this.expr, dest, ctx); - this.body.unshift( - '"use strict";', - 'const concat = Array.prototype.concat;', - 'let result = undefined;', - this.vars.map((elm) => `let ${elm};`).join(''), - ); + code.push(`let ${dest};`); + code.push(this.vars.map((elm) => `let ${elm};`).join('')); + code.push(exprCode); + code.push(`return ${dest};`); - this.body.push('return result;'); - - this.code = this.body.join(''); - } - return this.code; + return code.join(''); } - private translateExpr(expr: Expression, dest: string, ctx: string) { + private translateExpr(expr: Expression, dest: string, ctx: string): string { switch (expr.type) { case SyntaxType.STATEMENTS_EXPR: - this.translateStatementsExpr(expr as StatementsExpression, dest, ctx); - break; + return this.translateStatementsExpr(expr as StatementsExpression, dest, ctx); + case SyntaxType.PATH: - this.translatePath(expr as PathExpression, dest, ctx); - break; + return this.translatePath(expr as PathExpression, dest, ctx); - case SyntaxType.CONCAT_EXPR: - this.translateConcatExpr(expr as ConcatExpression, dest, ctx); - break; + case SyntaxType.IN_EXPR: + return this.translateINExpr(expr as BinaryExpression, dest, ctx); case SyntaxType.COMPARISON_EXPR: - this.translateComparisonExpr(expr as BinaryExpression, dest, ctx); - break; - case SyntaxType.MATH_EXPR: - this.translateMathExpr(expr as BinaryExpression, dest, ctx); - break; + return this.translateBinaryExpr(expr as BinaryExpression, dest, ctx); - case SyntaxType.LOGICAL_EXPR: - this.translateLogicalExpr(expr as BinaryExpression, dest, ctx); - break; + case SyntaxType.LOGICAL_COALESCE_EXPR: + case SyntaxType.LOGICAL_AND_EXPR: + case SyntaxType.LOGICAL_OR_EXPR: + return this.translateLogicalExpr(expr as BinaryExpression, dest, ctx); case SyntaxType.UNARY_EXPR: - this.translateUnaryExpr(expr as UnaryExpression, dest, ctx); - break; + return this.translateUnaryExpr(expr as UnaryExpression, dest, ctx); + + case SyntaxType.LAMBDA_ARG: + return this.translateLambdaArgExpr(expr as LambdaArgExpression, dest, ctx); + + case SyntaxType.SPREAD_EXPR: + return this.translateSpreadExpr(expr as SpreadExpression, dest, ctx); case SyntaxType.LITERAL: - this.translateLiteralExpr(expr as LiteralExpression, dest, ctx); - break; + return this.translateLiteralExpr(expr as LiteralExpression, dest, ctx); case SyntaxType.ARRAY_EXPR: - this.translateArrayExpr(expr as ArrayExpression, dest, ctx); - break; + return this.translateArrayExpr(expr as ArrayExpression, dest, ctx); case SyntaxType.OBJECT_EXPR: - this.translateObjectExpr(expr as ObjectExpression, dest, ctx); - break; + return this.translateObjectExpr(expr as ObjectExpression, dest, ctx); case SyntaxType.FUNCTION_EXPR: - this.translateFunctionExpr(expr as FunctionExpression, dest, ctx); - break; + return this.translateFunctionExpr(expr as FunctionExpression, dest, ctx); case SyntaxType.FUNCTION_CALL_EXPR: - this.translateFunctionCallExpr(expr as FunctionCallExpression, dest, ctx); - break; + return this.translateFunctionCallExpr(expr as FunctionCallExpression, dest, ctx); + + case SyntaxType.DEFINTION_EXPR: + return this.translateDefinitionExpr(expr as DefinitionExpression, dest, ctx); case SyntaxType.ASSIGNMENT_EXPR: - this.translateAssignmentExpr(expr as AssignmentExpression, dest, ctx); - break; - + return this.translateAssignmentExpr(expr as AssignmentExpression, dest, ctx); + case SyntaxType.OBJECT_FILTER_EXPR: - this.translateObjectFilterExpression(expr as ObjectFilterExpression, dest, dest); - break; + return this.translateObjectFilterExpr(expr as FilterExpression, dest, ctx); + + case SyntaxType.RANGE_FILTER_EXPR: + return this.translateRangeFilterExpr(expr as RangeFilterExpression, dest, ctx); + + case SyntaxType.ARRAY_INDEX_FILTER_EXPR: + case SyntaxType.OBJECT_INDEX_FILTER_EXPR: + return this.translateIndexFilterExpr(expr as IndexFilterExpression, dest, ctx); - case SyntaxType.POS_FILTER_EXPR: - this.translatePosFilterExpression(expr as ObjectFilterExpression, dest, dest); - break; - case SyntaxType.SELECTOR: - this.translateSelector(expr as SelectorExpression, dest, dest); - break; + return this.translateSelector(expr as SelectorExpression, dest, ctx); + + case SyntaxType.TO_ARRAY_EXPR: + return this.translateToArrayExpr(expr as ToArrayExpression, dest, ctx); + + case SyntaxType.CONDITIONAL_EXPR: + return this.translateConditionalExpr(expr as ConditionalExpression, dest, ctx); + + default: + return ''; } } - private translatePath(expr: PathExpression, dest: string, ctx: string) { - const parts = expr.parts; - let newCtx = expr.root || ctx; - this.body.push(dest, '=', newCtx, ';'); + 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('} else {'); + code.push(this.translateExpr(expr.else, elseVar, ctx)); + code.push(`${dest} = ${elseVar};`); + code.push('}'); + this.releaseVars(elseVar); + this.releaseVars(thenVar); + this.releaseVars(ifVar); + return code.join(''); + } - parts.forEach(part => { - this.translateExpr(part, dest, dest); - }); + private translateLambdaArgExpr(expr: LambdaArgExpression, dest: string, ctx: string): string { + return `${dest} = args[${expr.index}];`; } - private translateDescendantSelector(expr: SelectorExpression, dest: string, baseCtx: string) { - const { prop } = expr; - const ctx = this.acquireVar(); - const curCtx = this.acquireVar(); - const childCtxs = this.acquireVar(); - const i = this.acquireVar(); - const j = this.acquireVar(); - const val = this.acquireVar(); - const len = this.acquireVar(); - const result = this.acquireVar(); - this.body.push(JsonTemplateTranslator.covertToArrayValue(dest)); - this.body.push( - ctx, - '=', - baseCtx, - '.slice(),', - result, - '=[];', - 'while(', - ctx, - '.length) {', - curCtx, - '=', - ctx, - '.shift();', - ); - prop - ? this.body.push('if(typeof ', curCtx, '=== "object" &&', curCtx, ') {') - : this.body.push('if(typeof ', curCtx, '!= null) {'); - this.body.push( - childCtxs, - '= [];', - 'if(Array.isArray(', - curCtx, - ')) {', - i, - '= 0,', - len, - '=', - curCtx, - '.length;', - 'while(', - i, - '<', - len, - ') {', - val, - '=', - curCtx, - '[', - i, - '++];', - ); - prop && this.body.push('if(typeof ', val, '=== "object") {'); - this.inlineAppendToArray(childCtxs, val); - prop && this.body.push('}'); - this.body.push('}', '}', 'else {'); - if (prop) { - if (prop !== '*') { - this.body.push(val, '=', curCtx, `["${prop}"];`); - this.inlineAppendToArray(result, val); - } - } else { - this.inlineAppendToArray(result, curCtx); - this.body.push('if(typeof ', curCtx, '=== "object") {'); - } - - this.body.push( - 'for(', - j, - ' in ', - curCtx, - ') {', - 'if(', - curCtx, - '.hasOwnProperty(', - j, - ')) {', - val, - '=', - curCtx, - '[', - j, - '];', - ); - this.inlineAppendToArray(childCtxs, val); - prop === '*' && this.inlineAppendToArray(result, val); - this.body.push('}', '}'); - prop || this.body.push('}'); - this.body.push( - '}', - childCtxs, - '.length &&', - ctx, - '.unshift.apply(', - ctx, - ',', - childCtxs, - ');', - '}', - '}', - dest, - '=', - result, - ';', - ); + private translateToArrayExpr(expr: ToArrayExpression, dest: string, ctx: string): string { + const code: string[] = []; + code.push(this.translateExpr(expr.value, dest, ctx)); + code.push(JsonTemplateTranslator.covertToArrayValue(dest)); + return code.join(''); + } - this.releaseVars(ctx, curCtx, childCtxs, i, j, val, len, result); + private translateSpreadExpr(expr: SpreadExpression, dest: string, ctx: string): string { + return this.translateExpr(expr.value, dest, ctx); } - private translateConcatExpr(expr: ConcatExpression, dest: string, ctx: string) { - const argVars: any[] = []; - const { args } = expr; - const len = args.length; - let i = 0; + private translatePathRoot(path: PathExpression, dest: string, ctx: string): string { + if (typeof path.root === 'object') { + return this.translateExpr(path.root, dest, ctx); + } else if(path.subPath && path.parts.length) { + if(JsonTemplateTranslator.isSinglePropSelection(path.parts[0])) { + const part = path.parts.shift() as SelectorExpression; + const propStr = CommonUtils.escapeStr(part.prop?.value); + const code: string[] = []; + code.push(`if(!${ctx}[${propStr}]) {continue;}`); + code.push(`${dest} = ${ctx}[${propStr}];`); + return code.join(''); + } + } + return `${dest} = ${path.root || ctx};`; + } - while (i < len) { - argVars.push(this.acquireVar()); - this.translateExpr(args[i], argVars[i++], ctx); + private translatePathContext(context: ContextVariable, item: string, idx: string): string { + const code: string[] = []; + if (context.item) { + code.push(`let ${context.item} = ${item};`); + } + if (context.index) { + code.push(`let ${context.index} = ${idx};`); } + return code.join(''); + } - this.body.push(JsonTemplateTranslator.covertToArrayValue(dest)); - this.body.push(dest, '= concat.call(', argVars.join(','), ');'); + private prepareDataForPathPart(part: Expression, data: string): string { + const code: string[] = []; + code.push(JsonTemplateTranslator.covertToArrayValue(data)); + if (JsonTemplateTranslator.isSinglePropSelection(part)) { + const selector = part as SelectorExpression; + const propStr = CommonUtils.escapeStr(selector.prop?.value); + code.push(`if(Object.prototype.hasOwnProperty.call(${data}, ${propStr})){`); + code.push(`${data} = [${data}];`); + code.push('}'); + } else if (JsonTemplateTranslator.isArrayFilterExpr(part)) { + code.push(`${data} = [${data}];`); + } + return code.join(''); + } - this.releaseVars(argVars); + private translatePath(expr: PathExpression, dest: string, baseCtx: string): string { + const rootCode = this.translatePathRoot(expr, dest, baseCtx); + if (!expr.parts.length) { + return rootCode; + } + let code: string[] = [rootCode]; + const numParts = expr.parts.length; + const dataVars = this.acquireVars(numParts); + const indexVars = this.acquireVars(numParts); + const itemVars = this.acquireVars(numParts); + const resultVar = this.acquireVar(); + code.push(resultVar, '= [];'); + code.push(dataVars[0], '=', dest, ';'); + for (let i = 0; i < numParts; i++) { + const part = expr.parts[i]; + const idx = indexVars[i]; + const item = itemVars[i]; + const data = dataVars[i]; + code.push(this.prepareDataForPathPart(part, data)); + code.push(`for(${idx}=0; ${idx}<${data}.length; ${idx}++) {`); + code.push(`${item} = ${data}[${idx}];`); + if (i > 0 && expr.parts[i - 1].context) { + code.push(this.translatePathContext(expr.parts[i - 1].context, item, idx)); + } + code.push(this.translateExpr(part, item, item)); + code.push(`if(!${item}) { continue; }`); + if (i < numParts - 1) { + code.push(dataVars[i + 1], '=', item, ';'); + } else { + code.push(JsonTemplateTranslator.covertToArrayValue(item)); + code.push(`${resultVar} = ${resultVar}.concat(${item});`); + } + } + for (let i = 0; i < numParts; i++) { + code.push('}'); + } + this.releaseVars(...indexVars); + this.releaseVars(...itemVars); + this.releaseVars(...dataVars); + this.releaseVars(resultVar); + code.push(dest, '=', JsonTemplateTranslator.returnSingleValueIfSafe(resultVar), ';'); + return code.join(''); } - private translateFunctionExpr(expr: FunctionExpression, dest: string, ctx: string) { - this.body.push(dest, '=(', expr.params.join(','), ') => {'); - const returnVal = this.acquireVar(); - this.body.push(`let ${returnVal} = undefined;`); - this.translateStatementsExpr(expr.body, returnVal, ctx); - this.body.push('return ', returnVal, ';};'); - this.releaseVars(returnVal); + private translateCurrentSelector(expr: SelectorExpression, dest, ctx) { + const code: string[] = []; + const prop = expr.prop?.value; + if (prop === '*') { + const valuesCode = JsonTemplateTranslator.returnObjectValues(ctx); + code.push(`${dest} = ${valuesCode}.flat();`); + } else if (prop) { + const propStr = CommonUtils.escapeStr(prop); + code.push(`if(${ctx} && Object.prototype.hasOwnProperty.call(${ctx}, ${propStr})){`); + code.push(`${dest}=${ctx}[${propStr}];`); + code.push('} else {'); + code.push(`${dest} = undefined`); + code.push('}'); + } + return code.join(''); } - private getFunctionName(expr: FunctionCallExpression, dest: string) { - let functionName = expr.dot ? `${dest}.${expr.id}` : (expr.id || dest); - if (expr.isNew) { - functionName = `new ${functionName}`; + private translateSelector(expr: SelectorExpression, dest: string, ctx: string): string { + if (expr.selector === '.') { + return this.translateCurrentSelector(expr, dest, ctx); } - return functionName; + return this.translateDescendantSelector(expr, dest, ctx); } - private translateFunctionCallExpr(expr: FunctionCallExpression, dest: string, ctx: string) { - const vars: string[] = []; - const functionArgs: string[] = []; - for (let arg of expr.args) { - const varName = this.acquireVar(); - this.translateExpr(arg.value, varName, ctx); - vars.push(varName); - functionArgs.push(arg.spread ? `...${varName}`: varName); + private translateDescendantSelector( + expr: SelectorExpression, + dest: string, + baseCtx: string, + ): string { + let code: string[] = []; + const ctxs = this.acquireVar(); + const currCtx = this.acquireVar(); + const result = this.acquireVar(); + + code.push(`${result} = [];`); + const { prop } = expr; + const propStr = CommonUtils.escapeStr(prop?.value); + code.push(`${ctxs}=[${baseCtx}];`); + code.push(`while(${ctxs}.length > 0) {`); + code.push(`${currCtx} = ${ctxs}.shift();`); + code.push(`if(!${currCtx}){continue;}`); + code.push(`if(Array.isArray(${currCtx})){`); + code.push(`${ctxs} = ${ctxs}.concat(${currCtx});`); + code.push('continue;'); + code.push('}'); + code.push(`if(typeof ${currCtx} === "object") {`); + const valuesCode = JsonTemplateTranslator.returnObjectValues(currCtx); + code.push(`${ctxs} = ${ctxs}.concat(${valuesCode});`); + if (prop) { + if (prop?.value === '*') { + code.push(`${result} = ${result}.concat(${valuesCode});`); + } else { + code.push(`if(Object.prototype.hasOwnProperty.call(${currCtx}, ${propStr})){`); + code.push(`${result}.push(${currCtx}[${propStr}]);`); + code.push('}'); + } + } + code.push('}'); + if (!prop) { + code.push(`${result}.push(${currCtx});`); } + code.push('}'); + code.push(`${dest} = ${result}.flat();`); + return code.join(''); + } - this.body.push(dest, '=', this.getFunctionName(expr, dest), '(', functionArgs.join(','), ');'); - this.releaseVars(...vars); + private translateFunctionExpr(expr: FunctionExpression, dest: string, ctx: string): string { + let code: string[] = []; + const fnHead = expr.async ? 'async function' : 'function'; + code.push(dest, '=', fnHead, '(', (expr.params || []).join(','), '){'); + const fnTranslator = new JsonTemplateTranslator(expr.body); + code.push(fnTranslator.translate(FUNCTION_RESULT_KEY, ctx)); + code.push('}'); + if (expr.block) { + code.push('()'); + } + code.push(';'); + return code.join(''); } - private translateObjectExpr(expr: ObjectExpression, dest: string, ctx: string) { + private getFunctionName(expr: FunctionCallExpression, ctx: string): string { + return expr.dot ? `${ctx}.${expr.id}` : expr.id || ctx; + } + + private translateFunctionCallExpr( + expr: FunctionCallExpression, + dest: string, + ctx: string, + ): string { + let code: string[] = []; + const resultVar = this.acquireVar(); + code.push(resultVar, '=', ctx, ';'); + if (expr.object) { + code.push(this.translateExpr(expr.object, resultVar, ctx)); + } + if (!expr.id) { + code.push(JsonTemplateTranslator.convertToSingleValue(resultVar)); + } + const functionArgsStr = this.translateSpreadableExpressions(expr.args, resultVar, code); + code.push(dest, '=', this.getFunctionName(expr, resultVar), '(', functionArgsStr, ');'); + return code.join(''); + } + + private translateObjectExpr(expr: ObjectExpression, dest: string, ctx: string): string { + let code: string[] = []; const propExprs: string[] = []; const vars: string[] = []; for (let prop of expr.props) { + const propParts: string[] = []; + if (prop.key) { + if (typeof prop.key !== 'string') { + const keyVar = this.acquireVar(); + code.push(this.translateExpr(prop.key, keyVar, ctx)); + propParts.push(`[${keyVar}]`); + vars.push(keyVar); + } else { + propParts.push(prop.key); + } + propParts.push(':'); + } const valueVar = this.acquireVar(); - let key = prop.key; - if (typeof prop.key !== 'string') { - const keyVar = this.acquireVar(); - this.translateExpr(prop.key, keyVar, ctx); - key = `[${keyVar}]`; - vars.push(keyVar); + code.push(this.translateExpr(prop.value, valueVar, ctx)); + if (prop.value.type === SyntaxType.SPREAD_EXPR) { + propParts.push('...'); } - this.translateExpr(prop.value, valueVar, ctx); - propExprs.push(`${key}:${valueVar}`); + propParts.push(valueVar); + propExprs.push(propParts.join('')); vars.push(valueVar); } - this.body.push(dest, '={', propExprs.join(','), '};'); + code.push(dest, '={', propExprs.join(','), '};'); this.releaseVars(...vars); + return code.join(''); } - private translateArrayExpr(expr: ArrayExpression, dest: string, ctx: string) { - const vars = expr.elements.map((arg) => { + private translateSpreadableExpressions(items: Expression[], ctx: string, code: string[]): string { + const vars: string[] = []; + const itemParts: string[] = []; + for (let item of items) { const varName = this.acquireVar(); - this.translateExpr(arg, varName, ctx); - return varName; - }); - this.body.push(dest, '=[', vars.join(','), '];'); + code.push(this.translateExpr(item, varName, ctx)); + itemParts.push(item.type === SyntaxType.SPREAD_EXPR ? `...${varName}` : varName); + vars.push(varName); + } this.releaseVars(...vars); + return itemParts.join(','); } - private translateLiteralExpr(expr: LiteralExpression, dest: string, _ctx: string) { - this.body.push(dest, '='); - this.translateLiteral(expr.value); - this.body.push(';'); + private translateArrayExpr(expr: ArrayExpression, dest: string, ctx: string): string { + const code: string[] = []; + const elementsStr = this.translateSpreadableExpressions(expr.elements, ctx, code); + code.push(`${dest} = [${elementsStr}];`); + return code.join(''); } - private translateAssignmentExpr(expr: AssignmentExpression, _dest: string, ctx: string) { - const varName = this.acquireVar(); - this.translateExpr(expr.value, varName, ctx); - this.body.push(`${expr.operator || ""} ${expr.id}=${varName};`) - this.releaseVars(varName); + private translateLiteralExpr(expr: LiteralExpression, dest: string, _ctx: string): string { + const literalCode = this.translateLiteral(expr.tokenType, expr.value); + return `${dest} = ${literalCode};`; } - private translateStatementsExpr(expr: StatementsExpression, dest: string, ctx: string) { - for (let statement of expr.statements) { - this.translateExpr(statement, dest, ctx); + private getSelectorAssignmentPart(expr: SelectorExpression): string { + if (!JsonTemplateTranslator.isValidSelectorForAssignment(expr)) { + throw new JsosTemplateTranslatorError('Invalid assignment path'); + } + if (expr.prop?.type === TokenType.STR) { + return `[${CommonUtils.escapeStr(expr.prop?.value)}]`; + } else { + return `.${expr.prop?.value}`; + } + } + private getArrayIndexAssignmentPart( + expr: IndexFilterExpression, + code: string[], + ctx: string, + ): string { + if (expr.indexes.elements.length > 1) { + throw new JsosTemplateTranslatorError('Invalid assignment path'); } + const keyVar = this.acquireVar(); + code.push(this.translateExpr(expr.indexes.elements[0], keyVar, ctx)); + this.releaseVars(keyVar); + return `[${keyVar}]`; } - private translateComparisonExpr(expr: BinaryExpression, dest: string, ctx: string) { - const val1 = this.acquireVar(); - const val2 = this.acquireVar(); - const isVal1Array = this.acquireVar(); - const isVal2Array = this.acquireVar(); - const i = this.acquireVar(); - const j = this.acquireVar(); - const len1 = this.acquireVar(); - const len2 = this.acquireVar(); - const leftArg = expr.args[0]; - const rightArg = expr.args[1]; - - this.body.push(dest, '= false;'); - - this.translateExpr(leftArg, val1, ctx); - this.translateExpr(rightArg, val2, ctx); - - const isLeftArgPath = leftArg.type === SyntaxType.PATH; - const isRightArgLiteral = rightArg.type === SyntaxType.LITERAL; - - this.body.push(isVal1Array, '='); - isLeftArgPath ? this.body.push('true;') : this.body.push('Array.isArray(', val1, ');'); - - this.body.push(isVal2Array, '='); - isRightArgLiteral ? this.body.push('false;') : this.body.push('Array.isArray(', val2, ');'); - - this.body.push('if('); - isLeftArgPath || this.body.push(isVal1Array, '&&'); - this.body.push(val1, '.length === 1) {', val1, '=', val1, '[0];', isVal1Array, '= false;', '}'); - isRightArgLiteral || - this.body.push( - 'if(', - isVal2Array, - '&&', - val2, - '.length === 1) {', - val2, - '=', - val2, - '[0];', - isVal2Array, - '= false;', - '}', - ); - - this.body.push(i, '= 0;', 'if(', isVal1Array, ') {', len1, '=', val1, '.length;'); - - if (!isRightArgLiteral) { - this.body.push( - 'if(', - isVal2Array, - ') {', - len2, - '=', - val2, - '.length;', - 'while(', - i, - '<', - len1, - '&& !', - dest, - ') {', - j, - '= 0;', - 'while(', - j, - '<', - len2, - ') {', - ); - this.writeCondition(expr.op, [val1, '[', i, ']'].join(''), [val2, '[', j, ']'].join('')); - this.body.push( - dest, - '= true;', - 'break;', - '}', - '++', - j, - ';', - '}', - '++', - i, - ';', - '}', - '}', - 'else {', - ); - } - this.body.push('while(', i, '<', len1, ') {'); - this.writeCondition(expr.op, [val1, '[', i, ']'].join(''), val2); - this.body.push(dest, '= true;', 'break;', '}', '++', i, ';', '}'); - - isRightArgLiteral || this.body.push('}'); - - this.body.push('}'); - - if (!isRightArgLiteral) { - this.body.push( - 'else if(', - isVal2Array, - ') {', - len2, - '=', - val2, - '.length;', - 'while(', - i, - '<', - len2, - ') {', - ); - this.writeCondition(expr.op, val1, [val2, '[', i, ']'].join('')); - this.body.push(dest, '= true;', 'break;', '}', '++', i, ';', '}', '}'); - } - - this.body.push('else {', dest, '=', binaryOperators[expr.op](val1, val2), ';', '}'); - - this.releaseVars(val1, val2, isVal1Array, isVal2Array, i, j, len1, len2); - } - - private inlineAppendToArray(result, val, tmpArr?, len?) { - this.body.push(` - if(${val} !== undefined) { - if(Array.isArray(${val})) { - `); - if (tmpArr) { - this.body.push(len, '> 1?'); - this.inlinePushToArray(tmpArr, val); - this.body.push(':'); - } - this.body.push( - `${result} = ${result}.length ? ${result}.concat(${val}) : ${val}.slice(); - } else {`, - ); - tmpArr && - this.body.push( - `if(${tmpArr}.length) { - ${result} = ${result}.concat(tmpArr); - ${tmpArr} = []; - }` - ); - this.inlinePushToArray(result, val); - this.body.push(';', '}', '}'); + private translateAssignmentExpr(expr: AssignmentExpression, dest: string, ctx: string): string { + const code: string[] = []; + const valueVar = this.acquireVar(); + code.push(this.translateExpr(expr.value, valueVar, ctx)); + const assignmentPathParts: string[] = []; + const root = expr.path.root; + if (!root || root === DATA_PARAM_KEY || typeof root === 'object') { + throw new JsosTemplateTranslatorError('Invalid assignment path'); + } + assignmentPathParts.push(root); + for (let part of expr.path.parts) { + switch (part.type) { + case SyntaxType.SELECTOR: + assignmentPathParts.push(this.getSelectorAssignmentPart(part as SelectorExpression)); + break; + case SyntaxType.ARRAY_INDEX_FILTER_EXPR: + assignmentPathParts.push( + this.getArrayIndexAssignmentPart(part as IndexFilterExpression, code, ctx), + ); + break; + default: + throw new JsosTemplateTranslatorError('Invalid assignment path'); + } + } + const assignmentPath = assignmentPathParts.join(''); + code.push(`${assignmentPath}=${valueVar};`); + code.push(`${dest} = ${valueVar};`); + this.releaseVars(valueVar); + return code.join(''); } - private inlinePushToArray(result, val) { - this.body.push(result, '.length?', result, '.push(', val, ') :', result, '[0] =', val); + private translateDefinitionVars(expr: DefinitionExpression): string { + let vars: string[] = [expr.vars.join(',')]; + if (expr.fromObject) { + vars.unshift('{'); + vars.push('}'); + } + return vars.join(''); } - private translateLiteral(val) { - this.body.push( - typeof val === 'string' ? JsonTemplateTranslator.escapeStr(val) : val === null ? 'null' : val, - ); + private translateDefinitionExpr(expr: DefinitionExpression, dest: string, ctx: string): string { + const code: string[] = []; + const valueVar = this.acquireVar(); + code.push(this.translateExpr(expr.value, valueVar, ctx)); + const defVars = this.translateDefinitionVars(expr); + code.push(`${expr.definition} ${defVars}=${valueVar};`); + code.push(`${dest} = ${valueVar};`); + this.releaseVars(valueVar); + return code.join(''); } - private translateSelector(sel: SelectorExpression, dest: string, ctx: string) { - if(sel.selector === '...') { - return this.translateDescendantSelector(sel, dest, dest); - } - - if (sel.prop) { - const propStr = JsonTemplateTranslator.escapeStr(sel.prop); - const result = this.acquireVar(); - const i = this.acquireVar(); - const len = this.acquireVar(); - const curCtx = this.acquireVar(); - const j = this.acquireVar(); - const val = this.acquireVar(); - const tmpArr = this.acquireVar(); - - this.body.push(JsonTemplateTranslator.covertToArrayValue(dest)); - - this.body.push( - result, - '= [];', - i, - '= 0;', - len, - '=', - ctx, - '.length;', - tmpArr, - '= [];', - 'while(', - i, - '<', - len, - ') {', - curCtx, - '=', - ctx, - '[', - i, - '++];', - 'if(', - curCtx, - '!= null) {', - ); - if (sel.prop === '*') { - this.body.push( - 'if(typeof ', - curCtx, - '=== "object") {', - 'if(Array.isArray(', - curCtx, - ')) {', - result, - '=', - result, - '.concat(', - curCtx, - ');', - '}', - 'else {', - 'for(', - j, - ' in ', - curCtx, - ') {', - 'if(', - curCtx, - '.hasOwnProperty(', - j, - ')) {', - val, - '=', - curCtx, - '[', - j, - '];', - ); - this.inlineAppendToArray(result, val); - this.body.push('}', '}', '}', '}'); - } else { - this.body.push(val, '=', curCtx, '[', propStr, '];'); - this.inlineAppendToArray(result, val, tmpArr, len); - } - this.body.push( - '}', - '}', - dest, - '=', - len, - '> 1 &&', - tmpArr, - '.length?', - tmpArr, - '.length > 1?', - 'concat.apply(', - result, - ',', - tmpArr, - ') :', - result, - '.concat(', - tmpArr, - '[0]) :', - result, - ';', - ); - - this.releaseVars(result, i, len, curCtx, j, val, tmpArr); - } - } - - private translateUnaryExpr(expr: UnaryExpression, dest: string, ctx: string) { - const val = this.acquireVar(); - const { arg } = expr; + private translateStatementsExpr(expr: StatementsExpression, dest: string, ctx: string): string { + return this.translateStatements(expr.statements, dest, ctx); + } - this.translateExpr(arg, val, ctx); + private translateStatements(statements: Expression[], dest: string, ctx: string): string { + return statements.map((statement) => this.translateExpr(statement, dest, ctx)).join(''); + } - switch (expr.op) { - case '!': - this.body.push(dest, '= !', JsonTemplateTranslator.convertToBool(arg, val), ';'); - break; + private getLogicalConditionCode(type: SyntaxType, varName: string): string { + switch (type) { + case SyntaxType.LOGICAL_COALESCE_EXPR: + return `${varName} !== null && ${varName} !== undefined`; + case SyntaxType.LOGICAL_OR_EXPR: + return varName; + default: + return `!${varName}`; + } + } - case '-': - this.body.push(dest, '= -', JsonTemplateTranslator.convertToSingleValue(arg, val), ';'); - break; - - case Keyword.TYPEOF: - this.body.push(dest, '=typeof ', val, ';'); - break; + private translateLogicalExpr(expr: BinaryExpression, dest: string, ctx: string): string { + const val1 = this.acquireVar(); + const code: string[] = []; + code.push(this.translateExpr(expr.args[0], val1, ctx)); + const condition = this.getLogicalConditionCode(expr.type, val1); + code.push(`if(${condition}) {`); + code.push(`${dest} = ${val1};`); + code.push('} else {'); + const val2 = this.acquireVar(); + code.push(this.translateExpr(expr.args[1], val2, ctx)); + code.push(`${dest} = ${val2};`); + code.push('}'); + this.releaseVars(val1, val2); + return code.join(''); + } + + private translateINExpr(expr: BinaryExpression, dest: string, ctx: string): string { + const code: string[] = []; + const val1 = this.acquireVar(); + const val2 = this.acquireVar(); + const resultVar = this.acquireVar(); + code.push(this.translateExpr(expr.args[0], val1, ctx)); + code.push(this.translateExpr(expr.args[1], val2, ctx)); + const inCode = `(Array.isArray(${val2}) ? ${val2}.includes(${val1}) : ${val1} in ${val2})`; + code.push(`${resultVar} = ${inCode};`); + code.push(`${dest} = ${resultVar};`); + return code.join(''); + } + + private translateLiteral(type: TokenType, val: any): string { + if (type === TokenType.STR) { + return CommonUtils.escapeStr(val); } + return String(val); + } + private translateUnaryExpr(expr: UnaryExpression, dest: string, ctx: string): string { + const code: string[] = []; + const val = this.acquireVar(); + code.push(this.translateExpr(expr.arg, val, ctx)); + code.push(`${dest} = ${expr.op} ${val};`); this.releaseVars(val); - } - private static escapeStr(s) { - return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; + return code.join(''); } - private static convertToBool(arg, varName) { - switch (arg.type) { - case SyntaxType.LOGICAL_EXPR: - return varName; + private static isArrayFilterExpr(expr: Expression): boolean { + return ( + expr.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR || expr.type === SyntaxType.RANGE_FILTER_EXPR + ); + } - case SyntaxType.LITERAL: - return `!!${varName}`; + private static isValidSelectorForAssignment(expr: SelectorExpression): boolean { + return expr.selector === '.' && !!expr.prop && expr.prop.type !== TokenType.PUNCT; + } - case SyntaxType.PATH: - return `${varName}.length > 0`; + private static isSinglePropSelection(expr: Expression): boolean { + if (expr.type === SyntaxType.SELECTOR) { + const part = expr as SelectorExpression; + return part.selector === '.' && + (part.prop?.type === TokenType.ID || + part.prop?.type === TokenType.STR); + } + return false; + } - default: - return [ - '(typeof ', - varName, - '=== "boolean"?', - varName, - ':', - 'Array.isArray(', - varName, - ')?', - varName, - '.length > 0 : !!', - varName, - ')', - ].join(''); - } - } - - private static convertToSingleValue(arg, varName) { + private static returnObjectValues(varName: string): string { + 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; } @@ -684,163 +605,125 @@ export class JsonTemplateTranslator { return `(Array.isArray(${varName}) ? ${varName}[0] : ${varName})`; } - private static covertToArrayValue(varName) { - return `(Array.isArray(${varName}) || (${varName} = [${varName}]));`; - } - - private translateObjectFilterExpression(expr: ObjectFilterExpression, dest: string, ctx: string) { - for (let filter of expr.filters) { - const resVar = this.acquireVar(); - const i = this.acquireVar(); - const len = this.acquireVar(); - const cond = this.acquireVar(); - const curItem = this.acquireVar(); - - this.body.push(JsonTemplateTranslator.covertToArrayValue(dest)); - - this.body.push( - resVar, - '= [];', - i, - '= 0;', - len, - '=', - ctx, - '.length;', - 'while(', - i, - '<', - len, - ') {', - curItem, - '=', - ctx, - '[', - i, - '++];', - ); - this.translateExpr(filter, cond, curItem); - this.body.push( - JsonTemplateTranslator.convertToBool(filter, cond), - '&&', - resVar, - '.push(', - curItem, - ');', - '}', - dest, - '=', - resVar, - ';', - ); - - this.releaseVars(resVar, i, len, curItem, cond); - } - } - - private translatePosFilterExpression(expr: PosFilterExpression, dest: string, ctx: string) { - if (expr.empty) { - return; - } - this.body.push(JsonTemplateTranslator.covertToArrayValue(dest)); - if (expr.idx) { - const idx = this.acquireVar(); - this.translateExpr(expr.idx, idx, ctx); - this.body.push( - `(typeof ${idx} === "number" && ${idx} < 0 && (${idx} = ${ctx}.length + ${idx}));`, - ); - this.body.push(`${dest} = ${ctx}[${idx}];`); - this.releaseVars(idx); - return; + private static convertToSingleValue(varName: string): string { + return `${varName} = Array.isArray(${varName}) ? ${varName}[0] : ${varName};`; + } + + private static returnSingleValueIfSafe(varName: string): string { + return `(${varName}.length === 1 ? ${varName}[0] : ${varName})`; + } + + private static covertToArrayValue(varName: string) { + return `${varName} = Array.isArray(${varName}) ? ${varName} : [${varName}];`; + } + + private translateObjectFilterExpr(expr: FilterExpression, dest: string, ctx: string): string { + const code: string[] = []; + const condition = this.acquireVar(); + code.push(this.translateExpr(expr.filter, condition, ctx)); + code.push(`if(!${condition}) {${dest} = undefined;}`); + this.releaseVars(condition); + return code.join(''); + } + + private translateObjectIndexFilterExpr( + ctx: string, + allKeys: string, + resultVar: string, + shouldExclude?: boolean, + ): string { + const code: string[] = []; + if (shouldExclude) { + code.push(`${allKeys}=Object.keys(${ctx}).filter(key => !${allKeys}.includes(key));`); } + code.push(`${resultVar} = {};`); + code.push(`for(let key of ${allKeys}){`); + code.push( + `if(Object.prototype.hasOwnProperty.call(${ctx}, key)){${resultVar}[key] = ${ctx}[key];}`, + ); + code.push('}'); + return code.join(''); + } + private translateArrayIndexFilterExpr(ctx: string, allKeys: string, resultVar: string): string { + const code: string[] = []; + code.push(`${resultVar} = [];`); + code.push(`for(let key of ${allKeys}){`); + code.push(`if(typeof key === 'string'){`); + code.push(`for(let childCtx of ${ctx}){`); + code.push(`if(Object.prototype.hasOwnProperty.call(childCtx, key)){`); + code.push(`${resultVar}.push(childCtx[key]);`); + code.push('}'); + code.push('}'); + code.push('continue;'); + code.push('}'); + code.push(`if(key < 0){key = ${ctx}.length + key;}`); + code.push( + `if(Object.prototype.hasOwnProperty.call(${ctx}, key)){${resultVar}.push(${ctx}[key]);}`, + ); + code.push('}'); + code.push(`if(${allKeys}.length === 1) {${resultVar} = ${resultVar}[0];}`); + return code.join(''); + } + + private translateIndexFilterExpr(expr: IndexFilterExpression, dest: string, ctx: string): string { + const code: string[] = []; + const allKeys = this.acquireVar(); + code.push(this.translateArrayExpr(expr.indexes, allKeys, ctx)); + code.push(`${allKeys} = ${allKeys}.flat();`); + const resultVar = this.acquireVar(); + if (expr.type === SyntaxType.OBJECT_INDEX_FILTER_EXPR) { + code.push(this.translateObjectIndexFilterExpr(ctx, allKeys, resultVar, expr.exclude)); + } else { + code.push(this.translateArrayIndexFilterExpr(ctx, allKeys, resultVar)); + } + code.push(`${dest}=${resultVar};`); + this.releaseVars(allKeys); + this.releaseVars(resultVar); + return code.join(''); + } + + private translateRangeFilterExpr(expr: RangeFilterExpression, dest: string, ctx: string): string { + const code: string[] = []; let fromIdx, toIdx; if (expr.fromIdx) { if (expr.toIdx) { - this.translateExpr(expr.fromIdx, (fromIdx = this.acquireVar()), ctx); - this.translateExpr(expr.toIdx, (toIdx = this.acquireVar()), ctx); - this.body.push(dest, '=', ctx, '.slice(', fromIdx, ',', toIdx, ');'); + code.push(this.translateExpr(expr.fromIdx, (fromIdx = this.acquireVar()), ctx)); + code.push(this.translateExpr(expr.toIdx, (toIdx = this.acquireVar()), ctx)); + code.push(dest, '=', ctx, '.slice(', fromIdx, ',', toIdx, ');'); this.releaseVars(fromIdx, toIdx); } else { - this.translateExpr(expr.fromIdx, (fromIdx = this.acquireVar()), ctx); - this.body.push(dest, '=', ctx, '.slice(', fromIdx, ');'); + code.push(this.translateExpr(expr.fromIdx, (fromIdx = this.acquireVar()), ctx)); + code.push(dest, '=', ctx, '.slice(', fromIdx, ');'); this.releaseVars(fromIdx); } } else if (expr.toIdx) { - this.translateExpr(expr.toIdx, (toIdx = this.acquireVar()), ctx); - this.body.push(dest, '=', ctx, '.slice(0,', toIdx, ');'); + code.push(this.translateExpr(expr.toIdx, (toIdx = this.acquireVar()), ctx)); + code.push(dest, '=', ctx, '.slice(0,', toIdx, ');'); this.releaseVars(toIdx); } + return code.join(''); } - private writeCondition(op: string, val1: any, val2: any) { - this.body.push('if(', binaryOperators[op](val1, val2), ') {'); - } - - private translateMathExpr(expr: BinaryExpression, dest: string, ctx: string) { + private translateBinaryExpr(expr: BinaryExpression, dest: string, ctx: string): string { const val1 = this.acquireVar(); const val2 = this.acquireVar(); const { args } = expr; + const code: string[] = []; + code.push(this.translateExpr(args[0], val1, ctx)); + code.push(this.translateExpr(args[1], val2, ctx)); - this.translateExpr(args[0], val1, ctx); - this.translateExpr(args[1], val2, ctx); - - this.body.push( + code.push( dest, '=', binaryOperators[expr.op]( - JsonTemplateTranslator.convertToSingleValue(args[0], val1), - JsonTemplateTranslator.convertToSingleValue(args[1], val2), + JsonTemplateTranslator.returnSingleValue(args[0], val1), + JsonTemplateTranslator.returnSingleValue(args[1], val2), ), ';', ); this.releaseVars(val1, val2); - } - - private translateLogicalExpr(expr: BinaryExpression, dest: string, ctx: string) { - const conditionVars: any[] = []; - const { args } = expr; - let len = args.length; - let i = 0; - let val; - - this.body.push(dest, '= false;'); - switch (expr.op) { - case '&&': - while (i < len) { - val = this.acquireVar(); - conditionVars.push(val); - this.translateExpr(args[i], val, ctx); - this.body.push('if(', JsonTemplateTranslator.convertToBool(args[i++], val), ') {'); - } - this.body.push(dest, '= true;'); - break; - - case '||': - while (i < len) { - conditionVars.push((val = this.acquireVar())); - this.translateExpr(args[i], val, ctx); - this.body.push( - 'if(', - JsonTemplateTranslator.convertToBool(args[i], val), - ') {', - dest, - '= true;', - '}', - ); - if (i++ + 1 < len) { - this.body.push('else {'); - } - } - --len; - break; - } - - while (len--) { - this.body.push('}'); - } - - this.releaseVars(conditionVars); + return code.join(''); } } diff --git a/src/types.ts b/src/types.ts index 510ce8c..9675080 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,15 @@ export type Dictionary = Record; export enum Keyword { - FUNCTION = "function", - NEW = "new", - TYPEOF = "typeof", - RETURN = "return", - LET = "let", - CONST = "const", + FUNCTION = 'function', + NEW = 'new', + TYPEOF = 'typeof', + LET = 'let', + CONST = 'const', + LAMBDA = 'lambda', + AWAIT = 'await', + ASYNC = 'async', + IN = 'in', } export enum TokenType { @@ -16,31 +19,60 @@ export enum TokenType { STR, BOOL, NULL, + UNDEFINED, + LAMBDA_ARG, PUNCT, THROW, - OPERATOR, + KEYWORD, EOT, } +// In the order of precedence +export enum OperatorType { + BASE, + CONDITIONAL, + ASSIGNMENT, + COALESCING, + OR, + AND, + EQUALITY, + RELATIONAL, + SHIFT, + ADDITION, + MULTIPLICATION, + POWER, + UNARY, +} + export enum SyntaxType { EMPTY, PATH, SELECTOR, - LOGICAL_EXPR, + LAMBDA_ARG, + LITERAL, + LOGICAL_COALESCE_EXPR, + LOGICAL_OR_EXPR, + LOGICAL_AND_EXPR, COMPARISON_EXPR, + IN_EXPR, MATH_EXPR, - CONCAT_EXPR, UNARY_EXPR, - POS_FILTER_EXPR, + SPREAD_EXPR, + ARRAY_INDEX_FILTER_EXPR, + OBJECT_INDEX_FILTER_EXPR, + RANGE_FILTER_EXPR, OBJECT_FILTER_EXPR, + DEFINTION_EXPR, ASSIGNMENT_EXPR, - LITERAL, + OBJECT_PROP_EXPR, OBJECT_EXPR, + TO_ARRAY_EXPR, ARRAY_EXPR, FUNCTION_EXPR, - FUNCTION_CALL_ARG_EXPR, + FUNCTION_CALL_ARG, FUNCTION_CALL_EXPR, STATEMENTS_EXPR, + CONDITIONAL_EXPR, } export type Token = { @@ -54,12 +86,23 @@ export interface Expression { [key: string]: any; } +export interface LambdaArgExpression extends Expression { + index: number; +} + export interface FunctionExpression extends Expression { - params: string[]; + params?: string[]; body: StatementsExpression; + block?: boolean; + async?: boolean; } +export interface ObjectPropExpression extends Expression { + key?: Expression | string; + value: Expression; +} + export interface ObjectExpression extends Expression { - props: { key: Expression | string; value: Expression }[]; + props: ObjectPropExpression[]; } export interface ArrayExpression extends Expression { @@ -70,10 +113,6 @@ export interface StatementsExpression extends Expression { statements: Expression[]; } -export interface ObjectPredicateExpression extends Expression { - arg: Expression; -} - export interface UnaryExpression extends Expression { arg: Expression; op: string; @@ -87,42 +126,68 @@ export interface BinaryExpression extends Expression { export interface ConcatExpression extends Expression { args: Expression[]; } + export interface AssignmentExpression extends Expression { - id: string; + path: PathExpression; value: Expression; - operator?: string; } -export interface PosFilterExpression extends Expression { +export interface DefinitionExpression extends Expression { + vars: string[]; + fromObject?: boolean; + value: Expression; + definition: string; +} + +export interface RangeFilterExpression extends Expression { fromIdx?: Expression; toIdx?: Expression; - idx?: Expression; - empty?: boolean; } -export interface ObjectFilterExpression extends Expression { - filters: Expression[]; +export interface IndexFilterExpression extends Expression { + indexes: ArrayExpression; + exclude?: boolean; } +export interface FilterExpression extends Expression { + filter: Expression; +} + export interface LiteralExpression extends Expression { - value: string | number | boolean | null; + value: string | number | boolean | null | undefined; tokenType: TokenType; } export interface PathExpression extends Expression { parts: Expression[]; - root?: string; + root?: Expression | string; + // Used in a part of another Path + subPath?: boolean; } +export interface ContextVariable { + item?: string; + index?: string; +} export interface SelectorExpression extends Expression { selector: string; - prop?: string; + prop?: Token; + context?: ContextVariable; } -export interface FunctionCallArgExpression extends Expression { +export interface SpreadExpression extends Expression { value: Expression; - spread?: boolean; } +export interface ToArrayExpression extends Expression { + value: Expression; +} + export interface FunctionCallExpression extends Expression { - args: FunctionCallArgExpression[]; + args: Expression[]; + object?: Expression; id?: string; dot?: boolean; - isNew?: boolean; +} + +export interface ConditionalExpression extends Expression { + if: Expression; + then: Expression; + else: Expression; } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..3f36fb2 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,31 @@ +import { Expression, StatementsExpression, SyntaxType } from './types'; + +export class CommonUtils { + static toArray(val: any): any[] { + return Array.isArray(val) ? val : [val]; + } + + static getLastElement(arr: T[]): T | undefined { + if (!arr.length) { + return undefined; + } + return arr[arr.length - 1]; + } + static convertToStatementsExpr(...expressions: Expression[]): StatementsExpression { + return { + type: SyntaxType.STATEMENTS_EXPR, + statements: expressions, + }; + } + + static CreateAsyncFunction(...args) { + return async function () {}.constructor(...args); + } + + static escapeStr(s?: string): string { + if (typeof s !== 'string') { + return ''; + } + return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; + } +} diff --git a/test/e2e.test.ts b/test/e2e.test.ts new file mode 100644 index 0000000..941260f --- /dev/null +++ b/test/e2e.test.ts @@ -0,0 +1,35 @@ +import { readdirSync } from 'fs'; +import { join } from 'path'; +import { Command } from 'commander'; +import { SceanarioUtils } from './utils'; + +const rootDirName = 'scenarios'; +const command = new Command(); +command.allowUnknownOption().option('--scenarios ', 'Enter Scenario Names', 'all').parse(); + +const opts = command.opts(); +let scenarios = opts.scenarios.split(/[, ]/); + +if (scenarios[0] === 'all') { + scenarios = readdirSync(join(__dirname, rootDirName)); +} + +describe('Scenarios tests', () => { + scenarios.forEach((scenarioName) => { + describe(`${scenarioName}`, () => { + const scenarioDir = join(__dirname, rootDirName, scenarioName); + const sceanarios = SceanarioUtils.extractScenarios(scenarioDir); + sceanarios.forEach((scenario, index) => { + it(`Scenario ${index}`, async () => { + try { + const templateEngine = SceanarioUtils.createTemplateEngine(scenarioDir, scenario); + const result = await SceanarioUtils.evaluateScenario(templateEngine, scenario); + expect(result).toEqual(scenario.output); + } catch (error: any) { + expect(error.message).toContain(scenario.error); + } + }); + }); + }); + }); +}); diff --git a/test/main.test.ts b/test/main.test.ts deleted file mode 100644 index a9bd040..0000000 --- a/test/main.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('Main tests', () => { - it('Test 1', async () => { - expect(true).toEqual(true); - }); -}); diff --git a/test/scenarios/arrays/data.ts b/test/scenarios/arrays/data.ts new file mode 100644 index 0000000..c16397b --- /dev/null +++ b/test/scenarios/arrays/data.ts @@ -0,0 +1,7 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: [1, 2, 3, ['string1', 0.2], ['string2', 'string3', 'aa"a', true, false], 2], + }, +]; diff --git a/test/scenarios/arrays/template.jt b/test/scenarios/arrays/template.jt new file mode 100644 index 0000000..4ff8f0d --- /dev/null +++ b/test/scenarios/arrays/template.jt @@ -0,0 +1,6 @@ +let a = [ + "string1", `string2`, 'string3', "aa\"a", true, false, + undefined, null, .2, 0.22, [1, 2, 3], {"b": [1, 2]} +]; +[...a[-2], a[0,8], a[1:6], a[-1].b[1]] + diff --git a/test/scenarios/assignments/data.ts b/test/scenarios/assignments/data.ts new file mode 100644 index 0000000..5a6c832 --- /dev/null +++ b/test/scenarios/assignments/data.ts @@ -0,0 +1,12 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: { + a: { + b: [12, 22], + 'c key': 4, + }, + }, + }, +]; diff --git a/test/scenarios/assignments/template.jt b/test/scenarios/assignments/template.jt new file mode 100644 index 0000000..d04f342 --- /dev/null +++ b/test/scenarios/assignments/template.jt @@ -0,0 +1,10 @@ +let a = 10 +let b = -a + 30 +let cKey = "c key"; +let c = { a: { b: [a, b], [cKey]: 2 } } +let {d, e, f} = {d: 2, e: 2, f: 1} +c.a.b[0] = c.a.b[0] + d +c.a.b[1] = c.a.b[1] + e +c.a."c key" = c.a."c key" + f +c.a[cKey] = c.a[cKey] + f +c diff --git a/test/scenarios/bad_templates/bad_async_usage.jt b/test/scenarios/bad_templates/bad_async_usage.jt new file mode 100644 index 0000000..4bdf439 --- /dev/null +++ b/test/scenarios/bad_templates/bad_async_usage.jt @@ -0,0 +1 @@ +async abc \ No newline at end of file diff --git a/test/scenarios/bad_templates/bad_context_var.jt b/test/scenarios/bad_templates/bad_context_var.jt new file mode 100644 index 0000000..e5c66ce --- /dev/null +++ b/test/scenarios/bad_templates/bad_context_var.jt @@ -0,0 +1 @@ +.a@1#2 \ No newline at end of file diff --git a/test/scenarios/bad_templates/bad_function_params.jt b/test/scenarios/bad_templates/bad_function_params.jt new file mode 100644 index 0000000..9bf9f22 --- /dev/null +++ b/test/scenarios/bad_templates/bad_function_params.jt @@ -0,0 +1 @@ +function(1, 2, 3){} \ No newline at end of file diff --git a/test/scenarios/bad_templates/bad_function_rest_param.jt b/test/scenarios/bad_templates/bad_function_rest_param.jt new file mode 100644 index 0000000..94c6c49 --- /dev/null +++ b/test/scenarios/bad_templates/bad_function_rest_param.jt @@ -0,0 +1 @@ +function(a, ...b, c){} \ No newline at end of file diff --git a/test/scenarios/bad_templates/bad_number.jt b/test/scenarios/bad_templates/bad_number.jt new file mode 100644 index 0000000..6b4d157 --- /dev/null +++ b/test/scenarios/bad_templates/bad_number.jt @@ -0,0 +1 @@ +2.2.3 \ No newline at end of file diff --git a/test/scenarios/bad_templates/bad_string.jt b/test/scenarios/bad_templates/bad_string.jt new file mode 100644 index 0000000..55cb886 --- /dev/null +++ b/test/scenarios/bad_templates/bad_string.jt @@ -0,0 +1 @@ +"aaaa \ No newline at end of file diff --git a/test/scenarios/bad_templates/data.ts b/test/scenarios/bad_templates/data.ts new file mode 100644 index 0000000..0e98ce5 --- /dev/null +++ b/test/scenarios/bad_templates/data.ts @@ -0,0 +1,93 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + templatePath: 'bad_async_usage.jt', + error: 'Unexpected token', + }, + { + templatePath: 'bad_context_var.jt', + error: 'Unexpected token', + }, + { + templatePath: 'bad_function_params.jt', + error: 'Unexpected token', + }, + { + templatePath: 'bad_function_rest_param.jt', + error: 'Unexpected token', + }, + { + templatePath: 'bad_number.jt', + error: 'Unexpected token', + }, + { + templatePath: 'bad_string.jt', + error: 'Unexpected end of template', + }, + { + templatePath: 'empty_object_vars_for_definition.jt', + error: 'Empty object vars', + }, + { + templatePath: 'incomplete_statement.jt', + error: 'Unexpected end of template', + }, + { + templatePath: 'invalid_new_function_call.jt', + error: 'Unexpected token', + }, + { + templatePath: 'invalid_object_vars_for_definition.jt', + error: 'Invalid object vars', + }, + { + templatePath: 'invalid_variable_assignment1.jt', + error: 'Invalid assignment path', + }, + { + templatePath: 'invalid_variable_assignment2.jt', + error: 'Invalid assignment path', + }, + { + templatePath: 'invalid_variable_assignment3.jt', + error: 'Invalid assignment path', + }, + { + templatePath: 'invalid_variable_assignment4.jt', + error: 'Invalid assignment path', + }, + + { + templatePath: 'invalid_variable_assignment5.jt', + error: 'Invalid assignment path', + }, + { + templatePath: 'invalid_variable_definition.jt', + error: 'Invalid normal vars', + }, + { + templatePath: 'invalid_token_after_function_def.jt', + error: 'Unexpected token', + }, + { + templatePath: 'object_with_invalid_closing.jt', + error: 'Unexpected token', + }, + { + templatePath: 'object_with_invalid_key.jt', + error: 'Unexpected token', + }, + { + templatePath: 'reserved_id.jt', + error: 'Reserved ID pattern', + }, + { + templatePath: 'unknown_token.jt', + error: 'Unknown token', + }, + { + templatePath: 'unsupported_assignment.jt', + error: 'Unexpected token', + }, +]; diff --git a/test/scenarios/bad_templates/empty_object_vars_for_definition.jt b/test/scenarios/bad_templates/empty_object_vars_for_definition.jt new file mode 100644 index 0000000..8c260a8 --- /dev/null +++ b/test/scenarios/bad_templates/empty_object_vars_for_definition.jt @@ -0,0 +1 @@ +let {} = {a: 1} \ No newline at end of file diff --git a/test/scenarios/bad_templates/incomplete_statement.jt b/test/scenarios/bad_templates/incomplete_statement.jt new file mode 100644 index 0000000..917d0ed --- /dev/null +++ b/test/scenarios/bad_templates/incomplete_statement.jt @@ -0,0 +1 @@ +2 ** \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_new_function_call.jt b/test/scenarios/bad_templates/invalid_new_function_call.jt new file mode 100644 index 0000000..7f90474 --- /dev/null +++ b/test/scenarios/bad_templates/invalid_new_function_call.jt @@ -0,0 +1 @@ +new .a() \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_object_vars_for_definition.jt b/test/scenarios/bad_templates/invalid_object_vars_for_definition.jt new file mode 100644 index 0000000..8cacc3f --- /dev/null +++ b/test/scenarios/bad_templates/invalid_object_vars_for_definition.jt @@ -0,0 +1 @@ +let {"a"} = {a: 1} \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_token_after_function_def.jt b/test/scenarios/bad_templates/invalid_token_after_function_def.jt new file mode 100644 index 0000000..5fa3248 --- /dev/null +++ b/test/scenarios/bad_templates/invalid_token_after_function_def.jt @@ -0,0 +1 @@ +function(){}[] \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_variable_assignment1.jt b/test/scenarios/bad_templates/invalid_variable_assignment1.jt new file mode 100644 index 0000000..d15b409 --- /dev/null +++ b/test/scenarios/bad_templates/invalid_variable_assignment1.jt @@ -0,0 +1,2 @@ +let a = [{a: 1, b: 2}]; +a{.a===1}.b = 3; \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_variable_assignment2.jt b/test/scenarios/bad_templates/invalid_variable_assignment2.jt new file mode 100644 index 0000000..193ec16 --- /dev/null +++ b/test/scenarios/bad_templates/invalid_variable_assignment2.jt @@ -0,0 +1,2 @@ +let a = [{a: 1, b: 2}]; +a[1, 2].b = 3; \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_variable_assignment3.jt b/test/scenarios/bad_templates/invalid_variable_assignment3.jt new file mode 100644 index 0000000..04e82c0 --- /dev/null +++ b/test/scenarios/bad_templates/invalid_variable_assignment3.jt @@ -0,0 +1,2 @@ +let a = [{a: 1, b: 2}]; +a..b = 3; \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_variable_assignment4.jt b/test/scenarios/bad_templates/invalid_variable_assignment4.jt new file mode 100644 index 0000000..2faba06 --- /dev/null +++ b/test/scenarios/bad_templates/invalid_variable_assignment4.jt @@ -0,0 +1 @@ +^.a = 1 \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_variable_assignment5.jt b/test/scenarios/bad_templates/invalid_variable_assignment5.jt new file mode 100644 index 0000000..312a43a --- /dev/null +++ b/test/scenarios/bad_templates/invalid_variable_assignment5.jt @@ -0,0 +1 @@ +[].length = 1 \ No newline at end of file diff --git a/test/scenarios/bad_templates/invalid_variable_definition.jt b/test/scenarios/bad_templates/invalid_variable_definition.jt new file mode 100644 index 0000000..e8838dd --- /dev/null +++ b/test/scenarios/bad_templates/invalid_variable_definition.jt @@ -0,0 +1 @@ +let 1 = 2; \ No newline at end of file diff --git a/test/scenarios/bad_templates/object_with_invalid_closing.jt b/test/scenarios/bad_templates/object_with_invalid_closing.jt new file mode 100644 index 0000000..a88f0b5 --- /dev/null +++ b/test/scenarios/bad_templates/object_with_invalid_closing.jt @@ -0,0 +1 @@ +{ "aa": 1] \ No newline at end of file diff --git a/test/scenarios/bad_templates/object_with_invalid_key.jt b/test/scenarios/bad_templates/object_with_invalid_key.jt new file mode 100644 index 0000000..78ba118 --- /dev/null +++ b/test/scenarios/bad_templates/object_with_invalid_key.jt @@ -0,0 +1 @@ +{1: 2} \ No newline at end of file diff --git a/test/scenarios/bad_templates/reserved_id.jt b/test/scenarios/bad_templates/reserved_id.jt new file mode 100644 index 0000000..ff3014a --- /dev/null +++ b/test/scenarios/bad_templates/reserved_id.jt @@ -0,0 +1 @@ +let ___a = 1; \ No newline at end of file diff --git a/test/scenarios/bad_templates/unknown_token.jt b/test/scenarios/bad_templates/unknown_token.jt new file mode 100644 index 0000000..b7d5379 --- /dev/null +++ b/test/scenarios/bad_templates/unknown_token.jt @@ -0,0 +1 @@ +\ \ No newline at end of file diff --git a/test/scenarios/bad_templates/unsupported_assignment.jt b/test/scenarios/bad_templates/unsupported_assignment.jt new file mode 100644 index 0000000..72a79e9 --- /dev/null +++ b/test/scenarios/bad_templates/unsupported_assignment.jt @@ -0,0 +1,2 @@ +let a = 1 +var b = 1 diff --git a/test/scenarios/bindings/async.jt b/test/scenarios/bindings/async.jt new file mode 100644 index 0000000..1aaf245 --- /dev/null +++ b/test/scenarios/bindings/async.jt @@ -0,0 +1,4 @@ +const data = await Promise.all(.map(async lambda await $.square(?0))) +Promise.all(data.map(async function(a){ + await $.sqrt(a) +})) diff --git a/test/scenarios/bindings/data.ts b/test/scenarios/bindings/data.ts new file mode 100644 index 0000000..bb4d80e --- /dev/null +++ b/test/scenarios/bindings/data.ts @@ -0,0 +1,21 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + templatePath: 'async.jt', + bindings: { + square: (a) => new Promise((resolve) => setTimeout(() => resolve(a * a), 5)), + sqrt: (a) => new Promise((resolve) => setTimeout(() => resolve(Math.sqrt(a)), 5)), + }, + input: [1, 2, 3], + output: [1, 2, 3], + }, + { + bindings: { + a: 10, + b: 2, + c: (a, b) => a * b, + }, + output: 20, + }, +]; diff --git a/test/scenarios/bindings/template.jt b/test/scenarios/bindings/template.jt new file mode 100644 index 0000000..091c7b0 --- /dev/null +++ b/test/scenarios/bindings/template.jt @@ -0,0 +1 @@ +$.c($.a, $.b); diff --git a/test/scenarios/block/data.ts b/test/scenarios/block/data.ts new file mode 100644 index 0000000..6300f13 --- /dev/null +++ b/test/scenarios/block/data.ts @@ -0,0 +1,7 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: 15, + }, +]; diff --git a/test/scenarios/block/template.jt b/test/scenarios/block/template.jt new file mode 100644 index 0000000..7367d30 --- /dev/null +++ b/test/scenarios/block/template.jt @@ -0,0 +1,14 @@ +// Empty blocks will be ignored +() +let c = ( + let a = 1 + let b = 2 + a + b +) +/* + redefining a and b is possible because + we declared them in block previously + */ +let a = 2 +let b = 3 +(a + b) * c \ No newline at end of file diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts new file mode 100644 index 0000000..17e0e4e --- /dev/null +++ b/test/scenarios/comparisons/data.ts @@ -0,0 +1,31 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + }, +]; diff --git a/test/scenarios/comparisons/template.jt b/test/scenarios/comparisons/template.jt new file mode 100644 index 0000000..567f744 --- /dev/null +++ b/test/scenarios/comparisons/template.jt @@ -0,0 +1,24 @@ +[10>2, +2<10, +10>=2, +2<=10, +10 != 2, +'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', +'With' =$ 'I end with', +'with' ==$ 'I end with', +'I start with' ^= 'i', +'i' =^ 'I start with', +'I start with' ^== 'I', +'I' ==^ 'I start with', +"a" in ["a", "b"], +"a" in {a: 1, b: 2} +] \ No newline at end of file diff --git a/test/scenarios/conditions/data.ts b/test/scenarios/conditions/data.ts new file mode 100644 index 0000000..4133bd3 --- /dev/null +++ b/test/scenarios/conditions/data.ts @@ -0,0 +1,42 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + templatePath: 'if-then.jt', + input: { + a: -5, + }, + output: 0, + }, + { + templatePath: 'if-then.jt', + input: { + a: 5, + }, + output: 5, + }, + { + input: { + a: 5, + b: 10, + c: 15, + }, + output: 15, + }, + { + input: { + a: 15, + b: 5, + c: 10, + }, + output: 15, + }, + { + input: { + a: 10, + b: 15, + c: 5, + }, + output: 15, + }, +]; diff --git a/test/scenarios/conditions/if-then.jt b/test/scenarios/conditions/if-then.jt new file mode 100644 index 0000000..482a2eb --- /dev/null +++ b/test/scenarios/conditions/if-then.jt @@ -0,0 +1,3 @@ +let a = .a +a < 0 ? a = 0 +a diff --git a/test/scenarios/conditions/template.jt b/test/scenarios/conditions/template.jt new file mode 100644 index 0000000..95e63da --- /dev/null +++ b/test/scenarios/conditions/template.jt @@ -0,0 +1,4 @@ +let a = .a +let b = .b +let c = .c; +a > b ? a > c ? a : c : b > c ? b : c \ No newline at end of file diff --git a/test/scenarios/filters/array_filters.jt b/test/scenarios/filters/array_filters.jt new file mode 100644 index 0000000..d3746b9 --- /dev/null +++ b/test/scenarios/filters/array_filters.jt @@ -0,0 +1,2 @@ +let a = [1, 2, 3, 4, 5]; +[a[2:], a[:3], a[3:5], a[1, 3], a[-1], a[:-1], a[-2:]] \ No newline at end of file diff --git a/test/scenarios/filters/data.ts b/test/scenarios/filters/data.ts new file mode 100644 index 0000000..933567e --- /dev/null +++ b/test/scenarios/filters/data.ts @@ -0,0 +1,24 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + templatePath: 'object_filters.jt', + output: { + a: [3, 4], + 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: { + a: 1, + b: 2, + c: 3, + d: 4, + }, + }, +]; diff --git a/test/scenarios/filters/object_filters.jt b/test/scenarios/filters/object_filters.jt new file mode 100644 index 0000000..573e11b --- /dev/null +++ b/test/scenarios/filters/object_filters.jt @@ -0,0 +1,5 @@ +let a = [ + {a: [1, 2], b: "1"}, {a: [3, 4], b: 2}, + {a: [10, 5], b: 3}, {a:[5], b: 4}, {b: 5} +] +a{.a.length > 1}{.a.includes(3)}.{typeof .b === "number"} diff --git a/test/scenarios/filters/object_indexes.jt b/test/scenarios/filters/object_indexes.jt new file mode 100644 index 0000000..7682f29 --- /dev/null +++ b/test/scenarios/filters/object_indexes.jt @@ -0,0 +1,7 @@ +let obj = { + a: 1, + b: 2, + c: 3, + d: 4 +} +{...obj{["a", "b"]}, ...obj{~["a", "b"]}} \ No newline at end of file diff --git a/test/scenarios/functions/data.ts b/test/scenarios/functions/data.ts new file mode 100644 index 0000000..2771719 --- /dev/null +++ b/test/scenarios/functions/data.ts @@ -0,0 +1,28 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + templatePath: 'js_date_function.jt', + output: [2022, 8, 19], + }, + { + templatePath: 'new_operator.jt', + output: [ + { + name: 'foo', + grade: 1, + }, + { + name: 'bar', + grade: 2, + }, + ], + }, + { + templatePath: 'parent_scope_vars.jt', + output: 90, + }, + { + output: 80, + }, +]; diff --git a/test/scenarios/functions/js_date_function.jt b/test/scenarios/functions/js_date_function.jt new file mode 100644 index 0000000..2f1b301 --- /dev/null +++ b/test/scenarios/functions/js_date_function.jt @@ -0,0 +1,2 @@ +const date = new Date('2022-08-19'); +[date.getFullYear(), date.getMonth() + 1, date.getDate()]; diff --git a/test/scenarios/functions/new_operator.jt b/test/scenarios/functions/new_operator.jt new file mode 100644 index 0000000..bd72554 --- /dev/null +++ b/test/scenarios/functions/new_operator.jt @@ -0,0 +1,13 @@ +const Person = { + Student: function(name, grade) { + this.name = name; + this.grade = grade; + } +} + +const foo = new Person.Student('foo', 1); +const bar = new Person.Student('bar', 2); +[ + {name: foo.name, grade: foo.grade}, + {...bar} +] \ No newline at end of file diff --git a/test/scenarios/functions/parent_scope_vars.jt b/test/scenarios/functions/parent_scope_vars.jt new file mode 100644 index 0000000..bd4fd4c --- /dev/null +++ b/test/scenarios/functions/parent_scope_vars.jt @@ -0,0 +1,5 @@ +let a = 1; let b = 2 +let fn = function(e){ + (a+b)*e +} +fn(10) + fn(20) \ No newline at end of file diff --git a/test/scenarios/functions/template.jt b/test/scenarios/functions/template.jt new file mode 100644 index 0000000..2b4e784 --- /dev/null +++ b/test/scenarios/functions/template.jt @@ -0,0 +1,9 @@ +let normalFn = function(a) { + a + 10 +}; +let lambdaFn = lambda ?0 + 10; +let spreadFn = function(...a) { + a.reduce(lambda ?0 + ?1, 0) +}; +let fnArr = {spread: spreadFn, other: [normalFn, lambdaFn]}; +fnArr.spread(fnArr.other[0](10), fnArr.other[1](20), function(){30}()) \ No newline at end of file diff --git a/test/scenarios/inputs/data.ts b/test/scenarios/inputs/data.ts new file mode 100644 index 0000000..4e8f562 --- /dev/null +++ b/test/scenarios/inputs/data.ts @@ -0,0 +1,14 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + input: { + a: 10, + b: 2, + c: { + d: 30, + }, + }, + output: 50, + }, +]; diff --git a/test/scenarios/inputs/template.jt b/test/scenarios/inputs/template.jt new file mode 100644 index 0000000..22d2dd1 --- /dev/null +++ b/test/scenarios/inputs/template.jt @@ -0,0 +1 @@ +.c.(.d + ^.a * ^.b); diff --git a/test/scenarios/logics/data.ts b/test/scenarios/logics/data.ts new file mode 100644 index 0000000..53e9c07 --- /dev/null +++ b/test/scenarios/logics/data.ts @@ -0,0 +1,7 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: [3, 0, 2, 3, 0, 3, true, true], + }, +]; diff --git a/test/scenarios/logics/template.jt b/test/scenarios/logics/template.jt new file mode 100644 index 0000000..2895a09 --- /dev/null +++ b/test/scenarios/logics/template.jt @@ -0,0 +1,5 @@ +[ + 2 && 3, 0 && 3, 2 || 3, 0 || 3, + 0 ?? 3, null ?? undefined ?? 3, + !false, !!true +] diff --git a/test/scenarios/math/data.ts b/test/scenarios/math/data.ts new file mode 100644 index 0000000..f7464b6 --- /dev/null +++ b/test/scenarios/math/data.ts @@ -0,0 +1,7 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: [12, 8, 20, 5, 100, 0, 40, 2], + }, +]; diff --git a/test/scenarios/math/template.jt b/test/scenarios/math/template.jt new file mode 100644 index 0000000..22e237f --- /dev/null +++ b/test/scenarios/math/template.jt @@ -0,0 +1 @@ +[10 + 2, 10 - 2, 10 * 2, 10 / 2, 10 ** 2, 10 % 2, 10 << 2, 10 >> 2]; diff --git a/test/scenarios/objects/data.ts b/test/scenarios/objects/data.ts new file mode 100644 index 0000000..e79ea4d --- /dev/null +++ b/test/scenarios/objects/data.ts @@ -0,0 +1,11 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: { + a: 1, + b: 2, + d: 3, + }, + }, +]; diff --git a/test/scenarios/objects/template.jt b/test/scenarios/objects/template.jt new file mode 100644 index 0000000..e7d9a8d --- /dev/null +++ b/test/scenarios/objects/template.jt @@ -0,0 +1,17 @@ +let c = "c key"; +let d = 3; +let a = { + // short form for "a" + a: 1, + "b": 2, + // [c] coverts to "c key" + [c]: { + // this coverts to d: 3 + d: d + } +}; +a.({ + a: .a, + b: .b, + ...(.[c]) +}) \ No newline at end of file diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts new file mode 100644 index 0000000..31516e3 --- /dev/null +++ b/test/scenarios/paths/data.ts @@ -0,0 +1,20 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + input: { + a: { d: 1 }, + b: [{ d: 2 }, { d: 3 }], + c: { c: { d: 4 } }, + }, + output: [ + [3], + 'aa', + { + d: 1, + }, + 4, + 3, + ], + }, +]; diff --git a/test/scenarios/paths/template.jt b/test/scenarios/paths/template.jt new file mode 100644 index 0000000..85f31f2 --- /dev/null +++ b/test/scenarios/paths/template.jt @@ -0,0 +1,7 @@ +[ + 3[0][][], + "aa"[][0], + ^.a, + .c.c.d, + .b[1].d +] \ No newline at end of file diff --git a/test/scenarios/selectors/context_variables.jt b/test/scenarios/selectors/context_variables.jt new file mode 100644 index 0000000..c77403f --- /dev/null +++ b/test/scenarios/selectors/context_variables.jt @@ -0,0 +1,6 @@ +..b@b#bi.c#ci.({ + cid: .id, + cidx: ci, + bid: b.id, + bidx: bi +}) \ No newline at end of file diff --git a/test/scenarios/selectors/data.ts b/test/scenarios/selectors/data.ts new file mode 100644 index 0000000..744485d --- /dev/null +++ b/test/scenarios/selectors/data.ts @@ -0,0 +1,79 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + templatePath: 'context_variables.jt', + input: { + a: 10, + b: [ + { + c: [ + { + id: 1, + }, + { + id: 2, + }, + ], + id: 1, + }, + { + c: [ + { + id: 3, + }, + { + id: 4, + }, + ], + id: 2, + }, + ], + }, + output: [ + { + cid: 1, + cidx: 0, + bid: 1, + bidx: 0, + }, + { + cid: 2, + cidx: 1, + bid: 1, + bidx: 0, + }, + { + cid: 3, + cidx: 0, + bid: 2, + bidx: 1, + }, + { + cid: 4, + cidx: 1, + bid: 2, + bidx: 1, + }, + ], + }, + { + input: { + a: 10, + b: 2, + c: { + d: 30, + }, + }, + output: 50, + }, + { + templatePath: 'wild_cards.jt', + input: { + a: { d: 1 }, + b: [{ d: 2 }, { d: 3 }], + c: { c: { d: 4 } }, + }, + output: [1, 2, 3, 1, 2, 3, 4], + }, +]; diff --git a/test/scenarios/selectors/template.jt b/test/scenarios/selectors/template.jt new file mode 100644 index 0000000..bd2dd8e --- /dev/null +++ b/test/scenarios/selectors/template.jt @@ -0,0 +1 @@ +..d + .a * .b \ No newline at end of file diff --git a/test/scenarios/selectors/wild_cards.jt b/test/scenarios/selectors/wild_cards.jt new file mode 100644 index 0000000..8c54727 --- /dev/null +++ b/test/scenarios/selectors/wild_cards.jt @@ -0,0 +1 @@ +[.*.d, ..*.d].. \ No newline at end of file diff --git a/test/scenarios/statements/data.ts b/test/scenarios/statements/data.ts new file mode 100644 index 0000000..6e82a3b --- /dev/null +++ b/test/scenarios/statements/data.ts @@ -0,0 +1,7 @@ +import { Sceanario } from '../../types'; + +export const data: Sceanario[] = [ + { + output: [15], + }, +]; diff --git a/test/scenarios/statements/template.jt b/test/scenarios/statements/template.jt new file mode 100644 index 0000000..ea1130e --- /dev/null +++ b/test/scenarios/statements/template.jt @@ -0,0 +1,14 @@ +// To statements in one line +let a = 1; let b = 2 +// empty statements +;;; + +let c = [3] +// block statement +( + let d = 4; + let fn = function(a){ + 5*a + } + [a + b + c[0] + d + fn(1)] +) diff --git a/test/test_engine.ts b/test/test_engine.ts new file mode 100644 index 0000000..461e0df --- /dev/null +++ b/test/test_engine.ts @@ -0,0 +1,247 @@ +import { + JsonTemplateParser, + JsonTemplateTranslator, + JsonTemplateEngine, + JsonTemplateLexer, +} from '../src/'; + +const data = [ + { + city: 'WILDWOOD A', + loc: [-85.430456, 34.977911], + pop: 586, + state: 'GA', + _id: '30757', + }, + { + city: 'WILDWOOD', + loc: [-82.03473, 28.845353], + pop: 10604, + state: 'FL', + _id: '34785', + }, + { + city: 'WILDWOOD', + loc: [-122.918013, 40.316528], + pop: 119, + state: 'CA', + _id: '96076', + }, +]; + +const account = { + Account: { + 'Account Name': 'Firefly', + Order: [ + { + OrderID: 'order103', + Product: [ + { + 'Product Name': 'Bowler Hat', + ProductID: 858383, + SKU: '0406654608', + Description: { + Colour: 'Purple', + Width: 300, + Height: 200, + Depth: 210, + Weight: 0.75, + }, + Price: 34.45, + Quantity: 2, + }, + { + 'Product Name': 'Trilby hat', + ProductID: 858236, + SKU: '0406634348', + Description: { + Colour: 'Orange', + Width: 300, + Height: 200, + Depth: 210, + Weight: 0.6, + }, + Price: 21.67, + Quantity: 1, + }, + ], + }, + { + OrderID: 'order104', + Product: [ + { + 'Product Name': 'Bowler Hat', + ProductID: 858383, + SKU: '040657863', + Description: { + Colour: 'Purple', + Width: 300, + Height: 200, + Depth: 210, + Weight: 0.75, + }, + Price: 34.45, + Quantity: 4, + }, + { + ProductID: 345664, + SKU: '0406654603', + 'Product Name': 'Cloak', + Description: { + Colour: 'Black', + Width: 30, + Height: 20, + Depth: 210, + Weight: 2, + }, + Price: 107.99, + Quantity: 1, + }, + ], + }, + ], + }, +}; + +const address = { + FirstName: 'Fred', + Surname: 'Smith', + Age: 28, + Address: { + Street: 'Hursley Park', + City: 'Winchester', + Postcode: 'SO21 2JN', + }, + Phone: [ + { + type: 'home', + number: '0203 544 1234', + }, + { + type: 'office', + number: '01962 001234', + }, + { + type: 'office', + number: '01962 001235', + }, + { + type: 'mobile', + number: '077 7700 1234', + }, + ], + Email: [ + { + type: 'office', + address: ['fred.smith@my-work.com', 'fsmith@my-work.com'], + }, + { + type: 'home', + address: ['freddy@my-social.com', 'frederic.smith@very-serious.com'], + }, + ], + Other: { + 'Over 18 ?': true, + Misc: null, + 'Alternative.Address': { + Street: 'Brick Lane', + City: 'London', + Postcode: 'E1 6RF', + }, + }, +}; +// const extractor = new JsonTemplateEngine(`.{.city == "wildwood"}.pop`); + +// console.log(JSON.stringify(extractor.evaluate(data, { min: 1000 }), null, 2)); + +// console.log( +// JSON.stringify( +// new JsonTemplateEngine(` +// ..Product +// `).evaluate(account), +// ), +// ); + +// console.log( +// JSON.stringify( +// new JsonTemplateEngine(` +// .Account.Order@o#oi.Product@p#pi.({ +// orderId: o.OrderID, +// productId: p.ProductID, +// oi: oi, +// pi: pi +// }) +// `).evaluate(account, { a: { b: { c: () => (a) => a, d: 2 } } }), +// ), +// ); + +// console.log( +// JSON.stringify( +// new JsonTemplateEngine(` +// let a = null; +// let b = undefined; +// let c = ""; +// let d = {}; +// a || b || c || d +// `).evaluate(account, { a: { b: { c: () => (a) => a, d: 2 } } }), +// ), +// ); +// console.log( +// JSON.stringify( +// new JsonTemplateEngine(` +// let c = "c key"; +// let d = 3; +// { +// // short form for "a" +// a: 1, +// "b": 2, +// // [c] coverts to "c key" +// [c]: { +// // this coverts to d: 3 +// d: d +// } +// } +// `).evaluate(address), +// ), +// ); +// console.log( +// // JSON.stringify( +// new JsonTemplateTranslator( +// new JsonTemplateParser( +// new JsonTemplateLexer(` +// .{.city==="WILDWOOD" && .state==="GA"}.pop +// `), +// ).parse(), +// ).translate(), +// // ), +// ); + +new JsonTemplateEngine(` +let a = [{a: [1, 2], b: "1"}, {a: [3, 4], b: 2}, {a:[5], b: 3}, {b: 4}] +a{.a.length > 1} +`) + .evaluate({ a: 1 }) + .then((a) => console.log(JSON.stringify(a))); + +console.log( + new JsonTemplateTranslator( + new JsonTemplateParser( + new JsonTemplateLexer(` + let a = [{a: [1, 2], b: "1"}, {a: [3, 4], b: 2}, {a:[5], b: 3}, {b: 4}] + a{.a.length > 1} + `), + ).parse(), + ).translate(), +); + +console.log( + JSON.stringify( + new JsonTemplateParser( + new JsonTemplateLexer(` + .(.a) + `), + ).parse(), + // null, + // 2, + ), +); diff --git a/test/test_extractor.ts b/test/test_extractor.ts deleted file mode 100644 index 082c37a..0000000 --- a/test/test_extractor.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - JsonTemplateParser, - JsonTemplateTranslator, - JsonTemplateEngine, - JsonTemplateLexer, -} from '../src/'; - -const extractor = new JsonTemplateEngine(`.[{.city === "WILDWOOD"}{.state === "FL"}]`); - -const data = [ - { - city: 'WILDWOOD A', - loc: [-85.430456, 34.977911], - pop: 586, - state: 'GA', - _id: '30757', - }, - { - city: 'WILDWOOD', - loc: [-82.03473, 28.845353], - pop: 10604, - state: 'FL', - _id: '34785', - }, - { - city: 'WILDWOOD', - loc: [-122.918013, 40.316528], - pop: 119, - state: 'CA', - _id: '96076', - }, -]; -console.log(JSON.stringify(extractor.evaluate(data, { min: 1000 }), null, 2)); - -// const context = {} -// console.log(JSON.stringify(new JsonTemplateEngine(` -// .pop -// `).evaluate(data[0], {context, fn: () => 100}))); -// console.log(context); - -console.log( - JSON.stringify( - new JsonTemplateEngine(` - const a = [{ a: {c: 2} }, {b: 1}]; - const b = 2 + 2 ** 10; - a...c - `).evaluate([{ a:1 }, {b: 1}], {a : {fn: (...args) => args.map((e) => e*e)}}), - ), -); - -// console.log(JSON.stringify(new JsonTemplateParser(new JsonTemplateLexer('.a.(.b+.c)')).parse())); -console.log( - new JsonTemplateTranslator( - new JsonTemplateParser( - new JsonTemplateLexer(` - const a = [{ a:1 }, {b: 1}]; - const b = 2 + 2 ** 10; - [1, 2 ,3][0] - `), - ).parse(), - ).translate(), -); diff --git a/test/test_scenario.ts b/test/test_scenario.ts new file mode 100644 index 0000000..d413f48 --- /dev/null +++ b/test/test_scenario.ts @@ -0,0 +1,35 @@ +import { join } from 'path'; +import { deepEqual } from 'assert'; +import { Command } from 'commander'; +import { Sceanario } from './types'; +import { SceanarioUtils } from './utils'; + +const command = new Command(); +command + .allowUnknownOption() + .option('-s, --scenario ', 'Enter Scenario Name') + .option('-i, --index ', 'Enter Test case index') + .parse(); + +const opts = command.opts(); +const scenarioName = opts.scenario || 'assignments'; +const index = +(opts.index || 0); + +console.log(`Executing scenario: ${scenarioName} and test: ${index}`); + +async function createAndEvaluateTemplate() { + try { + const scenarioDir = join(__dirname, 'scenarios', scenarioName); + const scenarios: Sceanario[] = SceanarioUtils.extractScenarios(scenarioDir); + const scenario: Sceanario = scenarios[index] || scenarios[0]; + const templateEngine = SceanarioUtils.createTemplateEngine(scenarioDir, scenario); + const result = await SceanarioUtils.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'); + } catch (error) { + console.error(error); + } +} + +createAndEvaluateTemplate(); diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 0000000..e1377d6 --- /dev/null +++ b/test/types.ts @@ -0,0 +1,10 @@ +import { Dictionary } from '../src'; + +export type Sceanario = { + description?: string; + input?: any; + templatePath?: string; + bindings?: Dictionary; + output?: any; + error?: string; +}; diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 0000000..09e8d66 --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1 @@ +export * from './scenario'; diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts new file mode 100644 index 0000000..1dfb224 --- /dev/null +++ b/test/utils/scenario.ts @@ -0,0 +1,21 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { JsonTemplateEngine } from '../../src'; +import { Sceanario } from '../types'; + +export class SceanarioUtils { + static createTemplateEngine(scenarioDir: string, sceanario: Sceanario): JsonTemplateEngine { + const templatePath = join(scenarioDir, sceanario.templatePath || 'template.jt'); + const template = readFileSync(templatePath, 'utf-8'); + return new JsonTemplateEngine(template); + } + + static evaluateScenario(templateEngine: JsonTemplateEngine, sceanario: Sceanario): any { + return templateEngine.evaluate(sceanario.input, sceanario.bindings); + } + + static extractScenarios(scenarioDir: string): Sceanario[] { + const { data } = require(join(scenarioDir, 'data.ts')); + return data as Sceanario[]; + } +}