From 63f4953648b6f0a9a3d11fe5d3d436f39188f3e5 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Fri, 17 May 2024 22:25:17 +0530 Subject: [PATCH 01/29] feat: add support for json path syntax --- src/lexer.ts | 38 ++++++++++---- src/operators.ts | 10 +++- src/parser.ts | 68 +++++++++++++++++--------- src/translator.ts | 10 +++- src/types.ts | 6 +++ test/scenarios/comparisons/data.ts | 12 +++++ test/scenarios/comparisons/template.jt | 14 +++++- test/scenarios/paths/block.jt | 4 +- test/scenarios/paths/data.ts | 47 ++++++++++++++++++ test/scenarios/paths/json_path.jt | 5 ++ test/scenarios/paths/simple_path.jt | 2 +- test/test_engine.ts | 28 +++++------ 12 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 test/scenarios/paths/json_path.jt diff --git a/src/lexer.ts b/src/lexer.ts index fd10f84..fe18d6c 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -1,4 +1,4 @@ -import { BINDINGS_PARAM_KEY, VARS_PREFIX } from './constants'; +import { VARS_PREFIX } from './constants'; import { JsonTemplateLexerError } from './errors'; import { Keyword, Token, TokenType } from './types'; @@ -81,12 +81,16 @@ export class JsonTemplateLexer { return this.match('~r'); } + matchJsonPath(): boolean { + return this.match('~j'); + } + matchPathType(): boolean { - return this.matchRichPath() || this.matchSimplePath(); + return this.matchRichPath() || this.matchJsonPath() || this.matchSimplePath(); } matchPath(): boolean { - return this.matchPathSelector() || this.matchID(); + return this.matchPathType() || this.matchPathSelector() || this.matchID(); } matchSpread(): boolean { @@ -113,7 +117,7 @@ export class JsonTemplateLexer { const token = this.lookahead(); if (token.type === TokenType.PUNCT) { const { value } = token; - return value === '.' || value === '..' || value === '^'; + return value === '.' || value === '..' || value === '^' || value === '$' || value === '@'; } return false; @@ -145,8 +149,24 @@ export class JsonTemplateLexer { return token.type === TokenType.KEYWORD && token.value === val; } + matchContains(): boolean { + return this.matchKeywordValue(Keyword.CONTAINS); + } + + matchEmpty(): boolean { + return this.matchKeywordValue(Keyword.EMPTY); + } + + matchSize(): boolean { + return this.matchKeywordValue(Keyword.SIZE); + } + + matchSubsetOf(): boolean { + return this.matchKeywordValue(Keyword.SUBSETOF); + } + matchIN(): boolean { - return this.matchKeywordValue(Keyword.IN); + return this.matchKeywordValue(Keyword.IN) || this.matchKeywordValue(Keyword.NOT_IN); } matchFunction(): boolean { @@ -317,7 +337,7 @@ export class JsonTemplateLexer { } private static isIdStart(ch: string) { - return ch === '$' || ch === '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); + return ch === '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } private static isIdPart(ch: string) { @@ -383,7 +403,7 @@ export class JsonTemplateLexer { JsonTemplateLexer.validateID(id); return { type: TokenType.ID, - value: id.replace(/^\$/, `${BINDINGS_PARAM_KEY}`), + value: id, range: [start, this.idx], }; } @@ -565,7 +585,7 @@ export class JsonTemplateLexer { const start = this.idx; const ch1 = this.codeChars[this.idx]; - if (',;:{}()[]^+-*/%!><|=@~#?\n'.includes(ch1)) { + if (',;:{}()[]^+-*/%!><|=@~$#?\n'.includes(ch1)) { return { type: TokenType.PUNCT, value: ch1, @@ -594,7 +614,7 @@ export class JsonTemplateLexer { const ch1 = this.codeChars[this.idx]; const ch2 = this.codeChars[this.idx + 1]; - if (ch1 === '~' && 'rs'.includes(ch2)) { + if (ch1 === '~' && 'rsj'.includes(ch2)) { this.idx += 2; return { type: TokenType.PUNCT, diff --git a/src/operators.ts b/src/operators.ts index 9d90515..31791bd 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -23,7 +23,7 @@ function endsWith(val1, val2): string { } function containsStrict(val1, val2): string { - return `(typeof ${val1} === 'string' && ${val1}.includes(${val2}))`; + return `((typeof ${val1} === 'string' || Array.isArray(${val1})) && ${val1}.includes(${val2}))`; } function contains(val1, val2): string { @@ -74,10 +74,18 @@ export const binaryOperators = { '=$': (val1, val2): string => endsWith(val2, val1), + contains: containsStrict, + '==*': (val1, val2): string => containsStrict(val2, val1), '=*': (val1, val2): string => contains(val2, val1), + size: (val1, val2): string => `${val1}.length === ${val2}`, + + empty: (val1, val2): string => `(${val1}.length === 0) === ${val2}`, + + subsetof: (val1, val2): string => `${val1}.every((el) => {return ${val2}.includes(el);})`, + '+': (val1, val2): string => `${val1}+${val2}`, '-': (val1, val2): string => `${val1}-${val2}`, diff --git a/src/parser.ts b/src/parser.ts index dbacd30..e7d95dc 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -211,10 +211,10 @@ export class JsonTemplateParser { return this.parseSelector(); } else if (this.lexer.matchToArray()) { return this.parsePathOptions(); - } else if (this.lexer.match('[')) { - return this.parseArrayFilterExpr(); } else if (this.lexer.match('{')) { return this.parseObjectFiltersExpr(); + } else if (this.lexer.match('[')) { + return this.parseArrayFilterExpr(); } else if (this.lexer.match('@') || this.lexer.match('#')) { return this.parsePathOptions(); } @@ -265,7 +265,7 @@ export class JsonTemplateParser { }; } - private parsePathRoot(root?: Expression): Expression | string | undefined { + private parsePathRoot(pathType: PathType, root?: Expression): Expression | string | undefined { if (root) { return root; } @@ -273,6 +273,17 @@ export class JsonTemplateParser { this.lexer.ignoreTokens(1); return DATA_PARAM_KEY; } + + if (this.lexer.match('$')) { + this.lexer.ignoreTokens(1); + return pathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY; + } + + if (this.lexer.match('@')) { + this.lexer.ignoreTokens(1); + return undefined; + } + if (this.lexer.matchID()) { return this.lexer.value(); } @@ -287,23 +298,18 @@ export class JsonTemplateParser { this.lexer.ignoreTokens(1); return PathType.RICH; } - return this.options?.defaultPathType ?? PathType.RICH; - } - - private parsePathTypeExpr(): Expression { - const pathType = this.parsePathType(); - const expr = this.parseBaseExpr(); - if (expr.type === SyntaxType.PATH) { - expr.pathType = pathType; + if (this.lexer.matchJsonPath()) { + this.lexer.ignoreTokens(1); + return PathType.JSON; } - return expr; + return this.options?.defaultPathType ?? PathType.RICH; } private parsePath(options?: { root?: Expression }): PathExpression | Expression { const pathType = this.parsePathType(); const expr: PathExpression = { type: SyntaxType.PATH, - root: this.parsePathRoot(options?.root), + root: this.parsePathRoot(pathType, options?.root), parts: this.parsePathParts(), pathType, }; @@ -548,15 +554,29 @@ export class JsonTemplateParser { }; } - private parseArrayFilterExpr(): ArrayFilterExpression { + private parseArrayFilterExpr(): ArrayFilterExpression | ObjectFilterExpression | undefined { + let exprType = SyntaxType.ARRAY_FILTER_EXPR; + let filter: Expression | undefined; this.lexer.expect('['); - const filter = this.parseArrayFilter(); + if (this.lexer.match('?')) { + this.lexer.ignoreTokens(1); + exprType = SyntaxType.OBJECT_FILTER_EXPR; + filter = this.parseBaseExpr(); + } else if (this.lexer.match('*')) { + // this selects all the items so no filter + this.lexer.ignoreTokens(1); + } else { + filter = this.parseArrayFilter(); + } this.lexer.expect(']'); - return { - type: SyntaxType.ARRAY_FILTER_EXPR, - filter, - }; + if (filter) { + return { + type: exprType, + filter, + }; + } + return undefined; } private combineExpressionsAsBinaryExpr( @@ -666,7 +686,11 @@ export class JsonTemplateParser { this.lexer.match('>') || this.lexer.match('<=') || this.lexer.match('>=') || - this.lexer.matchIN() + this.lexer.matchIN() || + this.lexer.matchContains() || + this.lexer.matchSize() || + this.lexer.matchEmpty() || + this.lexer.matchSubsetOf() ) { return { type: this.lexer.matchIN() ? SyntaxType.IN_EXPR : SyntaxType.COMPARISON_EXPR, @@ -1247,10 +1271,6 @@ export class JsonTemplateParser { return this.parseArrayExpr(); } - if (this.lexer.matchPathType()) { - return this.parsePathTypeExpr(); - } - if (this.lexer.matchPath()) { return this.parsePath(); } diff --git a/src/translator.ts b/src/translator.ts index c6a772e..436463d 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -39,6 +39,7 @@ import { LoopExpression, IncrementExpression, LoopControlExpression, + Keyword, } from './types'; import { CommonUtils } from './utils'; @@ -686,8 +687,13 @@ export class JsonTemplateTranslator { code.push(this.translateExpr(expr.args[0], val1, ctx)); code.push(this.translateExpr(expr.args[1], val2, ctx)); code.push(`if(typeof ${val2} === 'object'){`); - const inCode = `(Array.isArray(${val2}) ? ${val2}.includes(${val1}) : ${val1} in ${val2})`; - code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, inCode)); + if (expr.op === Keyword.IN) { + const inCode = `(Array.isArray(${val2}) ? ${val2}.includes(${val1}) : ${val1} in ${val2})`; + code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, inCode)); + } else { + const notInCode = `(Array.isArray(${val2}) ? !${val2}.includes(${val1}) : !(${val1} in ${val2}))`; + code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, notInCode)); + } code.push('} else {'); code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, 'false')); code.push('}'); diff --git a/src/types.ts b/src/types.ts index 5f5e033..9235bbd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,7 +8,12 @@ export enum Keyword { AWAIT = 'await', ASYNC = 'async', IN = 'in', + NOT_IN = 'nin', NOT = 'not', + CONTAINS = 'contains', + SUBSETOF = 'subsetof', + EMPTY = 'empty', + SIZE = 'size', RETURN = 'return', THROW = 'throw', CONTINUE = 'continue', @@ -92,6 +97,7 @@ export enum SyntaxType { export enum PathType { SIMPLE = 'simple', RICH = 'rich', + JSON = 'json', } export interface EngineOptions { diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts index 0974cfa..aded9f0 100644 --- a/test/scenarios/comparisons/data.ts +++ b/test/scenarios/comparisons/data.ts @@ -23,6 +23,18 @@ export const data: Scenario[] = [ 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 index 613e133..3ab18e7 100644 --- a/test/scenarios/comparisons/template.jt +++ b/test/scenarios/comparisons/template.jt @@ -18,5 +18,17 @@ 'I start with' ^== 'I', 'I' ==^ 'I start with', "a" in ["a", "b"], -"a" in {a: 1, b: 2} +"a" in {a: 1, b: 2}, +"a" nin ["b"], +"a" nin {b: 1}, +["a", "b"] contains "a", +"abc" contains "a", +["a", "b"] size 2, +"abc" size 3, +[] empty true, +["a"] empty false, +"" empty true, +"abc" empty false, +["c", "a"] subsetof ["a", "b", "c"], +[] subsetof ["a", "b", "c"] ] \ No newline at end of file diff --git a/test/scenarios/paths/block.jt b/test/scenarios/paths/block.jt index 45c6aa2..0f51161 100644 --- a/test/scenarios/paths/block.jt +++ b/test/scenarios/paths/block.jt @@ -1,7 +1,7 @@ [ .({ - a: .a + 1, - b: .b + 2 + a: @.a + 1, + b: @.b + 2 }), .([.a + 1, .b + 2]) ] \ No newline at end of file diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts index 6012d2a..e617a34 100644 --- a/test/scenarios/paths/data.ts +++ b/test/scenarios/paths/data.ts @@ -69,6 +69,53 @@ export const data: Scenario[] = [ }, ], }, + { + templatePath: 'json_path.jt', + input: { + foo: 'bar', + items: [ + { + a: 1, + b: 1, + }, + { + a: 2, + b: 2, + }, + { + a: 3, + b: 3, + }, + ], + }, + output: [ + 'bar', + [ + { + a: 1, + b: 1, + }, + { + a: 2, + b: 2, + }, + { + a: 3, + b: 3, + }, + ], + [ + { + a: 2, + b: 2, + }, + { + a: 3, + b: 3, + }, + ], + ], + }, { templatePath: 'options.jt', options: { diff --git a/test/scenarios/paths/json_path.jt b/test/scenarios/paths/json_path.jt new file mode 100644 index 0000000..55f9c01 --- /dev/null +++ b/test/scenarios/paths/json_path.jt @@ -0,0 +1,5 @@ +[ + ~j $.foo, + ~j $.items[*], + ~j $.items[?(@.a>1)], +] \ No newline at end of file diff --git a/test/scenarios/paths/simple_path.jt b/test/scenarios/paths/simple_path.jt index 8f2c1c0..6b57327 100644 --- a/test/scenarios/paths/simple_path.jt +++ b/test/scenarios/paths/simple_path.jt @@ -2,5 +2,5 @@ ~s ^.0.a."e", ~s .0.d.1.1[], .[0].b.()~s .1.e.1, - ~s {a: {b : 1}}.a.b + {a: {b : 1}}.a.b ] \ No newline at end of file diff --git a/test/test_engine.ts b/test/test_engine.ts index efde75d..e26676f 100644 --- a/test/test_engine.ts +++ b/test/test_engine.ts @@ -216,25 +216,25 @@ const address = { // // ), // ); -JsonTemplateEngine.create( - ` -.b ?? .a -`, -) - .evaluate({ a: 1 }) - .then((a) => console.log(JSON.stringify(a))); +// JsonTemplateEngine.create( +// ` +// .b ?? .a +// `, +// ) +// .evaluate({ a: 1 }) +// .then((a) => console.log(JSON.stringify(a))); -console.log( - JsonTemplateEngine.translate(` - let a = [{a: [1, 2], b: "1"}, {a: [3, 4], b: 2}, {a:[5], b: 3}, {b: 4}] - a{.a.length > 1} - `), -); +// console.log( +// JsonTemplateEngine.translate(` +// let a = [{a: [1, 2], b: "1"}, {a: [3, 4], b: 2}, {a:[5], b: 3}, {b: 4}] +// a{.a.length > 1} +// `), +// ); console.log( JSON.stringify( JsonTemplateEngine.parse(` - .(.a) + ~j $.foo `), // null, // 2, From 4b2c4634e52fca67d450a020b66e585f145367e1 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Fri, 17 May 2024 22:36:47 +0530 Subject: [PATCH 02/29] chore: update read me for json paths --- readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/readme.md b/readme.md index 6965574..54117bb 100644 --- a/readme.md +++ b/readme.md @@ -216,6 +216,12 @@ If we use this rich path`~r a.b.c` then it automatically handles following varia - `{"a": [{ "b": [{"c": 2}]}]}` Refer this [example](test/scenarios/paths/rich_path.jt) for more details. +#### Json Paths +We support some features of [JSON Path](https://goessner.net/articles/JsonPath/index.html#) syntax using path option (`~j`). +Note: This is an experimental feature and may not support all the features of JSON Paths. + +Refer this [example](test/scenarios/paths/json_path.jt) for more details. + #### Simple selectors ```js @@ -333,6 +339,8 @@ We can override the default path option using tags. ~s a.b.c // Use ~r to treat a.b.c as rich path ~r a.b.c +// Use ~j for using json paths +~j items[?(@.a>1)] ``` **Note:** Rich paths are slower compare to the simple paths. From ffe4f140ae8b2093bd127abe98bca0bec4c6a34e Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 20 May 2024 08:00:40 +0530 Subject: [PATCH 03/29] fix: array all filters syntax tree --- src/parser.ts | 42 +++++++++++++++++++++--------------------- src/translator.ts | 2 +- src/types.ts | 5 ++++- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index e7d95dc..85a3926 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -269,19 +269,19 @@ export class JsonTemplateParser { if (root) { return root; } - if (this.lexer.match('^')) { - this.lexer.ignoreTokens(1); - return DATA_PARAM_KEY; - } - - if (this.lexer.match('$')) { - this.lexer.ignoreTokens(1); - return pathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY; - } - - if (this.lexer.match('@')) { - this.lexer.ignoreTokens(1); - return undefined; + const nextToken = this.lexer.lookahead(); + switch (nextToken.value) { + case '^': + this.lexer.ignoreTokens(1); + return DATA_PARAM_KEY; + case '$': + this.lexer.ignoreTokens(1); + return pathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY; + case '@': + this.lexer.ignoreTokens(1); + return undefined; + default: + break; } if (this.lexer.matchID()) { @@ -554,7 +554,7 @@ export class JsonTemplateParser { }; } - private parseArrayFilterExpr(): ArrayFilterExpression | ObjectFilterExpression | undefined { + private parseArrayFilterExpr(): ArrayFilterExpression | ObjectFilterExpression { let exprType = SyntaxType.ARRAY_FILTER_EXPR; let filter: Expression | undefined; this.lexer.expect('['); @@ -565,18 +565,18 @@ export class JsonTemplateParser { } else if (this.lexer.match('*')) { // this selects all the items so no filter this.lexer.ignoreTokens(1); + filter = { + type: SyntaxType.ALL_FILTER_EXPR, + }; } else { filter = this.parseArrayFilter(); } this.lexer.expect(']'); - if (filter) { - return { - type: exprType, - filter, - }; - } - return undefined; + return { + type: exprType, + filter, + }; } private combineExpressionsAsBinaryExpr( diff --git a/src/translator.ts b/src/translator.ts index 436463d..b30cd52 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -721,7 +721,7 @@ export class JsonTemplateTranslator { const code: string[] = []; if (expr.filter.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { code.push(this.translateIndexFilterExpr(expr.filter as IndexFilterExpression, dest, ctx)); - } else { + } else if (expr.filter.type === SyntaxType.RANGE_FILTER_EXPR) { code.push(this.translateRangeFilterExpr(expr.filter as RangeFilterExpression, dest, ctx)); } return code.join(''); diff --git a/src/types.ts b/src/types.ts index 9235bbd..1c2a81d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -74,6 +74,7 @@ export enum SyntaxType { SPREAD_EXPR = 'spread_expr', CONDITIONAL_EXPR = 'conditional_expr', ARRAY_INDEX_FILTER_EXPR = 'array_index_filter_expr', + ALL_FILTER_EXPR = 'all_filter_expr', OBJECT_INDEX_FILTER_EXPR = 'object_index_filter_expr', RANGE_FILTER_EXPR = 'range_filter_expr', OBJECT_FILTER_EXPR = 'object_filter_expr', @@ -192,12 +193,14 @@ export interface IndexFilterExpression extends Expression { exclude?: boolean; } +export interface AllFilterExpression extends Expression {} + export interface ObjectFilterExpression extends Expression { filter: Expression; } export interface ArrayFilterExpression extends Expression { - filter: RangeFilterExpression | IndexFilterExpression; + filter: RangeFilterExpression | IndexFilterExpression | AllFilterExpression; } export interface LiteralExpression extends Expression { value: string | number | boolean | null | undefined; From 3b90cc95548f0e6d9fb3092632755ad82a0121c3 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Wed, 22 May 2024 13:18:52 +0530 Subject: [PATCH 04/29] chore: add json path tests --- test/scenarios/paths/data.ts | 1 + test/scenarios/paths/json_path.jt | 1 + 2 files changed, 2 insertions(+) diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts index e617a34..961bbc0 100644 --- a/test/scenarios/paths/data.ts +++ b/test/scenarios/paths/data.ts @@ -114,6 +114,7 @@ export const data: Scenario[] = [ b: 3, }, ], + [2, 4, 6], ], }, { diff --git a/test/scenarios/paths/json_path.jt b/test/scenarios/paths/json_path.jt index 55f9c01..2749f9a 100644 --- a/test/scenarios/paths/json_path.jt +++ b/test/scenarios/paths/json_path.jt @@ -2,4 +2,5 @@ ~j $.foo, ~j $.items[*], ~j $.items[?(@.a>1)], + ~j $.items.(@.a + @.b) ] \ No newline at end of file From a2af49fd320dd530e5587c443e52617ce52c6fa9 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Wed, 22 May 2024 14:11:09 +0530 Subject: [PATCH 05/29] refactor: use path type stack to handle child paths --- src/parser.ts | 15 ++++++++++++++- test/scenarios/paths/data.ts | 1 + test/scenarios/paths/json_path.jt | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 85a3926..0856c23 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -46,12 +46,15 @@ export class JsonTemplateParser { private options?: EngineOptions; + private pathTypesStack: PathType[]; + // indicates currently how many loops being parsed private loopCount = 0; constructor(lexer: JsonTemplateLexer, options?: EngineOptions) { this.lexer = lexer; this.options = options; + this.pathTypesStack = []; } parse(): Expression { @@ -289,6 +292,13 @@ export class JsonTemplateParser { } } + private getCurrentPathType(): PathType | undefined { + if (this.pathTypesStack.length > 0) { + return this.pathTypesStack[this.pathTypesStack.length - 1]; + } + return undefined; + } + private parsePathType(): PathType { if (this.lexer.matchSimplePath()) { this.lexer.ignoreTokens(1); @@ -302,11 +312,13 @@ export class JsonTemplateParser { this.lexer.ignoreTokens(1); return PathType.JSON; } - return this.options?.defaultPathType ?? PathType.RICH; + + return this.getCurrentPathType() ?? this.options?.defaultPathType ?? PathType.RICH; } private parsePath(options?: { root?: Expression }): PathExpression | Expression { const pathType = this.parsePathType(); + this.pathTypesStack.push(pathType); const expr: PathExpression = { type: SyntaxType.PATH, root: this.parsePathRoot(pathType, options?.root), @@ -317,6 +329,7 @@ export class JsonTemplateParser { expr.pathType = PathType.SIMPLE; return expr; } + this.pathTypesStack.pop(); return JsonTemplateParser.updatePathExpr(expr); } diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts index 961bbc0..cf7b38f 100644 --- a/test/scenarios/paths/data.ts +++ b/test/scenarios/paths/data.ts @@ -73,6 +73,7 @@ export const data: Scenario[] = [ templatePath: 'json_path.jt', input: { foo: 'bar', + min: 1, items: [ { a: 1, diff --git a/test/scenarios/paths/json_path.jt b/test/scenarios/paths/json_path.jt index 2749f9a..cddee15 100644 --- a/test/scenarios/paths/json_path.jt +++ b/test/scenarios/paths/json_path.jt @@ -1,6 +1,6 @@ [ ~j $.foo, ~j $.items[*], - ~j $.items[?(@.a>1)], + ~j $.items[?(@.a>$.min)], ~j $.items.(@.a + @.b) ] \ No newline at end of file From e6309c1d7d81189325ed291788af1b251bbaaec2 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Fri, 24 May 2024 14:26:52 +0530 Subject: [PATCH 06/29] feat: add flat to object mapping converter --- .gitignore | 4 +- .nvmrc | 2 +- src/constants.ts | 3 + src/engine.ts | 71 ++-- src/parser.ts | 15 +- src/translator.ts | 2 +- src/types.ts | 10 + src/utils/common.test.ts | 66 ++++ src/{utils.ts => utils/common.ts} | 2 +- src/utils/converter.test.ts | 511 ++++++++++++++++++++++++++ src/utils/converter.ts | 102 +++++ src/utils/index.ts | 2 + test/scenarios/mappings/data.ts | 103 ++++++ test/scenarios/mappings/mappings.json | 42 +++ test/types.ts | 11 +- test/utils/scenario.ts | 7 +- 16 files changed, 912 insertions(+), 41 deletions(-) create mode 100644 src/utils/common.test.ts rename src/{utils.ts => utils/common.ts} (92%) create mode 100644 src/utils/converter.test.ts create mode 100644 src/utils/converter.ts create mode 100644 src/utils/index.ts create mode 100644 test/scenarios/mappings/data.ts create mode 100644 test/scenarios/mappings/mappings.json diff --git a/.gitignore b/.gitignore index 4187637..77f0619 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,6 @@ build/ .stryker-tmp Mac -.DS_Store \ No newline at end of file +.DS_Store + +test/test_engine.ts \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index a9d0873..561a1e9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.19.0 +18.20.3 diff --git a/src/constants.ts b/src/constants.ts index 401682b..ba00551 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,9 @@ +import { SyntaxType } from './types'; + export const VARS_PREFIX = '___'; export const DATA_PARAM_KEY = '___d'; export const BINDINGS_PARAM_KEY = '___b'; export const BINDINGS_CONTEXT_KEY = '___b.context.'; export const RESULT_KEY = '___r'; export const FUNCTION_RESULT_KEY = '___f'; +export const EMPTY_EXPR = { type: SyntaxType.EMPTY }; diff --git a/src/engine.ts b/src/engine.ts index c83d5e3..17952c9 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -2,8 +2,8 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; import { JsonTemplateTranslator } from './translator'; -import { EngineOptions, Expression } from './types'; -import { CommonUtils } from './utils'; +import { EngineOptions, Expression, FlatMappingAST, FlatMappingPaths } from './types'; +import { ConverterUtils, CommonUtils } from './utils'; export class JsonTemplateEngine { private readonly fn: Function; @@ -12,17 +12,6 @@ export class JsonTemplateEngine { this.fn = fn; } - static create(templateOrExpr: string | Expression, options?: EngineOptions): JsonTemplateEngine { - return new JsonTemplateEngine(this.compileAsAsync(templateOrExpr, options)); - } - - static createAsSync( - templateOrExpr: string | Expression, - options?: EngineOptions, - ): JsonTemplateEngine { - return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options)); - } - private static compileAsSync( templateOrExpr: string | Expression, options?: EngineOptions, @@ -32,7 +21,7 @@ export class JsonTemplateEngine { } private static compileAsAsync( - templateOrExpr: string | Expression, + templateOrExpr: string | Expression | FlatMappingPaths[], options?: EngineOptions, ): Function { return CommonUtils.CreateAsyncFunction( @@ -42,19 +31,6 @@ export class JsonTemplateEngine { ); } - static parse(template: string, options?: EngineOptions): Expression { - const lexer = new JsonTemplateLexer(template); - const parser = new JsonTemplateParser(lexer, options); - return parser.parse(); - } - - static translate(templateOrExpr: string | Expression, options?: EngineOptions): string { - if (typeof templateOrExpr === 'string') { - return this.translateTemplate(templateOrExpr, options); - } - return this.translateExpression(templateOrExpr); - } - private static translateTemplate(template: string, options?: EngineOptions): string { return this.translateExpression(this.parse(template, options)); } @@ -64,6 +40,47 @@ export class JsonTemplateEngine { return translator.translate(); } + static parseMappingPaths(mappings: FlatMappingPaths[]): FlatMappingAST[] { + return mappings.map((mapping) => ({ + input: JsonTemplateEngine.parse(mapping.input).statements[0], + output: JsonTemplateEngine.parse(mapping.output).statements[0], + })); + } + + static create( + templateOrExpr: string | Expression | FlatMappingPaths[], + options?: EngineOptions, + ): JsonTemplateEngine { + return new JsonTemplateEngine(this.compileAsAsync(templateOrExpr, options)); + } + + static createAsSync( + templateOrExpr: string | Expression, + options?: EngineOptions, + ): JsonTemplateEngine { + return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options)); + } + + static parse(template: string, options?: EngineOptions): Expression { + const lexer = new JsonTemplateLexer(template); + const parser = new JsonTemplateParser(lexer, options); + return parser.parse(); + } + + static translate( + template: string | Expression | FlatMappingPaths[], + options?: EngineOptions, + ): string { + if (typeof template === 'string') { + return this.translateTemplate(template, options); + } + let templateExpr = template as Expression; + if (Array.isArray(template)) { + templateExpr = ConverterUtils.convertToObjectMapping(this.parseMappingPaths(template)); + } + return this.translateExpression(templateExpr); + } + evaluate(data: any, bindings: Record = {}): any { return this.fn(data ?? {}, bindings); } diff --git a/src/parser.ts b/src/parser.ts index 0856c23..a652894 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,4 @@ -import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; +import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants'; import { JsonTemplateLexerError, JsonTemplateParserError } from './errors'; import { JsonTemplateLexer } from './lexer'; import { @@ -38,15 +38,14 @@ import { TokenType, UnaryExpression, } from './types'; -import { CommonUtils } from './utils'; +import { CommonUtils } from './utils/common'; -const EMPTY_EXPR = { type: SyntaxType.EMPTY }; export class JsonTemplateParser { private lexer: JsonTemplateLexer; private options?: EngineOptions; - private pathTypesStack: PathType[]; + private pathTypesStack: PathType[] = []; // indicates currently how many loops being parsed private loopCount = 0; @@ -54,7 +53,6 @@ export class JsonTemplateParser { constructor(lexer: JsonTemplateLexer, options?: EngineOptions) { this.lexer = lexer; this.options = options; - this.pathTypesStack = []; } parse(): Expression { @@ -361,7 +359,12 @@ export class JsonTemplateParser { } let prop: Token | undefined; - if (this.lexer.match('*') || this.lexer.matchID() || this.lexer.matchTokenType(TokenType.STR)) { + if ( + this.lexer.match('*') || + this.lexer.matchID() || + this.lexer.matchKeyword() || + this.lexer.matchTokenType(TokenType.STR) + ) { prop = this.lexer.lex(); } return { diff --git a/src/translator.ts b/src/translator.ts index b30cd52..a0ccb23 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -41,7 +41,7 @@ import { LoopControlExpression, Keyword, } from './types'; -import { CommonUtils } from './utils'; +import { CommonUtils } from './utils/common'; export class JsonTemplateTranslator { private vars: string[] = []; diff --git a/src/types.ts b/src/types.ts index 1c2a81d..8b8317e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -262,3 +262,13 @@ export interface LoopExpression extends Expression { export interface ThrowExpression extends Expression { value: Expression; } + +export type FlatMappingPaths = { + input: string; + output: string; +}; + +export type FlatMappingAST = { + input: PathExpression; + output: PathExpression; +}; diff --git a/src/utils/common.test.ts b/src/utils/common.test.ts new file mode 100644 index 0000000..2d9020e --- /dev/null +++ b/src/utils/common.test.ts @@ -0,0 +1,66 @@ +import { EMPTY_EXPR } from '../constants'; +import { SyntaxType } from '../types'; +import { CommonUtils } from './common'; + +describe('Common Utils tests', () => { + describe('toArray', () => { + it('should return array for non array', () => { + expect(CommonUtils.toArray(1)).toEqual([1]); + }); + it('should return array for array', () => { + expect(CommonUtils.toArray([1])).toEqual([1]); + }); + it('should return array for undefined', () => { + expect(CommonUtils.toArray(undefined)).toBeUndefined(); + }); + }); + describe('getLastElement', () => { + it('should return last element of non empty array', () => { + expect(CommonUtils.getLastElement([1, 2])).toEqual(2); + }); + it('should return undefined for empty array', () => { + expect(CommonUtils.getLastElement([])).toBeUndefined(); + }); + }); + describe('convertToStatementsExpr', () => { + it('should return statement expression for no expressions', () => { + expect(CommonUtils.convertToStatementsExpr()).toEqual({ + type: SyntaxType.STATEMENTS_EXPR, + statements: [], + }); + }); + it('should return statement expression for single expression', () => { + expect(CommonUtils.convertToStatementsExpr(EMPTY_EXPR)).toEqual({ + type: SyntaxType.STATEMENTS_EXPR, + statements: [EMPTY_EXPR], + }); + }); + it('should return statement expression for multiple expression', () => { + expect(CommonUtils.convertToStatementsExpr(EMPTY_EXPR, EMPTY_EXPR)).toEqual({ + type: SyntaxType.STATEMENTS_EXPR, + statements: [EMPTY_EXPR, EMPTY_EXPR], + }); + }); + }); + + describe('escapeStr', () => { + it('should return emtpy string for non string input', () => { + expect(CommonUtils.escapeStr(undefined)).toEqual(''); + }); + it('should return escaped string for simple string input', () => { + expect(CommonUtils.escapeStr('aabc')).toEqual(`'aabc'`); + }); + + it('should return escaped string for string with escape characters', () => { + expect(CommonUtils.escapeStr(`a\nb'c`)).toEqual(`'a\nb\\'c'`); + }); + }); + describe('CreateAsyncFunction', () => { + it('should return async function from code without args', async () => { + expect(await CommonUtils.CreateAsyncFunction('return 1')()).toEqual(1); + }); + it('should return async function from code with args', async () => { + expect(await CommonUtils.CreateAsyncFunction('input', 'return input')(1)).toEqual(1); + }); + }); +}); diff --git a/src/utils.ts b/src/utils/common.ts similarity index 92% rename from src/utils.ts rename to src/utils/common.ts index 07a8bcd..7a4a77d 100644 --- a/src/utils.ts +++ b/src/utils/common.ts @@ -1,4 +1,4 @@ -import { Expression, StatementsExpression, SyntaxType } from './types'; +import { Expression, StatementsExpression, SyntaxType } from '../types'; export class CommonUtils { static toArray(val: any): any[] | undefined { diff --git a/src/utils/converter.test.ts b/src/utils/converter.test.ts new file mode 100644 index 0000000..de62bbd --- /dev/null +++ b/src/utils/converter.test.ts @@ -0,0 +1,511 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { DATA_PARAM_KEY } from '../constants'; +import { PathType, SyntaxType, TokenType } from '../types'; +import { ConverterUtils } from './converter'; +import { JsonTemplateEngine } from '../engine'; + +describe('Converter Utils Tests', () => { + describe('convertToObjectMapping', () => { + it('should convert single simple flat mapping to object mapping', () => { + const objectMapping = ConverterUtils.convertToObjectMapping( + JsonTemplateEngine.parseMappingPaths([ + { + input: '.a.b', + output: '.foo.bar', + }, + ]), + ); + expect(objectMapping).toMatchObject({ + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'foo', + value: { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'a' }, + }, + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'b' }, + }, + ], + pathType: PathType.RICH, + }, + }, + ], + }, + }, + ], + }); + }); + it('should convert single simple flat mapping with array index to object mapping', () => { + const objectMapping = ConverterUtils.convertToObjectMapping( + JsonTemplateEngine.parseMappingPaths([ + { + input: '.a.b', + output: '.foo[0].bar', + }, + ]), + ); + expect(objectMapping).toMatchObject({ + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'foo', + value: { + type: SyntaxType.ARRAY_EXPR, + elements: [ + { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'a' }, + }, + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'b' }, + }, + ], + pathType: PathType.RICH, + }, + }, + ], + }, + ], + }, + }, + ], + }); + }); + it('should convert single flat array mapping to object mapping', () => { + const objectMapping = ConverterUtils.convertToObjectMapping( + JsonTemplateEngine.parseMappingPaths([ + { + input: '.a[*].b', + output: '.foo[*].bar', + }, + ]), + ); + expect(objectMapping).toMatchObject({ + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'foo', + value: { + type: SyntaxType.PATH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'a' }, + }, + { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: { type: SyntaxType.ALL_FILTER_EXPR }, + }, + { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'b' }, + }, + ], + }, + }, + ], + }, + ], + pathType: PathType.RICH, + returnAsArray: true, + }, + }, + ], + }); + }); + it('should convert multiple flat array mapping to object mapping', () => { + const objectMapping = ConverterUtils.convertToObjectMapping( + JsonTemplateEngine.parseMappingPaths([ + { + input: '.a[*].b', + output: '.foo[*].bar', + }, + { + input: '.a[*].c', + output: '.foo[*].car', + }, + ]), + ); + expect(objectMapping).toMatchObject({ + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'foo', + value: { + type: SyntaxType.PATH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'a' }, + }, + { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: { type: SyntaxType.ALL_FILTER_EXPR }, + }, + { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'b' }, + }, + ], + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'car', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'c' }, + }, + ], + }, + }, + ], + }, + ], + pathType: PathType.RICH, + returnAsArray: true, + }, + }, + ], + }); + }); + it('should convert multiple flat array mapping to object mapping with root level mapping', () => { + const objectMapping = ConverterUtils.convertToObjectMapping( + JsonTemplateEngine.parseMappingPaths([ + { + input: '~j $.root', + output: '.foo[*].boot', + }, + { + input: '.a[*].b', + output: '.foo[*].bar', + }, + { + input: '.a[*].c', + output: '.foo[*].car', + }, + ]), + ); + expect(objectMapping).toMatchObject({ + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'foo', + value: { + type: SyntaxType.PATH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'a' }, + }, + { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: { type: SyntaxType.ALL_FILTER_EXPR }, + }, + { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'boot', + value: { + type: SyntaxType.PATH, + root: DATA_PARAM_KEY, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'root', + }, + }, + ], + pathType: PathType.JSON, + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'b' }, + }, + ], + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'car', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { type: TokenType.ID, value: 'c' }, + }, + ], + }, + }, + ], + }, + ], + pathType: PathType.RICH, + returnAsArray: true, + }, + }, + ], + }); + }); + it('should convert multiple flat nested array mapping to object mapping with root level mapping', () => { + const objectMapping = ConverterUtils.convertToObjectMapping( + JsonTemplateEngine.parseMappingPaths([ + { + input: '~j $.root', + output: '.foo[*].boot', + }, + { + input: '.a[*].b', + output: '.foo[*].bar', + }, + { + input: '.a[*].c', + output: '.foo[*].car', + }, + { + input: '.a[*].d[*].b', + output: '.foo[*].dog[*].bar', + }, + { + input: '.a[*].d[*].c', + output: '.foo[*].dog[*].car', + }, + ]), + ); + expect(objectMapping).toMatchObject({ + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'foo', + value: { + type: SyntaxType.PATH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'a', + }, + }, + { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: { + type: SyntaxType.ALL_FILTER_EXPR, + }, + }, + { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'boot', + value: { + type: SyntaxType.PATH, + root: DATA_PARAM_KEY, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'root', + }, + }, + ], + pathType: PathType.JSON, + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'b', + }, + }, + ], + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'car', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'c', + }, + }, + ], + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'dog', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'd', + }, + }, + { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: { + type: SyntaxType.ALL_FILTER_EXPR, + }, + }, + { + type: SyntaxType.OBJECT_EXPR, + props: [ + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'bar', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'b', + }, + }, + ], + }, + }, + { + type: SyntaxType.OBJECT_PROP_EXPR, + key: 'car', + value: { + type: SyntaxType.PATH, + pathType: PathType.RICH, + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'c', + }, + }, + ], + }, + }, + ], + }, + ], + returnAsArray: true, + }, + }, + ], + }, + ], + pathType: PathType.RICH, + returnAsArray: true, + }, + }, + ], + }); + }); + }); +}); diff --git a/src/utils/converter.ts b/src/utils/converter.ts new file mode 100644 index 0000000..8614212 --- /dev/null +++ b/src/utils/converter.ts @@ -0,0 +1,102 @@ +import { + SyntaxType, + PathExpression, + ArrayFilterExpression, + ObjectPropExpression, + ArrayExpression, + ObjectExpression, + FlatMappingAST, +} from '../types'; + +type OutputObject = { + [key: string]: { + [key: string]: OutputObject | ObjectPropExpression[]; + }; +}; +/** + * Convert Flat to Object Mappings + */ +export class ConverterUtils { + // eslint-disable-next-line sonarjs/cognitive-complexity + static convertToObjectMapping(flatMappingAST: FlatMappingAST[]): ObjectExpression { + const outputAST: ObjectExpression = { + type: SyntaxType.OBJECT_EXPR, + props: [] as ObjectPropExpression[], + }; + const outputObject: OutputObject = {}; + for (const flatMapping of flatMappingAST) { + let currentOutputObject = outputObject; + let currentOutputAST = outputAST.props; + let currentInputAST = flatMapping.input; + const numOutputParts = flatMapping.output.parts.length; + for (let i = 0; i < numOutputParts; i++) { + const outputPart = flatMapping.output.parts[i]; + if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { + const key = outputPart.prop.value; + // If it's the last part, assign the value + if (i === numOutputParts - 1) { + currentOutputAST.push({ + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: currentInputAST, + } as ObjectPropExpression); + break; + } + + const nextOuptutPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; + const items = [] as ObjectPropExpression[]; + const objectExpr: ObjectExpression = { + type: SyntaxType.OBJECT_EXPR, + props: items, + }; + + if (!currentOutputObject[key]) { + const outputPropAST = { + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: objectExpr, + } as ObjectPropExpression; + if (nextOuptutPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + const arrayExpr: ArrayExpression = { + type: SyntaxType.ARRAY_EXPR, + elements: [], + }; + arrayExpr.elements[nextOuptutPart.filter.indexes.elements[0].value] = objectExpr; + outputPropAST.value = arrayExpr; + } + + currentOutputObject[key] = { + $___items: items, + $___ast: outputPropAST, + }; + currentOutputAST.push(outputPropAST); + } + if (nextOuptutPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { + const filterIndex = currentInputAST.parts.findIndex( + (part) => part.type === SyntaxType.ARRAY_FILTER_EXPR, + ); + if (filterIndex !== -1) { + const inputRemainingParts = currentInputAST.parts.splice(filterIndex + 1); + currentInputAST.returnAsArray = true; + const outputPropAST = currentOutputObject[key].$___ast as ObjectPropExpression; + if (outputPropAST.value.type !== SyntaxType.PATH) { + currentInputAST.parts.push(outputPropAST.value); + outputPropAST.value = currentInputAST; + } + currentInputAST = { + type: SyntaxType.PATH, + pathType: currentInputAST.pathType, + parts: inputRemainingParts, + } as PathExpression; + } + } + // Move to the next level + currentOutputAST = currentOutputObject[key].$___items as ObjectPropExpression[]; + currentOutputObject = currentOutputObject[key] as OutputObject; + } + } + } + + return outputAST; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..f0cb94d --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './common'; +export * from './converter'; diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts new file mode 100644 index 0000000..5c927aa --- /dev/null +++ b/test/scenarios/mappings/data.ts @@ -0,0 +1,103 @@ +import { Scenario } from '../../types'; + +export const data: Scenario[] = [ + { + containsMappings: true, + input: { + discount: 10, + event: 'purchase', + products: [ + { + id: 1, + name: 'p1', + category: 'baby', + variations: [ + { + color: 'blue', + size: 1, + }, + { + size: 2, + }, + ], + }, + { + id: 2, + name: 'p2', + variations: [ + { + length: 1, + }, + { + color: 'red', + length: 2, + }, + ], + }, + { + id: 3, + name: 'p3', + category: 'home', + variations: [ + { + width: 1, + height: 2, + length: 3, + }, + ], + }, + ], + }, + output: { + events: [ + { + items: [ + { + discount: 10, + product_id: 1, + product_name: 'p1', + product_category: 'baby', + options: [ + { + s: 1, + c: 'blue', + }, + { + s: 2, + }, + ], + }, + { + discount: 10, + product_id: 2, + product_name: 'p2', + options: [ + { + l: 1, + }, + { + l: 2, + c: 'red', + }, + ], + }, + { + discount: 10, + product_id: 3, + product_name: 'p3', + product_category: 'home', + options: [ + { + l: 3, + w: 1, + h: 2, + }, + ], + }, + ], + name: 'purchase', + }, + ], + }, + }, +]; diff --git a/test/scenarios/mappings/mappings.json b/test/scenarios/mappings/mappings.json new file mode 100644 index 0000000..7a148b6 --- /dev/null +++ b/test/scenarios/mappings/mappings.json @@ -0,0 +1,42 @@ +[ + { + "input": "~j $.discount", + "output": "~j $.events[0].items[*].discount" + }, + { + "input": "~j $.products[*].id", + "output": "~j $.events[0].items[*].product_id" + }, + { + "input": "~j $.event", + "output": "~j $.events[0].name" + }, + { + "input": "~j $.products[*].name", + "output": "~j $.events[0].items[*].product_name" + }, + { + "input": "~j $.products[*].category", + "output": "~j $.events[0].items[*].product_category" + }, + { + "input": "~j $.products[*].variations[*].size", + "output": "~j $.events[0].items[*].options[*].s" + }, + { + "input": "~j $.products[*].variations[*].length", + "output": "~j $.events[0].items[*].options[*].l" + }, + { + "input": "~j $.products[*].variations[*].width", + "output": "~j $.events[0].items[*].options[*].w" + }, + { + "input": "~j $.products[*].variations[*].color", + "output": "~j $.events[0].items[*].options[*].c" + }, + { + "input": "~j $.products[*].variations[*].height", + "output": "~j $.events[0].items[*].options[*].h" + } +] diff --git a/test/types.ts b/test/types.ts index ee0c7ec..f8b8f04 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,9 +1,10 @@ -import { EngineOptions, PathType } from '../src'; +import { EngineOptions, FlatMappingPaths, PathType } from '../src'; export type Scenario = { description?: string; input?: any; templatePath?: string; + containsMappings?: true; options?: EngineOptions; bindings?: any; output?: any; @@ -12,6 +13,12 @@ export type Scenario = { export namespace Scenario { export function getTemplatePath(scenario: Scenario): string { - return scenario.templatePath || 'template.jt'; + if (scenario.templatePath) { + return scenario.templatePath; + } + if (scenario.containsMappings) { + return 'mappings.json'; + } + return 'template.jt'; } } diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts index 6187fbc..d314699 100644 --- a/test/utils/scenario.ts +++ b/test/utils/scenario.ts @@ -1,12 +1,15 @@ import { readFileSync } from 'fs'; import { join } from 'path'; -import { JsonTemplateEngine, PathType } from '../../src'; +import { FlatMappingPaths, JsonTemplateEngine, PathType } from '../../src'; import { Scenario } from '../types'; export class ScenarioUtils { static createTemplateEngine(scenarioDir: string, scenario: Scenario): JsonTemplateEngine { const templatePath = join(scenarioDir, Scenario.getTemplatePath(scenario)); - const template = readFileSync(templatePath, 'utf-8'); + let template: string | FlatMappingPaths[] = readFileSync(templatePath, 'utf-8'); + if (scenario.containsMappings) { + template = JSON.parse(template) as FlatMappingPaths[]; + } scenario.options = scenario.options || {}; scenario.options.defaultPathType = scenario.options.defaultPathType || PathType.SIMPLE; return JsonTemplateEngine.create(template, scenario.options); From 312cad17dab4ee3dec71a282be0ea616e3474359 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Fri, 24 May 2024 14:41:44 +0530 Subject: [PATCH 07/29] refactor: static utils to modules --- src/engine.ts | 8 +- src/parser.ts | 14 ++-- src/translator.ts | 12 +-- src/utils/common.test.ts | 2 +- src/utils/common.ts | 52 +++++++------ src/utils/converter.test.ts | 14 ++-- src/utils/converter.ts | 144 ++++++++++++++++++------------------ 7 files changed, 121 insertions(+), 125 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 17952c9..0e6972a 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -3,7 +3,7 @@ import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; import { JsonTemplateTranslator } from './translator'; import { EngineOptions, Expression, FlatMappingAST, FlatMappingPaths } from './types'; -import { ConverterUtils, CommonUtils } from './utils'; +import { CreateAsyncFunction, convertToObjectMapping } from './utils'; export class JsonTemplateEngine { private readonly fn: Function; @@ -24,7 +24,7 @@ export class JsonTemplateEngine { templateOrExpr: string | Expression | FlatMappingPaths[], options?: EngineOptions, ): Function { - return CommonUtils.CreateAsyncFunction( + return CreateAsyncFunction( DATA_PARAM_KEY, BINDINGS_PARAM_KEY, this.translate(templateOrExpr, options), @@ -76,12 +76,12 @@ export class JsonTemplateEngine { } let templateExpr = template as Expression; if (Array.isArray(template)) { - templateExpr = ConverterUtils.convertToObjectMapping(this.parseMappingPaths(template)); + templateExpr = convertToObjectMapping(this.parseMappingPaths(template)); } return this.translateExpression(templateExpr); } - evaluate(data: any, bindings: Record = {}): any { + evaluate(data: unknown, bindings: Record = {}): unknown { return this.fn(data ?? {}, bindings); } } diff --git a/src/parser.ts b/src/parser.ts index a652894..110372b 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -38,7 +38,7 @@ import { TokenType, UnaryExpression, } from './types'; -import { CommonUtils } from './utils/common'; +import { convertToStatementsExpr, getLastElement, toArray } from './utils/common'; export class JsonTemplateParser { private lexer: JsonTemplateLexer; @@ -225,7 +225,7 @@ export class JsonTemplateParser { let parts: Expression[] = []; let newParts: Expression[] | undefined; // eslint-disable-next-line no-cond-assign - while ((newParts = CommonUtils.toArray(this.parsePathPart()))) { + while ((newParts = toArray(this.parsePathPart()))) { parts = parts.concat(newParts); if (newParts[0].type === SyntaxType.FUNCTION_CALL_EXPR) { break; @@ -1137,7 +1137,7 @@ export class JsonTemplateParser { const expr = this.parseBaseExpr(); return { type: SyntaxType.FUNCTION_EXPR, - body: CommonUtils.convertToStatementsExpr(expr), + body: convertToStatementsExpr(expr), params: ['...args'], async: asyncFn, }; @@ -1308,7 +1308,7 @@ export class JsonTemplateParser { return { type: SyntaxType.FUNCTION_EXPR, block: true, - body: CommonUtils.convertToStatementsExpr(expr), + body: convertToStatementsExpr(expr), }; } @@ -1342,7 +1342,7 @@ export class JsonTemplateParser { fnExpr: FunctionCallExpression, pathExpr: PathExpression, ): FunctionCallExpression | PathExpression { - const lastPart = CommonUtils.getLastElement(pathExpr.parts); + const lastPart = getLastElement(pathExpr.parts); // Updated const newFnExpr = fnExpr; if (lastPart?.type === SyntaxType.SELECTOR) { @@ -1401,13 +1401,13 @@ export class JsonTemplateParser { } const shouldConvertAsBlock = JsonTemplateParser.pathContainsVariables(newPathExpr.parts); - let lastPart = CommonUtils.getLastElement(newPathExpr.parts); + let lastPart = getLastElement(newPathExpr.parts); let fnExpr: FunctionCallExpression | undefined; if (lastPart?.type === SyntaxType.FUNCTION_CALL_EXPR) { fnExpr = newPathExpr.parts.pop() as FunctionCallExpression; } - lastPart = CommonUtils.getLastElement(newPathExpr.parts); + lastPart = getLastElement(newPathExpr.parts); if (lastPart?.type === SyntaxType.PATH_OPTIONS) { newPathExpr.parts.pop(); newPathExpr.returnAsArray = lastPart.options?.toArray; diff --git a/src/translator.ts b/src/translator.ts index a0ccb23..91f4482 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -41,7 +41,7 @@ import { LoopControlExpression, Keyword, } from './types'; -import { CommonUtils } from './utils/common'; +import { convertToStatementsExpr, escapeStr } from './utils/common'; export class JsonTemplateTranslator { private vars: string[] = []; @@ -390,7 +390,7 @@ export class JsonTemplateTranslator { const valuesCode = JsonTemplateTranslator.returnObjectValues(ctx); code.push(`${dest} = ${valuesCode}.flat();`); } else if (prop) { - const propStr = CommonUtils.escapeStr(prop); + const propStr = escapeStr(prop); code.push(`if(${ctx} && Object.prototype.hasOwnProperty.call(${ctx}, ${propStr})){`); code.push(`${dest}=${ctx}[${propStr}];`); code.push('} else {'); @@ -418,7 +418,7 @@ export class JsonTemplateTranslator { const result = this.acquireVar(); code.push(JsonTemplateTranslator.generateAssignmentCode(result, '[]')); const { prop } = expr; - const propStr = CommonUtils.escapeStr(prop?.value); + const propStr = escapeStr(prop?.value); code.push(`${ctxs}=[${baseCtx}];`); code.push(`while(${ctxs}.length > 0) {`); code.push(`${currCtx} = ${ctxs}.shift();`); @@ -454,7 +454,7 @@ export class JsonTemplateTranslator { } const fnExpr: FunctionExpression = { type: SyntaxType.FUNCTION_EXPR, - body: CommonUtils.convertToStatementsExpr(...expr.statements), + body: convertToStatementsExpr(...expr.statements), block: true, }; return this.translateExpr(fnExpr, dest, ctx); @@ -558,7 +558,7 @@ export class JsonTemplateTranslator { private getSimplePathSelector(expr: SelectorExpression, isAssignment: boolean): string { if (expr.prop?.type === TokenType.STR) { - return `${isAssignment ? '' : '?.'}[${CommonUtils.escapeStr(expr.prop?.value)}]`; + return `${isAssignment ? '' : '?.'}[${escapeStr(expr.prop?.value)}]`; } return `${isAssignment ? '' : '?'}.${expr.prop?.value}`; } @@ -703,7 +703,7 @@ export class JsonTemplateTranslator { private translateLiteral(type: TokenType, val: any): string { if (type === TokenType.STR) { - return CommonUtils.escapeStr(val); + return escapeStr(val); } return String(val); } diff --git a/src/utils/common.test.ts b/src/utils/common.test.ts index 2d9020e..16d0cd8 100644 --- a/src/utils/common.test.ts +++ b/src/utils/common.test.ts @@ -1,6 +1,6 @@ import { EMPTY_EXPR } from '../constants'; import { SyntaxType } from '../types'; -import { CommonUtils } from './common'; +import * as CommonUtils from './common'; describe('Common Utils tests', () => { describe('toArray', () => { diff --git a/src/utils/common.ts b/src/utils/common.ts index 7a4a77d..4fa1b49 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,36 +1,34 @@ -import { Expression, StatementsExpression, SyntaxType } from '../types'; +import { type Expression, type StatementsExpression, SyntaxType } from '../types'; -export class CommonUtils { - static toArray(val: any): any[] | undefined { - if (val === undefined || val === null) { - return undefined; - } - return Array.isArray(val) ? val : [val]; +export function toArray(val: any): any[] | undefined { + if (val === undefined || val === null) { + return undefined; } + return Array.isArray(val) ? val : [val]; +} - static getLastElement(arr: T[]): T | undefined { - if (!arr.length) { - return undefined; - } - return arr[arr.length - 1]; +export function 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, - }; - } +export function convertToStatementsExpr(...expressions: Expression[]): StatementsExpression { + return { + type: SyntaxType.STATEMENTS_EXPR, + statements: expressions, + }; +} - static CreateAsyncFunction(...args) { - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - return async function () {}.constructor(...args); - } +export function CreateAsyncFunction(...args) { + // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names + return async function () {}.constructor(...args); +} - static escapeStr(s?: string): string { - if (typeof s !== 'string') { - return ''; - } - return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; +export function escapeStr(s?: string): string { + if (typeof s !== 'string') { + return ''; } + return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; } diff --git a/src/utils/converter.test.ts b/src/utils/converter.test.ts index de62bbd..efde4fd 100644 --- a/src/utils/converter.test.ts +++ b/src/utils/converter.test.ts @@ -1,13 +1,13 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { DATA_PARAM_KEY } from '../constants'; import { PathType, SyntaxType, TokenType } from '../types'; -import { ConverterUtils } from './converter'; +import { convertToObjectMapping } from './converter'; import { JsonTemplateEngine } from '../engine'; describe('Converter Utils Tests', () => { describe('convertToObjectMapping', () => { it('should convert single simple flat mapping to object mapping', () => { - const objectMapping = ConverterUtils.convertToObjectMapping( + const objectMapping = convertToObjectMapping( JsonTemplateEngine.parseMappingPaths([ { input: '.a.b', @@ -51,7 +51,7 @@ describe('Converter Utils Tests', () => { }); }); it('should convert single simple flat mapping with array index to object mapping', () => { - const objectMapping = ConverterUtils.convertToObjectMapping( + const objectMapping = convertToObjectMapping( JsonTemplateEngine.parseMappingPaths([ { input: '.a.b', @@ -100,7 +100,7 @@ describe('Converter Utils Tests', () => { }); }); it('should convert single flat array mapping to object mapping', () => { - const objectMapping = ConverterUtils.convertToObjectMapping( + const objectMapping = convertToObjectMapping( JsonTemplateEngine.parseMappingPaths([ { input: '.a[*].b', @@ -155,7 +155,7 @@ describe('Converter Utils Tests', () => { }); }); it('should convert multiple flat array mapping to object mapping', () => { - const objectMapping = ConverterUtils.convertToObjectMapping( + const objectMapping = convertToObjectMapping( JsonTemplateEngine.parseMappingPaths([ { input: '.a[*].b', @@ -229,7 +229,7 @@ describe('Converter Utils Tests', () => { }); }); it('should convert multiple flat array mapping to object mapping with root level mapping', () => { - const objectMapping = ConverterUtils.convertToObjectMapping( + const objectMapping = convertToObjectMapping( JsonTemplateEngine.parseMappingPaths([ { input: '~j $.root', @@ -326,7 +326,7 @@ describe('Converter Utils Tests', () => { }); }); it('should convert multiple flat nested array mapping to object mapping with root level mapping', () => { - const objectMapping = ConverterUtils.convertToObjectMapping( + const objectMapping = convertToObjectMapping( JsonTemplateEngine.parseMappingPaths([ { input: '~j $.root', diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 8614212..978f276 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -16,87 +16,85 @@ type OutputObject = { /** * Convert Flat to Object Mappings */ -export class ConverterUtils { - // eslint-disable-next-line sonarjs/cognitive-complexity - static convertToObjectMapping(flatMappingAST: FlatMappingAST[]): ObjectExpression { - const outputAST: ObjectExpression = { - type: SyntaxType.OBJECT_EXPR, - props: [] as ObjectPropExpression[], - }; - const outputObject: OutputObject = {}; - for (const flatMapping of flatMappingAST) { - let currentOutputObject = outputObject; - let currentOutputAST = outputAST.props; - let currentInputAST = flatMapping.input; - const numOutputParts = flatMapping.output.parts.length; - for (let i = 0; i < numOutputParts; i++) { - const outputPart = flatMapping.output.parts[i]; - if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { - const key = outputPart.prop.value; - // If it's the last part, assign the value - if (i === numOutputParts - 1) { - currentOutputAST.push({ - type: SyntaxType.OBJECT_PROP_EXPR, - key, - value: currentInputAST, - } as ObjectPropExpression); - break; - } - - const nextOuptutPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; - const items = [] as ObjectPropExpression[]; - const objectExpr: ObjectExpression = { - type: SyntaxType.OBJECT_EXPR, - props: items, - }; +// eslint-disable-next-line sonarjs/cognitive-complexity +export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): ObjectExpression { + const outputAST: ObjectExpression = { + type: SyntaxType.OBJECT_EXPR, + props: [] as ObjectPropExpression[], + }; + const outputObject: OutputObject = {}; + for (const flatMapping of flatMappingAST) { + let currentOutputObject = outputObject; + let currentOutputAST = outputAST.props; + let currentInputAST = flatMapping.input; + const numOutputParts = flatMapping.output.parts.length; + for (let i = 0; i < numOutputParts; i++) { + const outputPart = flatMapping.output.parts[i]; + if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { + const key = outputPart.prop.value; + // If it's the last part, assign the value + if (i === numOutputParts - 1) { + currentOutputAST.push({ + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: currentInputAST, + } as ObjectPropExpression); + break; + } - if (!currentOutputObject[key]) { - const outputPropAST = { - type: SyntaxType.OBJECT_PROP_EXPR, - key, - value: objectExpr, - } as ObjectPropExpression; - if (nextOuptutPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - const arrayExpr: ArrayExpression = { - type: SyntaxType.ARRAY_EXPR, - elements: [], - }; - arrayExpr.elements[nextOuptutPart.filter.indexes.elements[0].value] = objectExpr; - outputPropAST.value = arrayExpr; - } + const nextOuptutPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; + const items = [] as ObjectPropExpression[]; + const objectExpr: ObjectExpression = { + type: SyntaxType.OBJECT_EXPR, + props: items, + }; - currentOutputObject[key] = { - $___items: items, - $___ast: outputPropAST, + if (!currentOutputObject[key]) { + const outputPropAST = { + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: objectExpr, + } as ObjectPropExpression; + if (nextOuptutPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + const arrayExpr: ArrayExpression = { + type: SyntaxType.ARRAY_EXPR, + elements: [], }; - currentOutputAST.push(outputPropAST); + arrayExpr.elements[nextOuptutPart.filter.indexes.elements[0].value] = objectExpr; + outputPropAST.value = arrayExpr; } - if (nextOuptutPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { - const filterIndex = currentInputAST.parts.findIndex( - (part) => part.type === SyntaxType.ARRAY_FILTER_EXPR, - ); - if (filterIndex !== -1) { - const inputRemainingParts = currentInputAST.parts.splice(filterIndex + 1); - currentInputAST.returnAsArray = true; - const outputPropAST = currentOutputObject[key].$___ast as ObjectPropExpression; - if (outputPropAST.value.type !== SyntaxType.PATH) { - currentInputAST.parts.push(outputPropAST.value); - outputPropAST.value = currentInputAST; - } - currentInputAST = { - type: SyntaxType.PATH, - pathType: currentInputAST.pathType, - parts: inputRemainingParts, - } as PathExpression; + + currentOutputObject[key] = { + $___items: items, + $___ast: outputPropAST, + }; + currentOutputAST.push(outputPropAST); + } + if (nextOuptutPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { + const filterIndex = currentInputAST.parts.findIndex( + (part) => part.type === SyntaxType.ARRAY_FILTER_EXPR, + ); + if (filterIndex !== -1) { + const inputRemainingParts = currentInputAST.parts.splice(filterIndex + 1); + currentInputAST.returnAsArray = true; + const outputPropAST = currentOutputObject[key].$___ast as ObjectPropExpression; + if (outputPropAST.value.type !== SyntaxType.PATH) { + currentInputAST.parts.push(outputPropAST.value); + outputPropAST.value = currentInputAST; } + currentInputAST = { + type: SyntaxType.PATH, + pathType: currentInputAST.pathType, + parts: inputRemainingParts, + } as PathExpression; } - // Move to the next level - currentOutputAST = currentOutputObject[key].$___items as ObjectPropExpression[]; - currentOutputObject = currentOutputObject[key] as OutputObject; } + // Move to the next level + currentOutputAST = currentOutputObject[key].$___items as ObjectPropExpression[]; + currentOutputObject = currentOutputObject[key] as OutputObject; } } - - return outputAST; } + + return outputAST; } From f864b2f78fb85ddd1bce31af263f46e9709b5e1e Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Sat, 25 May 2024 07:40:38 +0530 Subject: [PATCH 08/29] feat: add support for regexp --- src/lexer.ts | 62 +++++++++++++++++++++-- src/operators.ts | 3 ++ src/parser.ts | 2 +- src/translator.ts | 3 ++ src/types.ts | 1 + test/scenarios/bad_templates/bad_regex.jt | 1 + test/scenarios/bad_templates/data.ts | 4 ++ test/scenarios/comparisons/data.ts | 2 + test/scenarios/comparisons/template.jt | 4 +- 9 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 test/scenarios/bad_templates/bad_regex.jt diff --git a/src/lexer.ts b/src/lexer.ts index fe18d6c..9cc5ca2 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -279,7 +279,12 @@ export class JsonTemplateLexer { }; } - const token = this.scanPunctuator() ?? this.scanID() ?? this.scanString() ?? this.scanInteger(); + const token = + this.scanRegularExpressions() ?? + this.scanPunctuator() ?? + this.scanID() ?? + this.scanString() ?? + this.scanInteger(); if (token) { return token; } @@ -314,7 +319,8 @@ export class JsonTemplateLexer { token.type === TokenType.FLOAT || token.type === TokenType.STR || token.type === TokenType.NULL || - token.type === TokenType.UNDEFINED + token.type === TokenType.UNDEFINED || + token.type === TokenType.REGEXP ); } @@ -541,7 +547,7 @@ export class JsonTemplateLexer { }; } } else if (ch1 === '=') { - if ('^$*'.indexOf(ch2) >= 0) { + if ('^$*~'.indexOf(ch2) >= 0) { this.idx += 2; return { type: TokenType.PUNCT, @@ -638,6 +644,56 @@ export class JsonTemplateLexer { } } + private static isValidRegExp(regexp: string, modifiers: string) { + try { + RegExp(regexp, modifiers); + return true; + } catch (e) { + return false; + } + } + + private getRegExpModifiers(): string { + let modifiers = ''; + while ('gimsuyv'.includes(this.codeChars[this.idx])) { + modifiers += this.codeChars[this.idx]; + this.idx++; + } + return modifiers; + } + + private scanRegularExpressions(): Token | undefined { + const start = this.idx; + const ch1 = this.codeChars[this.idx]; + + if (ch1 === '/') { + let end = this.idx + 1; + while (end < this.codeChars.length) { + if (this.codeChars[end] === '\n') { + return; + } + if (this.codeChars[end] === '/') { + break; + } + end++; + } + + if (end < this.codeChars.length) { + this.idx = end + 1; + const regexp = this.getCode(start + 1, end); + const modifiers = this.getRegExpModifiers(); + if (!JsonTemplateLexer.isValidRegExp(regexp, modifiers)) { + JsonTemplateLexer.throwError("invalid regular expression '%0'", regexp); + } + return { + type: TokenType.REGEXP, + value: this.getCode(start, this.idx), + range: [start, this.idx], + }; + } + } + } + private scanPunctuator(): Token | undefined { return ( this.scanPunctuatorForDots() ?? diff --git a/src/operators.ts b/src/operators.ts index 31791bd..fe4e14c 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -74,6 +74,9 @@ export const binaryOperators = { '=$': (val1, val2): string => endsWith(val2, val1), + '=~': (val1, val2): string => + `(${val2} instanceof RegExp) ? (${val2}.test(${val1})) : (${val1}==${val2})`, + contains: containsStrict, '==*': (val1, val2): string => containsStrict(val2, val1), diff --git a/src/parser.ts b/src/parser.ts index 110372b..1ce02c8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -682,6 +682,7 @@ export class JsonTemplateParser { this.lexer.match('$=') || this.lexer.match('=$') || this.lexer.match('==*') || + this.lexer.match('=~') || this.lexer.match('=*') ) { return { @@ -690,7 +691,6 @@ export class JsonTemplateParser { args: [expr, this.parseEqualityExpr()], }; } - return expr; } diff --git a/src/translator.ts b/src/translator.ts index 91f4482..6e86a5e 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -705,6 +705,9 @@ export class JsonTemplateTranslator { if (type === TokenType.STR) { return escapeStr(val); } + if (type === TokenType.REGEXP) { + return val; + } return String(val); } diff --git a/src/types.ts b/src/types.ts index 8b8317e..4e4ddd5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,7 @@ export enum TokenType { THROW = 'throw', KEYWORD = 'keyword', EOT = 'eot', + REGEXP = 'regexp', } // In the order of precedence diff --git a/test/scenarios/bad_templates/bad_regex.jt b/test/scenarios/bad_templates/bad_regex.jt new file mode 100644 index 0000000..2dc3072 --- /dev/null +++ b/test/scenarios/bad_templates/bad_regex.jt @@ -0,0 +1 @@ +/?/ \ No newline at end of file diff --git a/test/scenarios/bad_templates/data.ts b/test/scenarios/bad_templates/data.ts index 97e98a3..34e1e6e 100644 --- a/test/scenarios/bad_templates/data.ts +++ b/test/scenarios/bad_templates/data.ts @@ -25,6 +25,10 @@ export const data: Scenario[] = [ templatePath: 'bad_number.jt', error: 'Unexpected token', }, + { + templatePath: 'bad_regex.jt', + error: 'invalid regular expression', + }, { templatePath: 'bad_string.jt', error: 'Unexpected end of template', diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts index aded9f0..c686f71 100644 --- a/test/scenarios/comparisons/data.ts +++ b/test/scenarios/comparisons/data.ts @@ -35,6 +35,8 @@ export const data: Scenario[] = [ true, true, true, + true, + true, ], }, ]; diff --git a/test/scenarios/comparisons/template.jt b/test/scenarios/comparisons/template.jt index 3ab18e7..f39b2c5 100644 --- a/test/scenarios/comparisons/template.jt +++ b/test/scenarios/comparisons/template.jt @@ -30,5 +30,7 @@ "" empty true, "abc" empty false, ["c", "a"] subsetof ["a", "b", "c"], -[] subsetof ["a", "b", "c"] +[] subsetof ["a", "b", "c"], +"abc" =~ /a.*c/, +"AdC" =~ /a.*c/i, ] \ No newline at end of file From 7789caefdbfc6cd976a16015977b6aafd1f48f69 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Sat, 25 May 2024 23:01:38 +0530 Subject: [PATCH 09/29] feat: add anyof noneof operators --- src/lexer.ts | 14 +++++++++- src/operators.ts | 2 ++ src/parser.ts | 38 ++++++++++++++++++++++++-- src/translator.ts | 9 ++---- src/types.ts | 2 ++ test/scenarios/comparisons/data.ts | 2 ++ test/scenarios/comparisons/template.jt | 2 ++ 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/lexer.ts b/src/lexer.ts index 9cc5ca2..b5177d8 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -165,8 +165,20 @@ export class JsonTemplateLexer { return this.matchKeywordValue(Keyword.SUBSETOF); } + matchAnyOf(): boolean { + return this.matchKeywordValue(Keyword.ANYOF); + } + + matchNoneOf(): boolean { + return this.matchKeywordValue(Keyword.NONEOF); + } + matchIN(): boolean { - return this.matchKeywordValue(Keyword.IN) || this.matchKeywordValue(Keyword.NOT_IN); + return this.matchKeywordValue(Keyword.IN); + } + + matchNotIN(): boolean { + return this.matchKeywordValue(Keyword.NOT_IN); } matchFunction(): boolean { diff --git a/src/operators.ts b/src/operators.ts index fe4e14c..eb29417 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -89,6 +89,8 @@ export const binaryOperators = { subsetof: (val1, val2): string => `${val1}.every((el) => {return ${val2}.includes(el);})`, + anyof: (val1, val2): string => `${val1}.some((el) => {return ${val2}.includes(el);})`, + '+': (val1, val2): string => `${val1}+${val2}`, '-': (val1, val2): string => `${val1}-${val2}`, diff --git a/src/parser.ts b/src/parser.ts index 1ce02c8..979ca3a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -694,6 +694,15 @@ export class JsonTemplateParser { return expr; } + private parseInExpr(expr: Expression): BinaryExpression { + this.lexer.ignoreTokens(1); + return { + type: SyntaxType.IN_EXPR, + op: Keyword.IN, + args: [expr, this.parseRelationalExpr()], + }; + } + private parseRelationalExpr(): BinaryExpression | Expression { const expr = this.parseNextExpr(OperatorType.RELATIONAL); @@ -702,19 +711,44 @@ export class JsonTemplateParser { this.lexer.match('>') || this.lexer.match('<=') || this.lexer.match('>=') || - this.lexer.matchIN() || this.lexer.matchContains() || this.lexer.matchSize() || this.lexer.matchEmpty() || + this.lexer.matchAnyOf() || this.lexer.matchSubsetOf() ) { return { - type: this.lexer.matchIN() ? SyntaxType.IN_EXPR : SyntaxType.COMPARISON_EXPR, + type: SyntaxType.COMPARISON_EXPR, op: this.lexer.value(), args: [expr, this.parseRelationalExpr()], }; } + if (this.lexer.matchIN()) { + return this.parseInExpr(expr); + } + + if (this.lexer.matchNotIN()) { + return { + type: SyntaxType.UNARY_EXPR, + op: '!', + arg: this.parseInExpr(expr), + }; + } + + if (this.lexer.matchNoneOf()) { + this.lexer.ignoreTokens(1); + return { + type: SyntaxType.UNARY_EXPR, + op: '!', + arg: { + type: SyntaxType.COMPARISON_EXPR, + op: Keyword.ANYOF, + args: [expr, this.parseRelationalExpr()], + }, + }; + } + return expr; } diff --git a/src/translator.ts b/src/translator.ts index 6e86a5e..b074366 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -687,13 +687,8 @@ export class JsonTemplateTranslator { code.push(this.translateExpr(expr.args[0], val1, ctx)); code.push(this.translateExpr(expr.args[1], val2, ctx)); code.push(`if(typeof ${val2} === 'object'){`); - if (expr.op === Keyword.IN) { - const inCode = `(Array.isArray(${val2}) ? ${val2}.includes(${val1}) : ${val1} in ${val2})`; - code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, inCode)); - } else { - const notInCode = `(Array.isArray(${val2}) ? !${val2}.includes(${val1}) : !(${val1} in ${val2}))`; - code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, notInCode)); - } + const inCode = `(Array.isArray(${val2}) ? ${val2}.includes(${val1}) : ${val1} in ${val2})`; + code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, inCode)); code.push('} else {'); code.push(JsonTemplateTranslator.generateAssignmentCode(resultVar, 'false')); code.push('}'); diff --git a/src/types.ts b/src/types.ts index 4e4ddd5..5a0b694 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,8 @@ export enum Keyword { NOT = 'not', CONTAINS = 'contains', SUBSETOF = 'subsetof', + ANYOF = 'anyof', + NONEOF = 'noneof', EMPTY = 'empty', SIZE = 'size', RETURN = 'return', diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts index c686f71..76cd34a 100644 --- a/test/scenarios/comparisons/data.ts +++ b/test/scenarios/comparisons/data.ts @@ -37,6 +37,8 @@ export const data: Scenario[] = [ true, true, true, + true, + true, ], }, ]; diff --git a/test/scenarios/comparisons/template.jt b/test/scenarios/comparisons/template.jt index f39b2c5..efb5141 100644 --- a/test/scenarios/comparisons/template.jt +++ b/test/scenarios/comparisons/template.jt @@ -33,4 +33,6 @@ [] subsetof ["a", "b", "c"], "abc" =~ /a.*c/, "AdC" =~ /a.*c/i, +["a", "b"] anyof ["a", "c"], +["c", "d"] noneof ["a", "b"] ] \ No newline at end of file From 17241c379764651c5d47663c399a47ca6eca4bff Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Sun, 26 May 2024 22:49:41 +0530 Subject: [PATCH 10/29] feat: add support for json path functions --- src/operators.ts | 63 +++++++++++++++++++ src/parser.ts | 11 ++-- src/translator.ts | 29 +++++++-- src/types.ts | 4 +- test/scenarios/functions/array_functions.jt | 4 ++ test/scenarios/functions/data.ts | 8 +++ test/scenarios/standard_functions/data.ts | 27 ++++++++ test/scenarios/standard_functions/template.jt | 14 +++++ 8 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 test/scenarios/functions/array_functions.jt create mode 100644 test/scenarios/standard_functions/data.ts create mode 100644 test/scenarios/standard_functions/template.jt diff --git a/src/operators.ts b/src/operators.ts index eb29417..c5cc633 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -107,3 +107,66 @@ export const binaryOperators = { '**': (val1, val2): string => `${val1}**${val2}`, }; + +export const standardFunctions = { + sum: `function sum(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return arr.reduce((a, b) => a + b, 0); + }`, + max: `function max(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return Math.max(...arr); + }`, + min: `function min(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return Math.min(...arr); + }`, + avg: `function avg(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return sum(arr) / arr.length; + }`, + length: `function length(arr) { + if(!Array.isArray(arr) && typeof arr !== 'string') { + throw new Error('Expected an array or string'); + } + return arr.length; + }`, + stddev: `function stddev(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + const mu = avg(arr); + const diffSq = arr.map((el) => (el - mu) ** 2); + return Math.sqrt(avg(diffSq)); + }`, + first: `function first(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return arr[0]; + }`, + last: `function last(arr) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + return arr[arr.length - 1]; + }`, + index: `function index(arr, i) { + if(!Array.isArray(arr)) { + throw new Error('Expected an array'); + } + if (i < 0) { + return arr[arr.length + i]; + } + return arr[i]; + }`, + keys: `function keys(obj) { return Object.keys(obj); }`, +}; diff --git a/src/parser.ts b/src/parser.ts index 979ca3a..78d4609 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -366,6 +366,9 @@ export class JsonTemplateParser { this.lexer.matchTokenType(TokenType.STR) ) { prop = this.lexer.lex(); + if (prop.type === TokenType.KEYWORD) { + prop.type = TokenType.ID; + } } return { type: SyntaxType.SELECTOR, @@ -1346,10 +1349,6 @@ export class JsonTemplateParser { }; } - private static prependFunctionID(prefix: string, id?: string): string { - return id ? `${prefix}.${id}` : prefix; - } - private static ignoreEmptySelectors(parts: Expression[]): Expression[] { return parts.filter( (part) => !(part.type === SyntaxType.SELECTOR && part.selector === '.' && !part.prop), @@ -1384,13 +1383,11 @@ export class JsonTemplateParser { if (selectorExpr.selector === '.' && selectorExpr.prop?.type === TokenType.ID) { pathExpr.parts.pop(); newFnExpr.id = selectorExpr.prop.value; - newFnExpr.dot = true; } } if (!pathExpr.parts.length && pathExpr.root && typeof pathExpr.root !== 'object') { - newFnExpr.id = this.prependFunctionID(pathExpr.root, fnExpr.id); - newFnExpr.dot = false; + newFnExpr.parent = pathExpr.root; } else { newFnExpr.object = pathExpr; } diff --git a/src/translator.ts b/src/translator.ts index b074366..09a7bc9 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -7,7 +7,7 @@ import { VARS_PREFIX, } from './constants'; import { JsonTemplateTranslatorError } from './errors'; -import { binaryOperators } from './operators'; +import { binaryOperators, standardFunctions } from './operators'; import { ArrayExpression, AssignmentExpression, @@ -39,7 +39,6 @@ import { LoopExpression, IncrementExpression, LoopControlExpression, - Keyword, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; @@ -50,6 +49,8 @@ export class JsonTemplateTranslator { private unusedVars: string[] = []; + private standardFunctions: Record = {}; + private readonly expr: Expression; constructor(expr: Expression) { @@ -91,6 +92,10 @@ export class JsonTemplateTranslator { this.init(); const code: string[] = []; const exprCode = this.translateExpr(this.expr, dest, ctx); + const functions = Object.values(this.standardFunctions); + if (functions.length > 0) { + code.push(functions.join('').replaceAll(/\s+/g, ' ')); + } code.push(`let ${dest};`); code.push(this.vars.map((elm) => `let ${elm};`).join('')); code.push(exprCode); @@ -475,7 +480,13 @@ export class JsonTemplateTranslator { } private getFunctionName(expr: FunctionCallExpression, ctx: string): string { - return expr.dot ? `${ctx}.${expr.id}` : expr.id || ctx; + if (expr.object) { + return expr.id ? `${ctx}.${expr.id}` : ctx; + } + if (expr.parent) { + return expr.id ? `${expr.parent}.${expr.id}` : expr.parent; + } + return expr.id as string; } private translateFunctionCallExpr( @@ -491,7 +502,17 @@ export class JsonTemplateTranslator { code.push(`if(${JsonTemplateTranslator.returnIsNotEmpty(result)}){`); } const functionArgsStr = this.translateSpreadableExpressions(expr.args, result, code); - code.push(result, '=', this.getFunctionName(expr, result), '(', functionArgsStr, ');'); + const functionName = this.getFunctionName(expr, result); + if (expr.id && standardFunctions[expr.id]) { + this.standardFunctions[expr.id] = standardFunctions[expr.id]; + code.push(`if(${functionName} && typeof ${functionName} === 'function'){`); + code.push(result, '=', functionName, '(', functionArgsStr, ');'); + code.push('} else {'); + code.push(result, '=', expr.id, '(', expr.parent || result, ',', functionArgsStr, ');'); + code.push('}'); + } else { + code.push(result, '=', functionName, '(', functionArgsStr, ');'); + } if (expr.object) { code.push('}'); } diff --git a/src/types.ts b/src/types.ts index 5a0b694..2592f48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -234,7 +234,7 @@ export interface FunctionCallExpression extends Expression { args: Expression[]; object?: Expression; id?: string; - dot?: boolean; + parent?: string; } export interface ConditionalExpression extends Expression { @@ -272,6 +272,6 @@ export type FlatMappingPaths = { }; export type FlatMappingAST = { - input: PathExpression; + input: PathExpression | FunctionCallExpression; output: PathExpression; }; diff --git a/test/scenarios/functions/array_functions.jt b/test/scenarios/functions/array_functions.jt new file mode 100644 index 0000000..e2b276c --- /dev/null +++ b/test/scenarios/functions/array_functions.jt @@ -0,0 +1,4 @@ +{ + map: .map(lambda ?0 * 2), + filter: .filter(lambda ?0 % 2 == 0) +} \ No newline at end of file diff --git a/test/scenarios/functions/data.ts b/test/scenarios/functions/data.ts index 0aa3a1a..e582341 100644 --- a/test/scenarios/functions/data.ts +++ b/test/scenarios/functions/data.ts @@ -1,6 +1,14 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ + { + templatePath: 'array_functions.jt', + input: [1, 2, 3, 4], + output: { + map: [2, 4, 6, 8], + filter: [2, 4], + }, + }, { templatePath: 'function_calls.jt', output: ['abc', null, undefined], diff --git a/test/scenarios/standard_functions/data.ts b/test/scenarios/standard_functions/data.ts new file mode 100644 index 0000000..3064221 --- /dev/null +++ b/test/scenarios/standard_functions/data.ts @@ -0,0 +1,27 @@ +import { Scenario } from '../../types'; + +export const data: Scenario[] = [ + { + input: { + arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + obj: { + foo: 1, + bar: 2, + baz: 3, + quux: 4, + }, + }, + output: { + sum: 55, + sum2: 55, + avg: 5.5, + min: 1, + max: 10, + stddev: 2.8722813232690143, + length: 10, + first: 1, + last: 10, + keys: ['foo', 'bar', 'baz', 'quux'], + }, + }, +]; diff --git a/test/scenarios/standard_functions/template.jt b/test/scenarios/standard_functions/template.jt new file mode 100644 index 0000000..fd6ff03 --- /dev/null +++ b/test/scenarios/standard_functions/template.jt @@ -0,0 +1,14 @@ +const arr = .arr; +const obj = .obj; +{ + sum: .arr.sum(), + sum2: (arr.index(0) + arr.index(-1)) * arr.length() / 2, + avg: arr.avg(), + min: arr.min(), + max: arr.max(), + stddev: arr.stddev(), + length: arr.length(), + first: arr.first(), + last: arr.last(), + keys: obj.keys(), +} \ No newline at end of file From 89a73880aceba1a30f14a7bca281568fcd246378 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 27 May 2024 07:23:58 +0530 Subject: [PATCH 11/29] refactor: update mappings test case --- test/scenarios/mappings/data.ts | 10 ++++++++++ test/scenarios/mappings/mappings.json | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index 5c927aa..cb94a2c 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -11,6 +11,8 @@ export const data: Scenario[] = [ id: 1, name: 'p1', category: 'baby', + price: 3, + quantity: 2, variations: [ { color: 'blue', @@ -24,6 +26,8 @@ export const data: Scenario[] = [ { id: 2, name: 'p2', + price: 5, + quantity: 3, variations: [ { length: 1, @@ -38,6 +42,8 @@ export const data: Scenario[] = [ id: 3, name: 'p3', category: 'home', + price: 10, + quantity: 1, variations: [ { width: 1, @@ -66,6 +72,7 @@ export const data: Scenario[] = [ s: 2, }, ], + value: 5.4, }, { discount: 10, @@ -80,6 +87,7 @@ export const data: Scenario[] = [ c: 'red', }, ], + value: 13.5, }, { discount: 10, @@ -93,9 +101,11 @@ export const data: Scenario[] = [ h: 2, }, ], + value: 9, }, ], name: 'purchase', + revenue: 27.9, }, ], }, diff --git a/test/scenarios/mappings/mappings.json b/test/scenarios/mappings/mappings.json index 7a148b6..0625c59 100644 --- a/test/scenarios/mappings/mappings.json +++ b/test/scenarios/mappings/mappings.json @@ -23,6 +23,14 @@ "input": "~j $.products[*].variations[*].size", "output": "~j $.events[0].items[*].options[*].s" }, + { + "input": "~j $.products[*].(@.price * @.quantity * (1 - $.discount / 100))", + "output": "~j $.events[0].items[*].value" + }, + { + "input": "~j $.products[*].(@.price * @.quantity * (1 - $.discount / 100)).sum()", + "output": "~j $.events[0].revenue" + }, { "input": "~j $.products[*].variations[*].length", "output": "~j $.events[0].items[*].options[*].l" From 078895a1963fb25a912a68b7ffd21dd934f31ada Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 27 May 2024 08:20:51 +0530 Subject: [PATCH 12/29] refactor: address pr comments --- src/operators.ts | 2 +- src/parser.ts | 74 ++++++++++++++++++--------------- src/translator.ts | 28 +++++++------ src/types.ts | 4 +- src/utils/common.ts | 2 +- test/scenarios/mappings/data.ts | 2 +- test/types.ts | 8 ++-- test/utils/scenario.ts | 4 +- 8 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/operators.ts b/src/operators.ts index c5cc633..515ecb3 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -168,5 +168,5 @@ export const standardFunctions = { } return arr[i]; }`, - keys: `function keys(obj) { return Object.keys(obj); }`, + keys: 'function keys(obj) { return Object.keys(obj); }', }; diff --git a/src/parser.ts b/src/parser.ts index 78d4609..579fc75 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -223,13 +223,13 @@ export class JsonTemplateParser { private parsePathParts(): Expression[] { let parts: Expression[] = []; - let newParts: Expression[] | undefined; - // eslint-disable-next-line no-cond-assign - while ((newParts = toArray(this.parsePathPart()))) { + let newParts: Expression[] | undefined = toArray(this.parsePathPart()); + while (newParts) { parts = parts.concat(newParts); if (newParts[0].type === SyntaxType.FUNCTION_CALL_EXPR) { break; } + newParts = toArray(this.parsePathPart()); } return JsonTemplateParser.ignoreEmptySelectors(parts); } @@ -270,24 +270,19 @@ export class JsonTemplateParser { if (root) { return root; } - const nextToken = this.lexer.lookahead(); - switch (nextToken.value) { - case '^': - this.lexer.ignoreTokens(1); - return DATA_PARAM_KEY; - case '$': - this.lexer.ignoreTokens(1); - return pathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY; - case '@': - this.lexer.ignoreTokens(1); - return undefined; - default: - break; - } - if (this.lexer.matchID()) { return this.lexer.value(); } + const nextToken = this.lexer.lookahead(); + const tokenReturnValues = { + '^': DATA_PARAM_KEY, + $: pathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY, + '@': undefined, + }; + if (Object.prototype.hasOwnProperty.call(tokenReturnValues, nextToken.value)) { + this.lexer.ignoreTokens(1); + return tokenReturnValues[nextToken.value]; + } } private getCurrentPathType(): PathType | undefined { @@ -573,29 +568,40 @@ export class JsonTemplateParser { }; } + private parseJSONObjectFilter(): ObjectFilterExpression { + this.lexer.expect('?'); + const filter = this.parseBaseExpr(); + return { + type: SyntaxType.OBJECT_FILTER_EXPR, + filter, + }; + } + + private parseAllFilter(): ArrayFilterExpression { + this.lexer.expect('*'); + return { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: { + type: SyntaxType.ALL_FILTER_EXPR, + }, + }; + } + private parseArrayFilterExpr(): ArrayFilterExpression | ObjectFilterExpression { - let exprType = SyntaxType.ARRAY_FILTER_EXPR; - let filter: Expression | undefined; this.lexer.expect('['); + let expr: ArrayFilterExpression | ObjectFilterExpression; if (this.lexer.match('?')) { - this.lexer.ignoreTokens(1); - exprType = SyntaxType.OBJECT_FILTER_EXPR; - filter = this.parseBaseExpr(); + expr = this.parseJSONObjectFilter(); } else if (this.lexer.match('*')) { - // this selects all the items so no filter - this.lexer.ignoreTokens(1); - filter = { - type: SyntaxType.ALL_FILTER_EXPR, - }; + expr = this.parseAllFilter(); } else { - filter = this.parseArrayFilter(); + expr = { + type: SyntaxType.ARRAY_FILTER_EXPR, + filter: this.parseArrayFilter(), + }; } this.lexer.expect(']'); - - return { - type: exprType, - filter, - }; + return expr; } private combineExpressionsAsBinaryExpr( diff --git a/src/translator.ts b/src/translator.ts index 09a7bc9..e64253d 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -39,6 +39,7 @@ import { LoopExpression, IncrementExpression, LoopControlExpression, + Literal, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; @@ -395,9 +396,9 @@ export class JsonTemplateTranslator { const valuesCode = JsonTemplateTranslator.returnObjectValues(ctx); code.push(`${dest} = ${valuesCode}.flat();`); } else if (prop) { - const propStr = escapeStr(prop); - code.push(`if(${ctx} && Object.prototype.hasOwnProperty.call(${ctx}, ${propStr})){`); - code.push(`${dest}=${ctx}[${propStr}];`); + const escapedPropName = escapeStr(prop); + code.push(`if(${ctx} && Object.prototype.hasOwnProperty.call(${ctx}, ${escapedPropName})){`); + code.push(`${dest}=${ctx}[${escapedPropName}];`); code.push('} else {'); code.push(`${dest} = undefined;`); code.push('}'); @@ -717,13 +718,11 @@ export class JsonTemplateTranslator { return code.join(''); } - private translateLiteral(type: TokenType, val: any): string { + private translateLiteral(type: TokenType, val: Literal): string { if (type === TokenType.STR) { - return escapeStr(val); - } - if (type === TokenType.REGEXP) { - return val; + return escapeStr(String(val)); } + return String(val); } @@ -738,10 +737,15 @@ export class JsonTemplateTranslator { private translateArrayFilterExpr(expr: ArrayFilterExpression, dest: string, ctx: string): string { const code: string[] = []; - if (expr.filter.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - code.push(this.translateIndexFilterExpr(expr.filter as IndexFilterExpression, dest, ctx)); - } else if (expr.filter.type === SyntaxType.RANGE_FILTER_EXPR) { - code.push(this.translateRangeFilterExpr(expr.filter as RangeFilterExpression, dest, ctx)); + switch (expr.filter.type) { + case SyntaxType.ARRAY_INDEX_FILTER_EXPR: + code.push(this.translateIndexFilterExpr(expr.filter as IndexFilterExpression, dest, ctx)); + break; + case SyntaxType.RANGE_FILTER_EXPR: + code.push(this.translateRangeFilterExpr(expr.filter as RangeFilterExpression, dest, ctx)); + break; + default: + break; } return code.join(''); } diff --git a/src/types.ts b/src/types.ts index 2592f48..4998f37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -205,8 +205,10 @@ export interface ObjectFilterExpression extends Expression { export interface ArrayFilterExpression extends Expression { filter: RangeFilterExpression | IndexFilterExpression | AllFilterExpression; } + +export type Literal = string | number | boolean | null | undefined; export interface LiteralExpression extends Expression { - value: string | number | boolean | null | undefined; + value: Literal; tokenType: TokenType; } export interface PathExpression extends Expression { diff --git a/src/utils/common.ts b/src/utils/common.ts index 4fa1b49..f72c61c 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,6 +1,6 @@ import { type Expression, type StatementsExpression, SyntaxType } from '../types'; -export function toArray(val: any): any[] | undefined { +export function toArray(val: T | T[] | undefined): T[] | undefined { if (val === undefined || val === null) { return undefined; } diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index cb94a2c..efbcd34 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -1,4 +1,4 @@ -import { Scenario } from '../../types'; +import type { Scenario } from '../../types'; export const data: Scenario[] = [ { diff --git a/test/types.ts b/test/types.ts index f8b8f04..32dc802 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,13 +1,13 @@ -import { EngineOptions, FlatMappingPaths, PathType } from '../src'; +import type { EngineOptions } from '../src'; export type Scenario = { description?: string; - input?: any; + input?: unknown; templatePath?: string; containsMappings?: true; options?: EngineOptions; - bindings?: any; - output?: any; + bindings?: Record | undefined; + output?: unknown; error?: string; }; diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts index d314699..53c1afe 100644 --- a/test/utils/scenario.ts +++ b/test/utils/scenario.ts @@ -1,5 +1,5 @@ -import { readFileSync } from 'fs'; -import { join } from 'path'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { FlatMappingPaths, JsonTemplateEngine, PathType } from '../../src'; import { Scenario } from '../types'; From f6b2fab1571b67bf17fff72cc2cd85709faae2a9 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 27 May 2024 13:51:44 +0530 Subject: [PATCH 13/29] fix: typos --- src/utils/converter.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 978f276..620dc56 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -22,17 +22,21 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object type: SyntaxType.OBJECT_EXPR, props: [] as ObjectPropExpression[], }; + const outputObject: OutputObject = {}; + for (const flatMapping of flatMappingAST) { let currentOutputObject = outputObject; let currentOutputAST = outputAST.props; let currentInputAST = flatMapping.input; + const numOutputParts = flatMapping.output.parts.length; for (let i = 0; i < numOutputParts; i++) { const outputPart = flatMapping.output.parts[i]; + if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { const key = outputPart.prop.value; - // If it's the last part, assign the value + if (i === numOutputParts - 1) { currentOutputAST.push({ type: SyntaxType.OBJECT_PROP_EXPR, @@ -42,7 +46,7 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object break; } - const nextOuptutPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; + const nextOutputPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; const items = [] as ObjectPropExpression[]; const objectExpr: ObjectExpression = { type: SyntaxType.OBJECT_EXPR, @@ -50,17 +54,18 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object }; if (!currentOutputObject[key]) { - const outputPropAST = { + const outputPropAST: ObjectPropExpression = { type: SyntaxType.OBJECT_PROP_EXPR, key, value: objectExpr, - } as ObjectPropExpression; - if (nextOuptutPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + }; + + if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { const arrayExpr: ArrayExpression = { type: SyntaxType.ARRAY_EXPR, elements: [], }; - arrayExpr.elements[nextOuptutPart.filter.indexes.elements[0].value] = objectExpr; + arrayExpr.elements[nextOutputPart.filter.indexes.elements[0].value] = objectExpr; outputPropAST.value = arrayExpr; } @@ -70,18 +75,22 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object }; currentOutputAST.push(outputPropAST); } - if (nextOuptutPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { + + if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { const filterIndex = currentInputAST.parts.findIndex( (part) => part.type === SyntaxType.ARRAY_FILTER_EXPR, ); + if (filterIndex !== -1) { const inputRemainingParts = currentInputAST.parts.splice(filterIndex + 1); currentInputAST.returnAsArray = true; const outputPropAST = currentOutputObject[key].$___ast as ObjectPropExpression; + if (outputPropAST.value.type !== SyntaxType.PATH) { currentInputAST.parts.push(outputPropAST.value); outputPropAST.value = currentInputAST; } + currentInputAST = { type: SyntaxType.PATH, pathType: currentInputAST.pathType, @@ -89,7 +98,7 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object } as PathExpression; } } - // Move to the next level + currentOutputAST = currentOutputObject[key].$___items as ObjectPropExpression[]; currentOutputObject = currentOutputObject[key] as OutputObject; } From b1cc70d0861b8b5c7f361c05487e0e8ca4394a10 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 28 May 2024 10:20:21 +0530 Subject: [PATCH 14/29] fix: parse mappings paths --- src/engine.ts | 11 +++--- src/parser.ts | 6 ++-- src/translator.ts | 14 +++----- src/types.ts | 2 +- src/utils/converter.test.ts | 10 +++--- src/utils/converter.ts | 2 +- test/scenario.test.ts | 1 + test/scenarios/mappings/data.ts | 23 ++++--------- test/scenarios/mappings/mappings.json | 48 +++++++++++++-------------- 9 files changed, 53 insertions(+), 64 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 0e6972a..23deb02 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -40,10 +40,13 @@ export class JsonTemplateEngine { return translator.translate(); } - static parseMappingPaths(mappings: FlatMappingPaths[]): FlatMappingAST[] { + static parseMappingPaths( + mappings: FlatMappingPaths[], + options?: EngineOptions, + ): FlatMappingAST[] { return mappings.map((mapping) => ({ - input: JsonTemplateEngine.parse(mapping.input).statements[0], - output: JsonTemplateEngine.parse(mapping.output).statements[0], + input: JsonTemplateEngine.parse(mapping.input, options).statements[0], + output: JsonTemplateEngine.parse(mapping.output, options).statements[0], })); } @@ -76,7 +79,7 @@ export class JsonTemplateEngine { } let templateExpr = template as Expression; if (Array.isArray(template)) { - templateExpr = convertToObjectMapping(this.parseMappingPaths(template)); + templateExpr = convertToObjectMapping(this.parseMappingPaths(template, options)); } return this.translateExpression(templateExpr); } diff --git a/src/parser.ts b/src/parser.ts index 579fc75..9cf8fd8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -138,7 +138,7 @@ export class JsonTemplateParser { if (!path.root || typeof path.root === 'object' || path.root === DATA_PARAM_KEY) { throw new JsonTemplateParserError('Invalid assignment path'); } - if (JsonTemplateParser.isRichPath(expr as PathExpression)) { + if (!JsonTemplateParser.isSimplePath(expr as PathExpression)) { throw new JsonTemplateParserError('Invalid assignment path'); } path.pathType = PathType.SIMPLE; @@ -577,10 +577,10 @@ export class JsonTemplateParser { }; } - private parseAllFilter(): ArrayFilterExpression { + private parseAllFilter(): ObjectFilterExpression { this.lexer.expect('*'); return { - type: SyntaxType.ARRAY_FILTER_EXPR, + type: SyntaxType.OBJECT_FILTER_EXPR, filter: { type: SyntaxType.ALL_FILTER_EXPR, }, diff --git a/src/translator.ts b/src/translator.ts index e64253d..28b6736 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -737,15 +737,10 @@ export class JsonTemplateTranslator { private translateArrayFilterExpr(expr: ArrayFilterExpression, dest: string, ctx: string): string { const code: string[] = []; - switch (expr.filter.type) { - case SyntaxType.ARRAY_INDEX_FILTER_EXPR: - code.push(this.translateIndexFilterExpr(expr.filter as IndexFilterExpression, dest, ctx)); - break; - case SyntaxType.RANGE_FILTER_EXPR: - code.push(this.translateRangeFilterExpr(expr.filter as RangeFilterExpression, dest, ctx)); - break; - default: - break; + if (expr.filter.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + code.push(this.translateIndexFilterExpr(expr.filter as IndexFilterExpression, dest, ctx)); + } else if (expr.filter.type === SyntaxType.RANGE_FILTER_EXPR) { + code.push(this.translateRangeFilterExpr(expr.filter as RangeFilterExpression, dest, ctx)); } return code.join(''); } @@ -757,6 +752,7 @@ export class JsonTemplateTranslator { ): string { const code: string[] = []; const condition = this.acquireVar(); + code.push(JsonTemplateTranslator.generateAssignmentCode(condition, 'true')); code.push(this.translateExpr(expr.filter, condition, ctx)); code.push(`if(!${condition}) {${dest} = undefined;}`); this.releaseVars(condition); diff --git a/src/types.ts b/src/types.ts index 4998f37..85c2acc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -274,6 +274,6 @@ export type FlatMappingPaths = { }; export type FlatMappingAST = { - input: PathExpression | FunctionCallExpression; + input: PathExpression; output: PathExpression; }; diff --git a/src/utils/converter.test.ts b/src/utils/converter.test.ts index efde4fd..d319316 100644 --- a/src/utils/converter.test.ts +++ b/src/utils/converter.test.ts @@ -123,7 +123,7 @@ describe('Converter Utils Tests', () => { prop: { type: TokenType.ID, value: 'a' }, }, { - type: SyntaxType.ARRAY_FILTER_EXPR, + type: SyntaxType.OBJECT_FILTER_EXPR, filter: { type: SyntaxType.ALL_FILTER_EXPR }, }, { @@ -182,7 +182,7 @@ describe('Converter Utils Tests', () => { prop: { type: TokenType.ID, value: 'a' }, }, { - type: SyntaxType.ARRAY_FILTER_EXPR, + type: SyntaxType.OBJECT_FILTER_EXPR, filter: { type: SyntaxType.ALL_FILTER_EXPR }, }, { @@ -260,7 +260,7 @@ describe('Converter Utils Tests', () => { prop: { type: TokenType.ID, value: 'a' }, }, { - type: SyntaxType.ARRAY_FILTER_EXPR, + type: SyntaxType.OBJECT_FILTER_EXPR, filter: { type: SyntaxType.ALL_FILTER_EXPR }, }, { @@ -368,7 +368,7 @@ describe('Converter Utils Tests', () => { }, }, { - type: SyntaxType.ARRAY_FILTER_EXPR, + type: SyntaxType.OBJECT_FILTER_EXPR, filter: { type: SyntaxType.ALL_FILTER_EXPR, }, @@ -447,7 +447,7 @@ describe('Converter Utils Tests', () => { }, }, { - type: SyntaxType.ARRAY_FILTER_EXPR, + type: SyntaxType.OBJECT_FILTER_EXPR, filter: { type: SyntaxType.ALL_FILTER_EXPR, }, diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 620dc56..362a149 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -78,7 +78,7 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { const filterIndex = currentInputAST.parts.findIndex( - (part) => part.type === SyntaxType.ARRAY_FILTER_EXPR, + (part) => part.type === SyntaxType.OBJECT_FILTER_EXPR, ); if (filterIndex !== -1) { diff --git a/test/scenario.test.ts b/test/scenario.test.ts index 1aeca0e..16babf7 100644 --- a/test/scenario.test.ts +++ b/test/scenario.test.ts @@ -34,6 +34,7 @@ describe(`${scenarioName}:`, () => { result = await ScenarioUtils.evaluateScenario(templateEngine, scenario); expect(result).toEqual(scenario.output); } catch (error: any) { + console.error(error); console.log('Actual result', JSON.stringify(result, null, 2)); console.log('Expected result', JSON.stringify(scenario.output, null, 2)); expect(error.message).toContain(scenario.error); diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index efbcd34..d3daab7 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -1,11 +1,15 @@ +import { PathType } from '../../../src'; import type { Scenario } from '../../types'; export const data: Scenario[] = [ { containsMappings: true, + options: { + defaultPathType: PathType.JSON, + }, input: { discount: 10, - event: 'purchase', + events: ['purchase', 'custom'], products: [ { id: 1, @@ -74,21 +78,6 @@ export const data: Scenario[] = [ ], value: 5.4, }, - { - discount: 10, - product_id: 2, - product_name: 'p2', - options: [ - { - l: 1, - }, - { - l: 2, - c: 'red', - }, - ], - value: 13.5, - }, { discount: 10, product_id: 3, @@ -105,7 +94,7 @@ export const data: Scenario[] = [ }, ], name: 'purchase', - revenue: 27.9, + revenue: 14.4, }, ], }, diff --git a/test/scenarios/mappings/mappings.json b/test/scenarios/mappings/mappings.json index 0625c59..32c049a 100644 --- a/test/scenarios/mappings/mappings.json +++ b/test/scenarios/mappings/mappings.json @@ -1,50 +1,50 @@ [ { - "input": "~j $.discount", - "output": "~j $.events[0].items[*].discount" + "input": "$.discount", + "output": "$.events[0].items[*].discount" }, { - "input": "~j $.products[*].id", - "output": "~j $.events[0].items[*].product_id" + "input": "$.products[?(@.category)].id", + "output": "$.events[0].items[*].product_id" }, { - "input": "~j $.event", - "output": "~j $.events[0].name" + "input": "$.events[0]", + "output": "$.events[0].name" }, { - "input": "~j $.products[*].name", - "output": "~j $.events[0].items[*].product_name" + "input": "$.products[?(@.category)].name", + "output": "$.events[0].items[*].product_name" }, { - "input": "~j $.products[*].category", - "output": "~j $.events[0].items[*].product_category" + "input": "$.products[?(@.category)].category", + "output": "$.events[0].items[*].product_category" }, { - "input": "~j $.products[*].variations[*].size", - "output": "~j $.events[0].items[*].options[*].s" + "input": "$.products[?(@.category)].variations[*].size", + "output": "$.events[0].items[*].options[*].s" }, { - "input": "~j $.products[*].(@.price * @.quantity * (1 - $.discount / 100))", - "output": "~j $.events[0].items[*].value" + "input": "$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100))", + "output": "$.events[0].items[*].value" }, { - "input": "~j $.products[*].(@.price * @.quantity * (1 - $.discount / 100)).sum()", - "output": "~j $.events[0].revenue" + "input": "$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()", + "output": "$.events[0].revenue" }, { - "input": "~j $.products[*].variations[*].length", - "output": "~j $.events[0].items[*].options[*].l" + "input": "$.products[?(@.category)].variations[*].length", + "output": "$.events[0].items[*].options[*].l" }, { - "input": "~j $.products[*].variations[*].width", - "output": "~j $.events[0].items[*].options[*].w" + "input": "$.products[?(@.category)].variations[*].width", + "output": "$.events[0].items[*].options[*].w" }, { - "input": "~j $.products[*].variations[*].color", - "output": "~j $.events[0].items[*].options[*].c" + "input": "$.products[?(@.category)].variations[*].color", + "output": "$.events[0].items[*].options[*].c" }, { - "input": "~j $.products[*].variations[*].height", - "output": "~j $.events[0].items[*].options[*].h" + "input": "$.products[?(@.category)].variations[*].height", + "output": "$.events[0].items[*].options[*].h" } ] From dab28b3f1678777b8b9d202c1f712aede2f46b34 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 28 May 2024 20:36:36 +0530 Subject: [PATCH 15/29] fix: multiple indexes in json paths --- src/engine.ts | 21 +- src/utils/converter.test.ts | 511 ------------------ src/utils/converter.ts | 78 ++- test/scenario.test.ts | 13 +- .../{mappings.json => all_features.json} | 0 test/scenarios/mappings/data.ts | 254 +++++++-- test/scenarios/mappings/filters.json | 14 + test/scenarios/mappings/index_mappings.json | 18 + .../mappings/mappings_with_root_fields.json | 18 + test/scenarios/mappings/nested_mappings.json | 34 ++ test/scenarios/mappings/transformations.json | 22 + test/scenarios/paths/data.ts | 2 +- test/scenarios/paths/json_path.jt | 2 +- 13 files changed, 361 insertions(+), 626 deletions(-) delete mode 100644 src/utils/converter.test.ts rename test/scenarios/mappings/{mappings.json => all_features.json} (100%) create mode 100644 test/scenarios/mappings/filters.json create mode 100644 test/scenarios/mappings/index_mappings.json create mode 100644 test/scenarios/mappings/mappings_with_root_fields.json create mode 100644 test/scenarios/mappings/nested_mappings.json create mode 100644 test/scenarios/mappings/transformations.json diff --git a/src/engine.ts b/src/engine.ts index 23deb02..6c0c8af 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -31,23 +31,17 @@ export class JsonTemplateEngine { ); } - private static translateTemplate(template: string, options?: EngineOptions): string { - return this.translateExpression(this.parse(template, options)); - } - private static translateExpression(expr: Expression): string { const translator = new JsonTemplateTranslator(expr); return translator.translate(); } - static parseMappingPaths( - mappings: FlatMappingPaths[], - options?: EngineOptions, - ): FlatMappingAST[] { - return mappings.map((mapping) => ({ + static parseMappingPaths(mappings: FlatMappingPaths[], options?: EngineOptions): Expression { + const flatMappingAST = mappings.map((mapping) => ({ input: JsonTemplateEngine.parse(mapping.input, options).statements[0], output: JsonTemplateEngine.parse(mapping.output, options).statements[0], })); + return convertToObjectMapping(flatMappingAST); } static create( @@ -74,12 +68,11 @@ export class JsonTemplateEngine { template: string | Expression | FlatMappingPaths[], options?: EngineOptions, ): string { - if (typeof template === 'string') { - return this.translateTemplate(template, options); - } let templateExpr = template as Expression; - if (Array.isArray(template)) { - templateExpr = convertToObjectMapping(this.parseMappingPaths(template, options)); + if (typeof template === 'string') { + templateExpr = this.parse(template, options); + } else if (Array.isArray(template)) { + templateExpr = this.parseMappingPaths(template, options); } return this.translateExpression(templateExpr); } diff --git a/src/utils/converter.test.ts b/src/utils/converter.test.ts deleted file mode 100644 index d319316..0000000 --- a/src/utils/converter.test.ts +++ /dev/null @@ -1,511 +0,0 @@ -/* eslint-disable sonarjs/no-duplicate-string */ -import { DATA_PARAM_KEY } from '../constants'; -import { PathType, SyntaxType, TokenType } from '../types'; -import { convertToObjectMapping } from './converter'; -import { JsonTemplateEngine } from '../engine'; - -describe('Converter Utils Tests', () => { - describe('convertToObjectMapping', () => { - it('should convert single simple flat mapping to object mapping', () => { - const objectMapping = convertToObjectMapping( - JsonTemplateEngine.parseMappingPaths([ - { - input: '.a.b', - output: '.foo.bar', - }, - ]), - ); - expect(objectMapping).toMatchObject({ - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'foo', - value: { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'a' }, - }, - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'b' }, - }, - ], - pathType: PathType.RICH, - }, - }, - ], - }, - }, - ], - }); - }); - it('should convert single simple flat mapping with array index to object mapping', () => { - const objectMapping = convertToObjectMapping( - JsonTemplateEngine.parseMappingPaths([ - { - input: '.a.b', - output: '.foo[0].bar', - }, - ]), - ); - expect(objectMapping).toMatchObject({ - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'foo', - value: { - type: SyntaxType.ARRAY_EXPR, - elements: [ - { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'a' }, - }, - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'b' }, - }, - ], - pathType: PathType.RICH, - }, - }, - ], - }, - ], - }, - }, - ], - }); - }); - it('should convert single flat array mapping to object mapping', () => { - const objectMapping = convertToObjectMapping( - JsonTemplateEngine.parseMappingPaths([ - { - input: '.a[*].b', - output: '.foo[*].bar', - }, - ]), - ); - expect(objectMapping).toMatchObject({ - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'foo', - value: { - type: SyntaxType.PATH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'a' }, - }, - { - type: SyntaxType.OBJECT_FILTER_EXPR, - filter: { type: SyntaxType.ALL_FILTER_EXPR }, - }, - { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'b' }, - }, - ], - }, - }, - ], - }, - ], - pathType: PathType.RICH, - returnAsArray: true, - }, - }, - ], - }); - }); - it('should convert multiple flat array mapping to object mapping', () => { - const objectMapping = convertToObjectMapping( - JsonTemplateEngine.parseMappingPaths([ - { - input: '.a[*].b', - output: '.foo[*].bar', - }, - { - input: '.a[*].c', - output: '.foo[*].car', - }, - ]), - ); - expect(objectMapping).toMatchObject({ - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'foo', - value: { - type: SyntaxType.PATH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'a' }, - }, - { - type: SyntaxType.OBJECT_FILTER_EXPR, - filter: { type: SyntaxType.ALL_FILTER_EXPR }, - }, - { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'b' }, - }, - ], - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'car', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'c' }, - }, - ], - }, - }, - ], - }, - ], - pathType: PathType.RICH, - returnAsArray: true, - }, - }, - ], - }); - }); - it('should convert multiple flat array mapping to object mapping with root level mapping', () => { - const objectMapping = convertToObjectMapping( - JsonTemplateEngine.parseMappingPaths([ - { - input: '~j $.root', - output: '.foo[*].boot', - }, - { - input: '.a[*].b', - output: '.foo[*].bar', - }, - { - input: '.a[*].c', - output: '.foo[*].car', - }, - ]), - ); - expect(objectMapping).toMatchObject({ - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'foo', - value: { - type: SyntaxType.PATH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'a' }, - }, - { - type: SyntaxType.OBJECT_FILTER_EXPR, - filter: { type: SyntaxType.ALL_FILTER_EXPR }, - }, - { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'boot', - value: { - type: SyntaxType.PATH, - root: DATA_PARAM_KEY, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'root', - }, - }, - ], - pathType: PathType.JSON, - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'b' }, - }, - ], - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'car', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { type: TokenType.ID, value: 'c' }, - }, - ], - }, - }, - ], - }, - ], - pathType: PathType.RICH, - returnAsArray: true, - }, - }, - ], - }); - }); - it('should convert multiple flat nested array mapping to object mapping with root level mapping', () => { - const objectMapping = convertToObjectMapping( - JsonTemplateEngine.parseMappingPaths([ - { - input: '~j $.root', - output: '.foo[*].boot', - }, - { - input: '.a[*].b', - output: '.foo[*].bar', - }, - { - input: '.a[*].c', - output: '.foo[*].car', - }, - { - input: '.a[*].d[*].b', - output: '.foo[*].dog[*].bar', - }, - { - input: '.a[*].d[*].c', - output: '.foo[*].dog[*].car', - }, - ]), - ); - expect(objectMapping).toMatchObject({ - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'foo', - value: { - type: SyntaxType.PATH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'a', - }, - }, - { - type: SyntaxType.OBJECT_FILTER_EXPR, - filter: { - type: SyntaxType.ALL_FILTER_EXPR, - }, - }, - { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'boot', - value: { - type: SyntaxType.PATH, - root: DATA_PARAM_KEY, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'root', - }, - }, - ], - pathType: PathType.JSON, - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'b', - }, - }, - ], - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'car', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'c', - }, - }, - ], - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'dog', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'd', - }, - }, - { - type: SyntaxType.OBJECT_FILTER_EXPR, - filter: { - type: SyntaxType.ALL_FILTER_EXPR, - }, - }, - { - type: SyntaxType.OBJECT_EXPR, - props: [ - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'bar', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'b', - }, - }, - ], - }, - }, - { - type: SyntaxType.OBJECT_PROP_EXPR, - key: 'car', - value: { - type: SyntaxType.PATH, - pathType: PathType.RICH, - parts: [ - { - type: SyntaxType.SELECTOR, - selector: '.', - prop: { - type: TokenType.ID, - value: 'c', - }, - }, - ], - }, - }, - ], - }, - ], - returnAsArray: true, - }, - }, - ], - }, - ], - pathType: PathType.RICH, - returnAsArray: true, - }, - }, - ], - }); - }); - }); -}); diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 362a149..503127b 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -7,27 +7,23 @@ import { ObjectExpression, FlatMappingAST, } from '../types'; +import { getLastElement } from './common'; -type OutputObject = { - [key: string]: { - [key: string]: OutputObject | ObjectPropExpression[]; +function CreateObjectExpression(): ObjectExpression { + return { + type: SyntaxType.OBJECT_EXPR, + props: [] as ObjectPropExpression[], }; -}; +} /** * Convert Flat to Object Mappings */ // eslint-disable-next-line sonarjs/cognitive-complexity export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): ObjectExpression { - const outputAST: ObjectExpression = { - type: SyntaxType.OBJECT_EXPR, - props: [] as ObjectPropExpression[], - }; - - const outputObject: OutputObject = {}; + const outputAST: ObjectExpression = CreateObjectExpression(); for (const flatMapping of flatMappingAST) { - let currentOutputObject = outputObject; - let currentOutputAST = outputAST.props; + let currentOutputPropsAST = outputAST.props; let currentInputAST = flatMapping.input; const numOutputParts = flatMapping.output.parts.length; @@ -38,7 +34,7 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object const key = outputPart.prop.value; if (i === numOutputParts - 1) { - currentOutputAST.push({ + currentOutputPropsAST.push({ type: SyntaxType.OBJECT_PROP_EXPR, key, value: currentInputAST, @@ -46,36 +42,20 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object break; } - const nextOutputPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; - const items = [] as ObjectPropExpression[]; - const objectExpr: ObjectExpression = { - type: SyntaxType.OBJECT_EXPR, - props: items, - }; + let currentOutputPropAST = currentOutputPropsAST.find((prop) => prop.key === key); + let objectExpr: ObjectExpression = CreateObjectExpression(); - if (!currentOutputObject[key]) { - const outputPropAST: ObjectPropExpression = { + if (!currentOutputPropAST) { + currentOutputPropAST = { type: SyntaxType.OBJECT_PROP_EXPR, key, value: objectExpr, }; - - if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - const arrayExpr: ArrayExpression = { - type: SyntaxType.ARRAY_EXPR, - elements: [], - }; - arrayExpr.elements[nextOutputPart.filter.indexes.elements[0].value] = objectExpr; - outputPropAST.value = arrayExpr; - } - - currentOutputObject[key] = { - $___items: items, - $___ast: outputPropAST, - }; - currentOutputAST.push(outputPropAST); + currentOutputPropsAST.push(currentOutputPropAST); } + objectExpr = currentOutputPropAST.value as ObjectExpression; + const nextOutputPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { const filterIndex = currentInputAST.parts.findIndex( (part) => part.type === SyntaxType.OBJECT_FILTER_EXPR, @@ -84,12 +64,12 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object if (filterIndex !== -1) { const inputRemainingParts = currentInputAST.parts.splice(filterIndex + 1); currentInputAST.returnAsArray = true; - const outputPropAST = currentOutputObject[key].$___ast as ObjectPropExpression; - if (outputPropAST.value.type !== SyntaxType.PATH) { - currentInputAST.parts.push(outputPropAST.value); - outputPropAST.value = currentInputAST; + if (currentOutputPropAST.value.type !== SyntaxType.PATH) { + currentInputAST.parts.push(currentOutputPropAST.value); + currentOutputPropAST.value = currentInputAST; } + objectExpr = getLastElement(currentOutputPropAST.value.parts) as ObjectExpression; currentInputAST = { type: SyntaxType.PATH, @@ -99,8 +79,22 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object } } - currentOutputAST = currentOutputObject[key].$___items as ObjectPropExpression[]; - currentOutputObject = currentOutputObject[key] as OutputObject; + if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + const arrayExpr: ArrayExpression = { + type: SyntaxType.ARRAY_EXPR, + elements: [], + }; + const filterIndex = nextOutputPart.filter.indexes.elements[0].value; + if (currentOutputPropAST.value.type !== SyntaxType.ARRAY_EXPR) { + arrayExpr.elements[filterIndex] = objectExpr; + currentOutputPropAST.value = arrayExpr; + } else if (!currentOutputPropAST.value.elements[filterIndex]) { + (currentOutputPropAST.value as ArrayExpression).elements[filterIndex] = + CreateObjectExpression(); + } + objectExpr = currentOutputPropAST.value.elements[filterIndex]; + } + currentOutputPropsAST = objectExpr.props; } } } diff --git a/test/scenario.test.ts b/test/scenario.test.ts index 16babf7..fbde619 100644 --- a/test/scenario.test.ts +++ b/test/scenario.test.ts @@ -12,17 +12,14 @@ command .parse(); const opts = command.opts(); -const scenarioName = opts.scenario || 'none'; +const scenarioName = opts.scenario || 'arrays'; const index = +(opts.index || 0); describe(`${scenarioName}:`, () => { - it(`Scenario ${index}`, async () => { - if (scenarioName === 'none') { - return; - } - const scenarioDir = join(__dirname, 'scenarios', scenarioName); - const scenarios = ScenarioUtils.extractScenarios(scenarioDir); - const scenario: Scenario = scenarios[index] || scenarios[0]; + const scenarioDir = join(__dirname, 'scenarios', scenarioName); + const scenarios = ScenarioUtils.extractScenarios(scenarioDir); + const scenario: Scenario = scenarios[index] || scenarios[0]; + it(`Scenario ${index}: ${Scenario.getTemplatePath(scenario)}`, async () => { let result; try { console.log( diff --git a/test/scenarios/mappings/mappings.json b/test/scenarios/mappings/all_features.json similarity index 100% rename from test/scenarios/mappings/mappings.json rename to test/scenarios/mappings/all_features.json diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index d3daab7..d125031 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -1,63 +1,65 @@ import { PathType } from '../../../src'; import type { Scenario } from '../../types'; -export const data: Scenario[] = [ - { - containsMappings: true, - options: { - defaultPathType: PathType.JSON, +const input = { + discount: 10, + events: ['purchase', 'custom'], + products: [ + { + id: 1, + name: 'p1', + category: 'baby', + price: 3, + quantity: 2, + variations: [ + { + color: 'blue', + size: 1, + }, + { + size: 2, + }, + ], }, - input: { - discount: 10, - events: ['purchase', 'custom'], - products: [ - { - id: 1, - name: 'p1', - category: 'baby', - price: 3, - quantity: 2, - variations: [ - { - color: 'blue', - size: 1, - }, - { - size: 2, - }, - ], + { + id: 2, + name: 'p2', + price: 5, + quantity: 3, + variations: [ + { + length: 1, }, { - id: 2, - name: 'p2', - price: 5, - quantity: 3, - variations: [ - { - length: 1, - }, - { - color: 'red', - length: 2, - }, - ], + color: 'red', + length: 2, }, + ], + }, + { + id: 3, + name: 'p3', + category: 'home', + price: 10, + quantity: 1, + variations: [ { - id: 3, - name: 'p3', - category: 'home', - price: 10, - quantity: 1, - variations: [ - { - width: 1, - height: 2, - length: 3, - }, - ], + width: 1, + height: 2, + length: 3, }, ], }, + ], +}; +export const data: Scenario[] = [ + { + containsMappings: true, + templatePath: 'all_features.json', + options: { + defaultPathType: PathType.JSON, + }, + input, output: { events: [ { @@ -99,4 +101,158 @@ export const data: Scenario[] = [ ], }, }, + { + containsMappings: true, + templatePath: 'filters.json', + options: { + defaultPathType: PathType.JSON, + }, + input, + output: { + items: [ + { + product_id: 1, + product_name: 'p1', + product_category: 'baby', + }, + { + product_id: 3, + product_name: 'p3', + product_category: 'home', + }, + ], + }, + }, + { + containsMappings: true, + templatePath: 'index_mappings.json', + options: { + defaultPathType: PathType.JSON, + }, + input, + output: { + events: [ + { + name: 'purchase', + type: 'identify', + }, + { + name: 'custom', + type: 'track', + }, + ], + }, + }, + + { + containsMappings: true, + templatePath: 'mappings_with_root_fields.json', + options: { + defaultPathType: PathType.JSON, + }, + input, + output: { + items: [ + { + product_id: 1, + product_name: 'p1', + product_category: 'baby', + discount: 10, + }, + { + product_id: 2, + product_name: 'p2', + discount: 10, + }, + { + product_id: 3, + product_name: 'p3', + product_category: 'home', + discount: 10, + }, + ], + }, + }, + + { + containsMappings: true, + templatePath: 'nested_mappings.json', + options: { + defaultPathType: PathType.JSON, + }, + input, + output: { + items: [ + { + product_id: 1, + product_name: 'p1', + product_category: 'baby', + options: [ + { + s: 1, + c: 'blue', + }, + { + s: 2, + }, + ], + }, + { + product_id: 2, + product_name: 'p2', + options: [ + { + l: 1, + }, + { + l: 2, + c: 'red', + }, + ], + }, + { + product_id: 3, + product_name: 'p3', + product_category: 'home', + options: [ + { + l: 3, + w: 1, + h: 2, + }, + ], + }, + ], + }, + }, + { + containsMappings: true, + templatePath: 'transformations.json', + options: { + defaultPathType: PathType.JSON, + }, + input, + output: { + items: [ + { + product_id: 1, + product_name: 'p1', + product_category: 'baby', + value: 5.4, + }, + { + product_id: 2, + product_name: 'p2', + value: 13.5, + }, + { + product_id: 3, + product_name: 'p3', + product_category: 'home', + value: 9, + }, + ], + revenue: 27.9, + }, + }, ]; diff --git a/test/scenarios/mappings/filters.json b/test/scenarios/mappings/filters.json new file mode 100644 index 0000000..34106f2 --- /dev/null +++ b/test/scenarios/mappings/filters.json @@ -0,0 +1,14 @@ +[ + { + "input": "$.products[?(@.category)].id", + "output": "$.items[*].product_id" + }, + { + "input": "$.products[?(@.category)].name", + "output": "$.items[*].product_name" + }, + { + "input": "$.products[?(@.category)].category", + "output": "$.items[*].product_category" + } +] diff --git a/test/scenarios/mappings/index_mappings.json b/test/scenarios/mappings/index_mappings.json new file mode 100644 index 0000000..09129a1 --- /dev/null +++ b/test/scenarios/mappings/index_mappings.json @@ -0,0 +1,18 @@ +[ + { + "input": "$.events[0]", + "output": "$.events[0].name" + }, + { + "input": "'identify'", + "output": "$.events[0].type" + }, + { + "input": "$.events[1]", + "output": "$.events[1].name" + }, + { + "input": "'track'", + "output": "$.events[1].type" + } +] diff --git a/test/scenarios/mappings/mappings_with_root_fields.json b/test/scenarios/mappings/mappings_with_root_fields.json new file mode 100644 index 0000000..1e80449 --- /dev/null +++ b/test/scenarios/mappings/mappings_with_root_fields.json @@ -0,0 +1,18 @@ +[ + { + "input": "$.discount", + "output": "$.items[*].discount" + }, + { + "input": "$.products[*].id", + "output": "$.items[*].product_id" + }, + { + "input": "$.products[*].name", + "output": "$.items[*].product_name" + }, + { + "input": "$.products[*].category", + "output": "$.items[*].product_category" + } +] diff --git a/test/scenarios/mappings/nested_mappings.json b/test/scenarios/mappings/nested_mappings.json new file mode 100644 index 0000000..9f4e0cc --- /dev/null +++ b/test/scenarios/mappings/nested_mappings.json @@ -0,0 +1,34 @@ +[ + { + "input": "$.products[*].id", + "output": "$.items[*].product_id" + }, + { + "input": "$.products[*].name", + "output": "$.items[*].product_name" + }, + { + "input": "$.products[*].category", + "output": "$.items[*].product_category" + }, + { + "input": "$.products[*].variations[*].size", + "output": "$.items[*].options[*].s" + }, + { + "input": "$.products[*].variations[*].length", + "output": "$.items[*].options[*].l" + }, + { + "input": "$.products[*].variations[*].width", + "output": "$.items[*].options[*].w" + }, + { + "input": "$.products[*].variations[*].color", + "output": "$.items[*].options[*].c" + }, + { + "input": "$.products[*].variations[*].height", + "output": "$.items[*].options[*].h" + } +] diff --git a/test/scenarios/mappings/transformations.json b/test/scenarios/mappings/transformations.json new file mode 100644 index 0000000..9ab2a3e --- /dev/null +++ b/test/scenarios/mappings/transformations.json @@ -0,0 +1,22 @@ +[ + { + "input": "$.products[*].id", + "output": "$.items[*].product_id" + }, + { + "input": "$.products[*].name", + "output": "$.items[*].product_name" + }, + { + "input": "$.products[*].category", + "output": "$.items[*].product_category" + }, + { + "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100))", + "output": "$.items[*].value" + }, + { + "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100)).sum()", + "output": "$.revenue" + } +] diff --git a/test/scenarios/paths/data.ts b/test/scenarios/paths/data.ts index cf7b38f..7e23e30 100644 --- a/test/scenarios/paths/data.ts +++ b/test/scenarios/paths/data.ts @@ -73,7 +73,7 @@ export const data: Scenario[] = [ templatePath: 'json_path.jt', input: { foo: 'bar', - min: 1, + size: 1, items: [ { a: 1, diff --git a/test/scenarios/paths/json_path.jt b/test/scenarios/paths/json_path.jt index cddee15..5ba54c7 100644 --- a/test/scenarios/paths/json_path.jt +++ b/test/scenarios/paths/json_path.jt @@ -1,6 +1,6 @@ [ ~j $.foo, ~j $.items[*], - ~j $.items[?(@.a>$.min)], + ~j $.items[?(@.a>$.size)], ~j $.items.(@.a + @.b) ] \ No newline at end of file From 7bc07333be1a09ce217d65cfea71a504915f3662 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 28 May 2024 20:49:06 +0530 Subject: [PATCH 16/29] fix: unused import --- src/engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine.ts b/src/engine.ts index 6c0c8af..dc8405a 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -2,7 +2,7 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; import { JsonTemplateTranslator } from './translator'; -import { EngineOptions, Expression, FlatMappingAST, FlatMappingPaths } from './types'; +import { EngineOptions, Expression, FlatMappingPaths } from './types'; import { CreateAsyncFunction, convertToObjectMapping } from './utils'; export class JsonTemplateEngine { From 2ff4c0a30197872956d9378def9702c0e327aaea Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 28 May 2024 22:02:10 +0530 Subject: [PATCH 17/29] refactor: convertToObjectMapping --- src/utils/converter.ts | 168 ++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 70 deletions(-) diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 503127b..c495099 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import { SyntaxType, PathExpression, @@ -6,6 +7,9 @@ import { ArrayExpression, ObjectExpression, FlatMappingAST, + Expression, + IndexFilterExpression, + AllFilterExpression, } from '../types'; import { getLastElement } from './common'; @@ -15,89 +19,113 @@ function CreateObjectExpression(): ObjectExpression { props: [] as ObjectPropExpression[], }; } -/** - * Convert Flat to Object Mappings - */ -// eslint-disable-next-line sonarjs/cognitive-complexity -export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): ObjectExpression { - const outputAST: ObjectExpression = CreateObjectExpression(); - - for (const flatMapping of flatMappingAST) { - let currentOutputPropsAST = outputAST.props; - let currentInputAST = flatMapping.input; - const numOutputParts = flatMapping.output.parts.length; - for (let i = 0; i < numOutputParts; i++) { - const outputPart = flatMapping.output.parts[i]; +function findOrCreateObjectPropExpression( + props: ObjectPropExpression[], + key: string, +): ObjectPropExpression { + let match = props.find((prop) => prop.key === key); + if (!match) { + match = { + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: CreateObjectExpression(), + }; + props.push(match); + } + return match; +} - if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { - const key = outputPart.prop.value; +function processArrayIndexFilter( + currrentOutputPropAST: ObjectPropExpression, + filter: IndexFilterExpression, +): ObjectExpression { + const filterIndex = filter.indexes.elements[0].value; + if (currrentOutputPropAST.value.type !== SyntaxType.ARRAY_EXPR) { + const elements: Expression[] = []; + elements[filterIndex] = currrentOutputPropAST.value; + currrentOutputPropAST.value = { + type: SyntaxType.ARRAY_EXPR, + elements, + }; + } else if (!currrentOutputPropAST.value.elements[filterIndex]) { + (currrentOutputPropAST.value as ArrayExpression).elements[filterIndex] = + CreateObjectExpression(); + } + return currrentOutputPropAST.value.elements[filterIndex]; +} - if (i === numOutputParts - 1) { - currentOutputPropsAST.push({ - type: SyntaxType.OBJECT_PROP_EXPR, - key, - value: currentInputAST, - } as ObjectPropExpression); - break; - } +function processAllFilter( + currentInputAST: PathExpression, + currentOutputPropAST: ObjectPropExpression, +): ObjectExpression { + const filterIndex = currentInputAST.parts.findIndex( + (part) => part.type === SyntaxType.OBJECT_FILTER_EXPR, + ); - let currentOutputPropAST = currentOutputPropsAST.find((prop) => prop.key === key); - let objectExpr: ObjectExpression = CreateObjectExpression(); + if (filterIndex === -1) { + return currentOutputPropAST.value as ObjectExpression; + } + const matchedInputParts = currentInputAST.parts.splice(0, filterIndex + 1); + if (currentOutputPropAST.value.type !== SyntaxType.PATH) { + matchedInputParts.push(currentOutputPropAST.value); + currentOutputPropAST.value = { + type: SyntaxType.PATH, + root: currentInputAST.root, + pathType: currentInputAST.pathType, + parts: matchedInputParts, + returnAsArray: true, + } as PathExpression; + } + currentInputAST.root = undefined; - if (!currentOutputPropAST) { - currentOutputPropAST = { - type: SyntaxType.OBJECT_PROP_EXPR, - key, - value: objectExpr, - }; - currentOutputPropsAST.push(currentOutputPropAST); - } - objectExpr = currentOutputPropAST.value as ObjectExpression; + return getLastElement(currentOutputPropAST.value.parts) as ObjectExpression; +} - const nextOutputPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; - if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { - const filterIndex = currentInputAST.parts.findIndex( - (part) => part.type === SyntaxType.OBJECT_FILTER_EXPR, - ); +function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpression) { + let currentOutputPropsAST = outputAST.props; + const currentInputAST = flatMapping.input; - if (filterIndex !== -1) { - const inputRemainingParts = currentInputAST.parts.splice(filterIndex + 1); - currentInputAST.returnAsArray = true; + const numOutputParts = flatMapping.output.parts.length; + for (let i = 0; i < numOutputParts; i++) { + const outputPart = flatMapping.output.parts[i]; - if (currentOutputPropAST.value.type !== SyntaxType.PATH) { - currentInputAST.parts.push(currentOutputPropAST.value); - currentOutputPropAST.value = currentInputAST; - } - objectExpr = getLastElement(currentOutputPropAST.value.parts) as ObjectExpression; + if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { + const key = outputPart.prop.value; - currentInputAST = { - type: SyntaxType.PATH, - pathType: currentInputAST.pathType, - parts: inputRemainingParts, - } as PathExpression; - } - } + if (i === numOutputParts - 1) { + currentOutputPropsAST.push({ + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: currentInputAST, + } as ObjectPropExpression); + break; + } - if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - const arrayExpr: ArrayExpression = { - type: SyntaxType.ARRAY_EXPR, - elements: [], - }; - const filterIndex = nextOutputPart.filter.indexes.elements[0].value; - if (currentOutputPropAST.value.type !== SyntaxType.ARRAY_EXPR) { - arrayExpr.elements[filterIndex] = objectExpr; - currentOutputPropAST.value = arrayExpr; - } else if (!currentOutputPropAST.value.elements[filterIndex]) { - (currentOutputPropAST.value as ArrayExpression).elements[filterIndex] = - CreateObjectExpression(); - } - objectExpr = currentOutputPropAST.value.elements[filterIndex]; - } - currentOutputPropsAST = objectExpr.props; + const currentOutputPropAST = findOrCreateObjectPropExpression(currentOutputPropsAST, key); + let objectExpr: ObjectExpression = currentOutputPropAST.value as ObjectExpression; + const nextOutputPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; + if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { + objectExpr = processAllFilter(currentInputAST, currentOutputPropAST); + } else if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + objectExpr = processArrayIndexFilter( + currentOutputPropAST, + nextOutputPart.filter as IndexFilterExpression, + ); } + currentOutputPropsAST = objectExpr.props; } } +} +/** + * Convert Flat to Object Mappings + */ +export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): ObjectExpression { + const outputAST: ObjectExpression = CreateObjectExpression(); + + for (const flatMapping of flatMappingAST) { + processFlatMapping(flatMapping, outputAST); + } return outputAST; } From aeb5912ce983aa98db7e671f80e4839a79f9aeed Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 28 May 2024 22:54:37 +0530 Subject: [PATCH 18/29] fix: imports --- src/utils/converter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/converter.ts b/src/utils/converter.ts index c495099..4290182 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -9,7 +9,6 @@ import { FlatMappingAST, Expression, IndexFilterExpression, - AllFilterExpression, } from '../types'; import { getLastElement } from './common'; From e7b37faff40e26d29ec28bc89eff817b4f23b4ed Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Wed, 29 May 2024 17:40:14 +0530 Subject: [PATCH 19/29] refactor: comparisons tests --- src/operators.ts | 23 +- src/parser.ts | 9 +- .../bad_templates/object_with_invalid_key.jt | 2 +- test/scenarios/comparisons/anyof.jt | 4 + test/scenarios/comparisons/contains.jt | 4 + test/scenarios/comparisons/data.ts | 211 ++++++++++++++---- test/scenarios/comparisons/empty.jt | 4 + test/scenarios/comparisons/ends_with.jt | 4 + .../comparisons/ends_with_ignore_case.jt | 4 + test/scenarios/comparisons/eq.jt | 4 + test/scenarios/comparisons/ge.jt | 4 + test/scenarios/comparisons/gte.jt | 4 + test/scenarios/comparisons/in.jt | 4 + test/scenarios/comparisons/le.jt | 4 + test/scenarios/comparisons/lte.jt | 4 + test/scenarios/comparisons/ne.jt | 4 + test/scenarios/comparisons/noneof.jt | 4 + test/scenarios/comparisons/not_in.jt | 4 + test/scenarios/comparisons/regex.jt | 4 + test/scenarios/comparisons/size.jt | 4 + test/scenarios/comparisons/starts_with.jt | 4 + .../comparisons/starts_with_ignore_case.jt | 4 + .../string_contains_ignore_case.jt | 4 + test/scenarios/comparisons/string_eq.jt | 4 + .../comparisons/string_eq_ingore_case.jt | 4 + test/scenarios/comparisons/string_ne.jt | 4 + .../comparisons/string_ne_ingore_case.jt | 4 + test/scenarios/comparisons/subsetof.jt | 4 + test/scenarios/comparisons/template.jt | 38 ---- 29 files changed, 292 insertions(+), 87 deletions(-) create mode 100644 test/scenarios/comparisons/anyof.jt create mode 100644 test/scenarios/comparisons/contains.jt create mode 100644 test/scenarios/comparisons/empty.jt create mode 100644 test/scenarios/comparisons/ends_with.jt create mode 100644 test/scenarios/comparisons/ends_with_ignore_case.jt create mode 100644 test/scenarios/comparisons/eq.jt create mode 100644 test/scenarios/comparisons/ge.jt create mode 100644 test/scenarios/comparisons/gte.jt create mode 100644 test/scenarios/comparisons/in.jt create mode 100644 test/scenarios/comparisons/le.jt create mode 100644 test/scenarios/comparisons/lte.jt create mode 100644 test/scenarios/comparisons/ne.jt create mode 100644 test/scenarios/comparisons/noneof.jt create mode 100644 test/scenarios/comparisons/not_in.jt create mode 100644 test/scenarios/comparisons/regex.jt create mode 100644 test/scenarios/comparisons/size.jt create mode 100644 test/scenarios/comparisons/starts_with.jt create mode 100644 test/scenarios/comparisons/starts_with_ignore_case.jt create mode 100644 test/scenarios/comparisons/string_contains_ignore_case.jt create mode 100644 test/scenarios/comparisons/string_eq.jt create mode 100644 test/scenarios/comparisons/string_eq_ingore_case.jt create mode 100644 test/scenarios/comparisons/string_ne.jt create mode 100644 test/scenarios/comparisons/string_ne_ingore_case.jt create mode 100644 test/scenarios/comparisons/subsetof.jt delete mode 100644 test/scenarios/comparisons/template.jt diff --git a/src/operators.ts b/src/operators.ts index 515ecb3..31fde54 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -28,9 +28,11 @@ function containsStrict(val1, val2): string { 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()))`); + code.push(`(typeof ${val1} === 'string' && typeof ${val2} === 'string') ?`); + code.push(`(${val1}.toLowerCase().includes(${val2}.toLowerCase()))`); + code.push(':'); + code.push(`(Array.isArray(${val1}) && (${val1}.includes(${val2})`); + code.push(`|| (typeof ${val2} === 'string' && ${val1}.includes(${val2}.toLowerCase()))))`); return code.join(''); } @@ -56,7 +58,14 @@ export const binaryOperators = { '!==': (val1, val2): string => `${val1}!==${val2}`, - '!=': (val1, val2): string => `${val1}!=${val2}`, + '!=': (val1, val2): string => { + const code: string[] = []; + code.push(`(typeof ${val1} == 'string' && typeof ${val2} == 'string') ?`); + code.push(`(${val1}.toLowerCase() != ${val2}.toLowerCase())`); + code.push(':'); + code.push(`(${val1} != ${val2})`); + return code.join(''); + }, '^==': startsWithStrict, @@ -77,11 +86,11 @@ export const binaryOperators = { '=~': (val1, val2): string => `(${val2} instanceof RegExp) ? (${val2}.test(${val1})) : (${val1}==${val2})`, - contains: containsStrict, + contains, - '==*': (val1, val2): string => containsStrict(val2, val1), + '==*': (val1, val2): string => containsStrict(val1, val2), - '=*': (val1, val2): string => contains(val2, val1), + '=*': (val1, val2): string => contains(val1, val2), size: (val1, val2): string => `${val1}.length === ${val2}`, diff --git a/src/parser.ts b/src/parser.ts index 9cf8fd8..0107d1f 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1059,9 +1059,9 @@ export class JsonTemplateParser { this.lexer.ignoreTokens(1); key = this.parseBaseExpr(); this.lexer.expect(']'); - } else if (this.lexer.matchID()) { + } else if (this.lexer.matchID() || this.lexer.matchKeyword()) { key = this.lexer.value(); - } else if (this.lexer.matchTokenType(TokenType.STR)) { + } else if (this.lexer.matchLiteral() && !this.lexer.matchTokenType(TokenType.REGEXP)) { key = this.parseLiteralExpr(); } else { this.lexer.throwUnexpectedToken(); @@ -1070,7 +1070,10 @@ export class JsonTemplateParser { } private parseShortKeyValueObjectPropExpr(): ObjectPropExpression | undefined { - if (this.lexer.matchID() && (this.lexer.match(',', 1) || this.lexer.match('}', 1))) { + if ( + (this.lexer.matchID() || this.lexer.matchKeyword()) && + (this.lexer.match(',', 1) || this.lexer.match('}', 1)) + ) { const key = this.lexer.lookahead().value; const value = this.parseBaseExpr(); return { diff --git a/test/scenarios/bad_templates/object_with_invalid_key.jt b/test/scenarios/bad_templates/object_with_invalid_key.jt index 78ba118..839c5db 100644 --- a/test/scenarios/bad_templates/object_with_invalid_key.jt +++ b/test/scenarios/bad_templates/object_with_invalid_key.jt @@ -1 +1 @@ -{1: 2} \ No newline at end of file +{/1/: 2} \ No newline at end of file diff --git a/test/scenarios/comparisons/anyof.jt b/test/scenarios/comparisons/anyof.jt new file mode 100644 index 0000000..7b8f859 --- /dev/null +++ b/test/scenarios/comparisons/anyof.jt @@ -0,0 +1,4 @@ +{ + true: [1, 2] anyof [2, 3], + false: [1, 2] anyof [3, 4] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/contains.jt b/test/scenarios/comparisons/contains.jt new file mode 100644 index 0000000..d9fedea --- /dev/null +++ b/test/scenarios/comparisons/contains.jt @@ -0,0 +1,4 @@ +{ + true: ["aBc" ==* "aB", "abc" contains "c", ["a", "b", "c"] contains "c"], + false: ["aBc" ==* "ab", "abc" contains "d", ["a", "b", "c"] contains "d"] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/data.ts b/test/scenarios/comparisons/data.ts index 76cd34a..8d98bbe 100644 --- a/test/scenarios/comparisons/data.ts +++ b/test/scenarios/comparisons/data.ts @@ -2,43 +2,178 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ { - output: [ - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], + templatePath: 'anyof.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'contains.jt', + output: { + true: [true, true, true], + false: [false, false, false], + }, + }, + { + templatePath: 'empty.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'string_contains_ignore_case.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'ends_with.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'ends_with_ignore_case.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'eq.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'ge.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'gte.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'in.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'le.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'lte.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'ne.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'noneof.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'not_in.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'regex.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'size.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'starts_with.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'starts_with_ignore_case.jt', + output: { + true: [true, true], + false: [false, false], + }, + }, + { + templatePath: 'string_eq.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'string_ne.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'string_eq_ingore_case.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'string_ne.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'string_ne_ingore_case.jt', + output: { + true: true, + false: false, + }, + }, + { + templatePath: 'subsetof.jt', + output: { + true: [true, true], + false: [false, false], + }, }, ]; diff --git a/test/scenarios/comparisons/empty.jt b/test/scenarios/comparisons/empty.jt new file mode 100644 index 0000000..d81d4c9 --- /dev/null +++ b/test/scenarios/comparisons/empty.jt @@ -0,0 +1,4 @@ +{ + true: ["" empty true, [] empty true], + false: ["a" empty true, ["a"] empty true] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/ends_with.jt b/test/scenarios/comparisons/ends_with.jt new file mode 100644 index 0000000..bc63d4e --- /dev/null +++ b/test/scenarios/comparisons/ends_with.jt @@ -0,0 +1,4 @@ +{ + true:["EndsWith" $== "With", "With" ==$ "EndsWith"], + false: ["EndsWith" $== "NotWith", "NotWith" ==$ "EndsWith"] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/ends_with_ignore_case.jt b/test/scenarios/comparisons/ends_with_ignore_case.jt new file mode 100644 index 0000000..fa66028 --- /dev/null +++ b/test/scenarios/comparisons/ends_with_ignore_case.jt @@ -0,0 +1,4 @@ +{ + true:["EndsWith" $= "with", "with" =$ "EndsWith"], + false: ["EndsWith" $= "NotWith", "NotWith" =$ "EndsWith"] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/eq.jt b/test/scenarios/comparisons/eq.jt new file mode 100644 index 0000000..1defeac --- /dev/null +++ b/test/scenarios/comparisons/eq.jt @@ -0,0 +1,4 @@ +{ + true: 10 == 10, + false: 10 == 2 +} \ No newline at end of file diff --git a/test/scenarios/comparisons/ge.jt b/test/scenarios/comparisons/ge.jt new file mode 100644 index 0000000..f62a813 --- /dev/null +++ b/test/scenarios/comparisons/ge.jt @@ -0,0 +1,4 @@ +{ + true: 10 > 2, + false: 2 > 10 +} \ No newline at end of file diff --git a/test/scenarios/comparisons/gte.jt b/test/scenarios/comparisons/gte.jt new file mode 100644 index 0000000..7e1885c --- /dev/null +++ b/test/scenarios/comparisons/gte.jt @@ -0,0 +1,4 @@ +{ + true: 10 >= 10, + false: 2 >= 10 +} \ No newline at end of file diff --git a/test/scenarios/comparisons/in.jt b/test/scenarios/comparisons/in.jt new file mode 100644 index 0000000..3b6a81d --- /dev/null +++ b/test/scenarios/comparisons/in.jt @@ -0,0 +1,4 @@ +{ + true: ["a" in ["a", "b"], "a" in {"a": 1, "b": 2}], + false: ["c" in ["a", "b"], "c" in {"a": 1, "b": 2}] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/le.jt b/test/scenarios/comparisons/le.jt new file mode 100644 index 0000000..b362697 --- /dev/null +++ b/test/scenarios/comparisons/le.jt @@ -0,0 +1,4 @@ +{ + true: 2 < 10, + false: 10 < 2 +} \ No newline at end of file diff --git a/test/scenarios/comparisons/lte.jt b/test/scenarios/comparisons/lte.jt new file mode 100644 index 0000000..a1a9c2f --- /dev/null +++ b/test/scenarios/comparisons/lte.jt @@ -0,0 +1,4 @@ +{ + true: 10 <= 10, + false: 10 <= 2 +} \ No newline at end of file diff --git a/test/scenarios/comparisons/ne.jt b/test/scenarios/comparisons/ne.jt new file mode 100644 index 0000000..a32bc59 --- /dev/null +++ b/test/scenarios/comparisons/ne.jt @@ -0,0 +1,4 @@ +{ + true: 10 != 2, + false: 10 != 10 +} \ No newline at end of file diff --git a/test/scenarios/comparisons/noneof.jt b/test/scenarios/comparisons/noneof.jt new file mode 100644 index 0000000..05a0103 --- /dev/null +++ b/test/scenarios/comparisons/noneof.jt @@ -0,0 +1,4 @@ +{ + true: [1, 2] noneof [3, 4], + false: [1, 2] noneof [2, 3], +} \ No newline at end of file diff --git a/test/scenarios/comparisons/not_in.jt b/test/scenarios/comparisons/not_in.jt new file mode 100644 index 0000000..aa87f2c --- /dev/null +++ b/test/scenarios/comparisons/not_in.jt @@ -0,0 +1,4 @@ +{ + true: ["c" nin ["a", "b"], "c" nin {"a": 1, "b": 2}], + false: ["a" nin ["a", "b"], "a" nin {"a": 1, "b": 2}] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/regex.jt b/test/scenarios/comparisons/regex.jt new file mode 100644 index 0000000..7d51c78 --- /dev/null +++ b/test/scenarios/comparisons/regex.jt @@ -0,0 +1,4 @@ +{ + true: ['abc' =~ /a.*c/, 'aBC' =~ /a.*c/i], + false: ['abC' =~ /a.*c/, 'aBd' =~ /a.*c/i] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/size.jt b/test/scenarios/comparisons/size.jt new file mode 100644 index 0000000..c73f863 --- /dev/null +++ b/test/scenarios/comparisons/size.jt @@ -0,0 +1,4 @@ +{ + true: [["a", "b"] size 2, "ab" size 2], + false: [[] size 1, "" size 1] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/starts_with.jt b/test/scenarios/comparisons/starts_with.jt new file mode 100644 index 0000000..345df80 --- /dev/null +++ b/test/scenarios/comparisons/starts_with.jt @@ -0,0 +1,4 @@ +{ + true:["StartsWith" ^== "Starts", "Starts" ==^ "StartsWith"], + false: ["StartsWith" ^= "NotStarts", "NotStarts" =^ "StartsWith"] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/starts_with_ignore_case.jt b/test/scenarios/comparisons/starts_with_ignore_case.jt new file mode 100644 index 0000000..3c7f7d4 --- /dev/null +++ b/test/scenarios/comparisons/starts_with_ignore_case.jt @@ -0,0 +1,4 @@ +{ + true:["StartsWith" ^= "starts", "starts" =^ "StartsWith"], + false: ["StartsWith" ^= "NotStarts", "NotStarts" =^ "StartsWith"] +} \ No newline at end of file diff --git a/test/scenarios/comparisons/string_contains_ignore_case.jt b/test/scenarios/comparisons/string_contains_ignore_case.jt new file mode 100644 index 0000000..c364247 --- /dev/null +++ b/test/scenarios/comparisons/string_contains_ignore_case.jt @@ -0,0 +1,4 @@ +{ + true: "aBc" =* "aB", + false: "ac" =* "aB" +} \ No newline at end of file diff --git a/test/scenarios/comparisons/string_eq.jt b/test/scenarios/comparisons/string_eq.jt new file mode 100644 index 0000000..006861a --- /dev/null +++ b/test/scenarios/comparisons/string_eq.jt @@ -0,0 +1,4 @@ +{ + true: "aBc" === "aBc", + false: "abc" === "aBc" +} \ No newline at end of file diff --git a/test/scenarios/comparisons/string_eq_ingore_case.jt b/test/scenarios/comparisons/string_eq_ingore_case.jt new file mode 100644 index 0000000..fb6e5b7 --- /dev/null +++ b/test/scenarios/comparisons/string_eq_ingore_case.jt @@ -0,0 +1,4 @@ +{ + true: "abc" == "aBc", + false: "adc" == "aBc" +} \ No newline at end of file diff --git a/test/scenarios/comparisons/string_ne.jt b/test/scenarios/comparisons/string_ne.jt new file mode 100644 index 0000000..773c1d8 --- /dev/null +++ b/test/scenarios/comparisons/string_ne.jt @@ -0,0 +1,4 @@ +{ + true: "abc" !== "aBc", + false: "aBc" !== "aBc" +} \ No newline at end of file diff --git a/test/scenarios/comparisons/string_ne_ingore_case.jt b/test/scenarios/comparisons/string_ne_ingore_case.jt new file mode 100644 index 0000000..8195fd4 --- /dev/null +++ b/test/scenarios/comparisons/string_ne_ingore_case.jt @@ -0,0 +1,4 @@ +{ + true: "adc" != "aBc", + false: "abc" != "aBc" +} \ No newline at end of file diff --git a/test/scenarios/comparisons/subsetof.jt b/test/scenarios/comparisons/subsetof.jt new file mode 100644 index 0000000..9a96331 --- /dev/null +++ b/test/scenarios/comparisons/subsetof.jt @@ -0,0 +1,4 @@ +{ + true: [[1, 2] subsetof [1, 2, 3], [] subsetof [1]], + false: [[1, 2] subsetof [1], [1] subsetof []], +} \ No newline at end of file diff --git a/test/scenarios/comparisons/template.jt b/test/scenarios/comparisons/template.jt deleted file mode 100644 index efb5141..0000000 --- a/test/scenarios/comparisons/template.jt +++ /dev/null @@ -1,38 +0,0 @@ -[ -10>2, -2<10, -10>=2, -2<=10, -10 != 2, -'IgnoreCase' == 'ignorecase', -'CompareWithCase' !== 'comparewithCase', -'CompareWithCase' === 'CompareWithCase', -'i' =* 'I contain', -'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}, -"a" nin ["b"], -"a" nin {b: 1}, -["a", "b"] contains "a", -"abc" contains "a", -["a", "b"] size 2, -"abc" size 3, -[] empty true, -["a"] empty false, -"" empty true, -"abc" empty false, -["c", "a"] subsetof ["a", "b", "c"], -[] subsetof ["a", "b", "c"], -"abc" =~ /a.*c/, -"AdC" =~ /a.*c/i, -["a", "b"] anyof ["a", "c"], -["c", "d"] noneof ["a", "b"] -] \ No newline at end of file From a9e591a2c9926e64f9347e11be641b1954e1d33f Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Thu, 30 May 2024 06:53:05 +0530 Subject: [PATCH 20/29] feat: add reverse translator --- src/engine.ts | 20 +- src/lexer.ts | 2 +- src/parser.ts | 83 +++- src/reverse_translator.ts | 469 ++++++++++++++++++ src/translator.ts | 16 +- src/types.ts | 10 +- src/utils/common.ts | 9 +- src/utils/converter.ts | 8 +- src/utils/transalator.ts | 10 + test/scenarios/context_variables/filter.jt | 3 +- test/scenarios/filters/object_filters.jt | 1 + test/scenarios/filters/object_indexes.jt | 2 +- test/scenarios/objects/template.jt | 4 +- test/scenarios/return/data.ts | 10 + test/scenarios/return/return_no_value.jt | 4 + .../return/{template.jt => return_value.jt} | 0 test/utils/scenario.ts | 6 +- 17 files changed, 604 insertions(+), 53 deletions(-) create mode 100644 src/reverse_translator.ts create mode 100644 src/utils/transalator.ts create mode 100644 test/scenarios/return/return_no_value.jt rename test/scenarios/return/{template.jt => return_value.jt} (100%) diff --git a/src/engine.ts b/src/engine.ts index dc8405a..2036c5f 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,6 +1,7 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; +import { JsonTemplateReverseTranslator } from './reverse_translator'; import { JsonTemplateTranslator } from './translator'; import { EngineOptions, Expression, FlatMappingPaths } from './types'; import { CreateAsyncFunction, convertToObjectMapping } from './utils'; @@ -36,7 +37,10 @@ export class JsonTemplateEngine { return translator.translate(); } - static parseMappingPaths(mappings: FlatMappingPaths[], options?: EngineOptions): Expression { + private static parseMappingPaths( + mappings: FlatMappingPaths[], + options?: EngineOptions, + ): Expression { const flatMappingAST = mappings.map((mapping) => ({ input: JsonTemplateEngine.parse(mapping.input, options).statements[0], output: JsonTemplateEngine.parse(mapping.output, options).statements[0], @@ -58,7 +62,10 @@ export class JsonTemplateEngine { return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options)); } - static parse(template: string, options?: EngineOptions): Expression { + static parse(template: string | FlatMappingPaths[], options?: EngineOptions): Expression { + if (Array.isArray(template)) { + return this.parseMappingPaths(template, options); + } const lexer = new JsonTemplateLexer(template); const parser = new JsonTemplateParser(lexer, options); return parser.parse(); @@ -69,14 +76,17 @@ export class JsonTemplateEngine { options?: EngineOptions, ): string { let templateExpr = template as Expression; - if (typeof template === 'string') { + if (typeof template === 'string' || Array.isArray(template)) { templateExpr = this.parse(template, options); - } else if (Array.isArray(template)) { - templateExpr = this.parseMappingPaths(template, options); } return this.translateExpression(templateExpr); } + static reverseTranslate(expr: Expression, options?: EngineOptions): string { + const translator = new JsonTemplateReverseTranslator(options); + return translator.translate(expr); + } + evaluate(data: unknown, bindings: Record = {}): unknown { return this.fn(data ?? {}, bindings); } diff --git a/src/lexer.ts b/src/lexer.ts index b5177d8..dcdc7e0 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -90,7 +90,7 @@ export class JsonTemplateLexer { } matchPath(): boolean { - return this.matchPathType() || this.matchPathSelector() || this.matchID(); + return this.matchPathSelector() || this.matchID(); } matchSpread(): boolean { diff --git a/src/parser.ts b/src/parser.ts index 0107d1f..965d16d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -38,14 +38,24 @@ import { TokenType, UnaryExpression, } from './types'; -import { convertToStatementsExpr, getLastElement, toArray } from './utils/common'; +import { + convertToStatementsExpr, + createBlockExpression, + getLastElement, + toArray, +} from './utils/common'; + +type PathTypeResult = { + pathType: PathType; + inferredPathType: PathType; +}; export class JsonTemplateParser { private lexer: JsonTemplateLexer; private options?: EngineOptions; - private pathTypesStack: PathType[] = []; + private pathTypesStack: PathTypeResult[] = []; // indicates currently how many loops being parsed private loopCount = 0; @@ -141,7 +151,7 @@ export class JsonTemplateParser { if (!JsonTemplateParser.isSimplePath(expr as PathExpression)) { throw new JsonTemplateParserError('Invalid assignment path'); } - path.pathType = PathType.SIMPLE; + path.inferredPathType = PathType.SIMPLE; return { type: SyntaxType.ASSIGNMENT_EXPR, value: this.parseBaseExpr(), @@ -266,7 +276,10 @@ export class JsonTemplateParser { }; } - private parsePathRoot(pathType: PathType, root?: Expression): Expression | string | undefined { + private parsePathRoot( + pathType: PathTypeResult, + root?: Expression, + ): Expression | string | undefined { if (root) { return root; } @@ -276,7 +289,7 @@ export class JsonTemplateParser { const nextToken = this.lexer.lookahead(); const tokenReturnValues = { '^': DATA_PARAM_KEY, - $: pathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY, + $: pathType.inferredPathType === PathType.JSON ? DATA_PARAM_KEY : BINDINGS_PARAM_KEY, '@': undefined, }; if (Object.prototype.hasOwnProperty.call(tokenReturnValues, nextToken.value)) { @@ -285,44 +298,60 @@ export class JsonTemplateParser { } } - private getCurrentPathType(): PathType | undefined { + private getInferredPathType(): PathTypeResult { if (this.pathTypesStack.length > 0) { return this.pathTypesStack[this.pathTypesStack.length - 1]; } - return undefined; + return { + pathType: PathType.UNKNOWN, + inferredPathType: this.options?.defaultPathType || PathType.RICH, + }; + } + + private createPathResult(pathType: PathType) { + return { + pathType, + inferredPathType: pathType, + }; } - private parsePathType(): PathType { + private parsePathType(): PathTypeResult { if (this.lexer.matchSimplePath()) { this.lexer.ignoreTokens(1); - return PathType.SIMPLE; + return this.createPathResult(PathType.SIMPLE); } if (this.lexer.matchRichPath()) { this.lexer.ignoreTokens(1); - return PathType.RICH; + return this.createPathResult(PathType.RICH); } if (this.lexer.matchJsonPath()) { this.lexer.ignoreTokens(1); - return PathType.JSON; + return this.createPathResult(PathType.JSON); } - return this.getCurrentPathType() ?? this.options?.defaultPathType ?? PathType.RICH; + return this.getInferredPathType(); + } + + private parsePathTypeExpr(): Expression { + const pathTypeResult = this.parsePathType(); + this.pathTypesStack.push(pathTypeResult); + const expr = this.parseBaseExpr(); + this.pathTypesStack.pop(); + return expr; } private parsePath(options?: { root?: Expression }): PathExpression | Expression { - const pathType = this.parsePathType(); - this.pathTypesStack.push(pathType); + const pathTypeResult = this.parsePathType(); const expr: PathExpression = { type: SyntaxType.PATH, - root: this.parsePathRoot(pathType, options?.root), + root: this.parsePathRoot(pathTypeResult, options?.root), parts: this.parsePathParts(), - pathType, + ...pathTypeResult, }; if (!expr.parts.length) { - expr.pathType = PathType.SIMPLE; + expr.inferredPathType = PathType.SIMPLE; return expr; } - this.pathTypesStack.pop(); return JsonTemplateParser.updatePathExpr(expr); } @@ -433,7 +462,7 @@ export class JsonTemplateParser { private parseObjectFilter(): IndexFilterExpression | ObjectFilterExpression { let exclude = false; - if (this.lexer.match('~')) { + if (this.lexer.match('~') || this.lexer.match('!')) { this.lexer.ignoreTokens(1); exclude = true; } @@ -741,7 +770,7 @@ export class JsonTemplateParser { return { type: SyntaxType.UNARY_EXPR, op: '!', - arg: this.parseInExpr(expr), + arg: createBlockExpression(this.parseInExpr(expr)), }; } @@ -750,11 +779,11 @@ export class JsonTemplateParser { return { type: SyntaxType.UNARY_EXPR, op: '!', - arg: { + arg: createBlockExpression({ type: SyntaxType.COMPARISON_EXPR, op: Keyword.ANYOF, args: [expr, this.parseRelationalExpr()], - }, + }), }; } @@ -1186,6 +1215,7 @@ export class JsonTemplateParser { body: convertToStatementsExpr(expr), params: ['...args'], async: asyncFn, + lambda: true, }; } @@ -1333,6 +1363,10 @@ export class JsonTemplateParser { return this.parseArrayExpr(); } + if (this.lexer.matchPathType()) { + return this.parsePathTypeExpr(); + } + if (this.lexer.matchPath()) { return this.parsePath(); } @@ -1453,15 +1487,16 @@ export class JsonTemplateParser { newPathExpr.returnAsArray = lastPart.options?.toArray; } newPathExpr.parts = JsonTemplateParser.combinePathOptionParts(newPathExpr.parts); + let expr: Expression = newPathExpr; if (fnExpr) { expr = JsonTemplateParser.convertToFunctionCallExpr(fnExpr, newPathExpr); } if (shouldConvertAsBlock) { expr = JsonTemplateParser.convertToBlockExpr(expr); - newPathExpr.pathType = PathType.RICH; + newPathExpr.inferredPathType = PathType.RICH; } else if (this.isRichPath(newPathExpr)) { - newPathExpr.pathType = PathType.RICH; + newPathExpr.inferredPathType = PathType.RICH; } return expr; } diff --git a/src/reverse_translator.ts b/src/reverse_translator.ts new file mode 100644 index 0000000..e813d16 --- /dev/null +++ b/src/reverse_translator.ts @@ -0,0 +1,469 @@ +import { + ArrayExpression, + ArrayFilterExpression, + AssignmentExpression, + BinaryExpression, + BlockExpression, + ConditionalExpression, + DefinitionExpression, + EngineOptions, + Expression, + FunctionCallExpression, + FunctionExpression, + IncrementExpression, + IndexFilterExpression, + LambdaArgExpression, + LiteralExpression, + LoopControlExpression, + LoopExpression, + ObjectExpression, + ObjectFilterExpression, + ObjectPropExpression, + PathExpression, + PathOptions, + PathType, + RangeFilterExpression, + ReturnExpression, + SelectorExpression, + SpreadExpression, + StatementsExpression, + SyntaxType, + ThrowExpression, + TokenType, + UnaryExpression, +} from './types'; +import { translateLiteral } from './utils/transalator'; +import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants'; +import { escapeStr } from './utils'; + +export class JsonTemplateReverseTranslator { + private options?: EngineOptions; + + constructor(options?: EngineOptions) { + this.options = options; + } + + translate(expr: Expression): string { + switch (expr.type) { + case SyntaxType.LITERAL: + return this.translateLiteralExpression(expr as LiteralExpression); + case SyntaxType.STATEMENTS_EXPR: + return this.translateStatementsExpression(expr as StatementsExpression); + case SyntaxType.MATH_EXPR: + case SyntaxType.COMPARISON_EXPR: + case SyntaxType.IN_EXPR: + case SyntaxType.LOGICAL_AND_EXPR: + case SyntaxType.LOGICAL_OR_EXPR: + case SyntaxType.LOGICAL_COALESCE_EXPR: + return this.translateBinaryExpression(expr as BinaryExpression); + case SyntaxType.ARRAY_EXPR: + return this.translateArrayExpression(expr as ArrayExpression); + case SyntaxType.OBJECT_EXPR: + return this.translateObjectExpression(expr as ObjectExpression); + case SyntaxType.SPREAD_EXPR: + return this.translateSpreadExpression(expr as SpreadExpression); + case SyntaxType.BLOCK_EXPR: + return this.translateBlockExpression(expr as BlockExpression); + case SyntaxType.UNARY_EXPR: + return this.translateUnaryExpression(expr as UnaryExpression); + case SyntaxType.INCREMENT: + return this.translateIncrementExpression(expr as IncrementExpression); + case SyntaxType.PATH: + return this.translatePathExpression(expr as PathExpression); + case SyntaxType.CONDITIONAL_EXPR: + return this.translateConditionalExpression(expr as ConditionalExpression); + case SyntaxType.DEFINITION_EXPR: + return this.translateDefinitionExpression(expr as DefinitionExpression); + case SyntaxType.ASSIGNMENT_EXPR: + return this.translateAssignmentExpression(expr as AssignmentExpression); + case SyntaxType.FUNCTION_CALL_EXPR: + return this.translateFunctionCallExpression(expr as FunctionCallExpression); + case SyntaxType.FUNCTION_EXPR: + return this.translateFunctionExpression(expr as FunctionExpression); + case SyntaxType.THROW_EXPR: + return this.translateThrowExpression(expr as ThrowExpression); + case SyntaxType.RETURN_EXPR: + return this.translateReturnExpression(expr as ReturnExpression); + case SyntaxType.LOOP_EXPR: + return this.translateLoopExpression(expr as LoopExpression); + case SyntaxType.LOOP_CONTROL_EXPR: + return this.translateLoopControlExpression(expr as LoopControlExpression); + case SyntaxType.LAMBDA_ARG: + return this.translateLambdaArgExpression(expr as LambdaArgExpression); + case SyntaxType.OBJECT_FILTER_EXPR: + return this.translateObjectFilterExpression(expr as ObjectFilterExpression); + case SyntaxType.SELECTOR: + return this.translateSelectorExpression(expr as SelectorExpression); + case SyntaxType.OBJECT_PROP_EXPR: + return this.translateObjectPropExpression(expr as ObjectPropExpression); + case SyntaxType.OBJECT_INDEX_FILTER_EXPR: + return this.translateObjectIndexFilterExpression(expr as IndexFilterExpression); + case SyntaxType.ARRAY_FILTER_EXPR: + return this.translateArrayFilterExpression(expr as ArrayFilterExpression); + case SyntaxType.ARRAY_INDEX_FILTER_EXPR: + return this.translateArrayIndexFilterExpression(expr as IndexFilterExpression); + case SyntaxType.RANGE_FILTER_EXPR: + return this.translateRangeFilterExpression(expr as RangeFilterExpression); + default: + return ''; + } + } + + translateArrayFilterExpression(expr: ArrayFilterExpression): string { + return this.translate(expr.filter); + } + + translateRangeFilterExpression(expr: RangeFilterExpression): string { + const code: string[] = []; + code.push('['); + if (expr.fromIdx) { + code.push(this.translate(expr.fromIdx)); + } + code.push(':'); + if (expr.toIdx) { + code.push(this.translate(expr.toIdx)); + } + code.push(']'); + return code.join(''); + } + + translateArrayIndexFilterExpression(expr: IndexFilterExpression): string { + return this.translate(expr.indexes); + } + + translateObjectIndexFilterExpression(expr: IndexFilterExpression): string { + const code: string[] = []; + code.push('{'); + if (expr.exclude) { + code.push('!'); + } + code.push(this.translate(expr.indexes)); + code.push('}'); + return code.join(''); + } + + translateSelectorExpression(expr: SelectorExpression): string { + const code: string[] = []; + code.push(expr.selector); + if (expr.prop) { + if (expr.prop.type === TokenType.STR) { + code.push(escapeStr(expr.prop.value)); + } else { + code.push(expr.prop.value); + } + } + return code.join(''); + } + + translateWithWrapper(expr: Expression, prefix: string, suffix: string): string { + return `${prefix}${this.translate(expr)}${suffix}`; + } + + translateObjectFilterExpression(expr: ObjectFilterExpression): string { + if (expr.filter.type === SyntaxType.ALL_FILTER_EXPR) { + return '[*]'; + } + if (this.options?.defaultPathType === PathType.JSON) { + return this.translateWithWrapper(expr.filter, '[?(', ')]'); + } + return this.translateWithWrapper(expr.filter, '{', '}'); + } + + translateLambdaArgExpression(expr: LambdaArgExpression): string { + return `?${expr.index}`; + } + + translateLoopControlExpression(expr: LoopControlExpression): string { + return expr.control; + } + + translateLoopExpression(expr: LoopExpression): string { + const code: string[] = []; + code.push('for'); + code.push('('); + if (expr.init) { + code.push(this.translate(expr.init)); + } + code.push(';'); + if (expr.test) { + code.push(this.translate(expr.test)); + } + code.push(';'); + if (expr.update) { + code.push(this.translate(expr.update)); + } + code.push(')'); + code.push('{'); + code.push(this.translate(expr.body)); + code.push('}'); + return code.join(' '); + } + + translateReturnExpression(expr: ReturnExpression): string { + return `return ${this.translate(expr.value || EMPTY_EXPR)};`; + } + + translateThrowExpression(expr: ThrowExpression): string { + return `throw ${this.translate(expr.value)}`; + } + + translateExpressions(exprs: Expression[], sep: string): string { + return exprs.map((expr) => this.translate(expr)).join(sep); + } + + translateLambdaFunctionExpression(expr: FunctionExpression): string { + return `lambda ${this.translate(expr.body)}`; + } + + translateRegularFunctionExpression(expr: FunctionExpression): string { + const code: string[] = []; + code.push('function'); + code.push('('); + if (expr.params && expr.params.length > 0) { + code.push(expr.params.join(', ')); + } + code.push(')'); + code.push('{'); + code.push(this.translate(expr.body)); + code.push('}'); + return code.join(' '); + } + + translateFunctionExpression(expr: FunctionExpression): string { + if (expr.block) { + return this.translate(expr.body.statements[0]); + } + const code: string[] = []; + if (expr.async) { + code.push('async'); + } + if (expr.lambda) { + code.push(this.translateLambdaFunctionExpression(expr)); + } else { + code.push(this.translateRegularFunctionExpression(expr)); + } + return code.join(' '); + } + + translateFunctionCallExpression(expr: FunctionCallExpression): string { + const code: string[] = []; + if (expr.object) { + code.push(this.translate(expr.object)); + if (expr.id) { + code.push(` .${expr.id}`); + } + } else if (expr.parent) { + code.push(this.translatePathRootString(expr.parent, PathType.SIMPLE)); + if (expr.id) { + code.push(` .${expr.id}`); + } + } else if (expr.id) { + code.push(expr.id); + } + code.push('('); + if (expr.args) { + code.push(this.translateExpressions(expr.args, ', ')); + } + code.push(')'); + return code.join(''); + } + + translateAssignmentExpression(expr: AssignmentExpression): string { + const code: string[] = []; + code.push(this.translatePathExpression(expr.path)); + code.push(expr.op); + code.push(this.translate(expr.value)); + return code.join(' '); + } + + translateDefinitionExpression(expr: DefinitionExpression): string { + const code: string[] = []; + code.push(expr.definition); + if (expr.fromObject) { + code.push('{ '); + } + code.push(expr.vars.join(', ')); + if (expr.fromObject) { + code.push(' }'); + } + code.push(' = '); + code.push(this.translate(expr.value)); + return code.join(' '); + } + + translateConditionalExpressionBody(expr: Expression): string { + if (expr.type === SyntaxType.STATEMENTS_EXPR) { + return this.translateWithWrapper(expr, '{', '}'); + } + return this.translate(expr); + } + + translateConditionalExpression(expr: ConditionalExpression): string { + const code: string[] = []; + code.push(this.translate(expr.if)); + code.push(' ? '); + code.push(this.translateConditionalExpressionBody(expr.then)); + if (expr.else) { + code.push(' : '); + code.push(this.translateConditionalExpressionBody(expr.else)); + } + return code.join(''); + } + + translatePathType(pathType: PathType): string { + switch (pathType) { + case PathType.JSON: + return '~j '; + case PathType.RICH: + return '~r '; + case PathType.SIMPLE: + return '~s '; + default: + return ''; + } + } + + translatePathRootString(root: string, pathType: PathType): string { + if (root === BINDINGS_PARAM_KEY) { + return '$'; + } + if (root === DATA_PARAM_KEY) { + return pathType === PathType.JSON ? '$' : '^'; + } + return root; + } + + translatePathRoot(expr: PathExpression, pathType: PathType): string { + if (typeof expr.root === 'string') { + return this.translatePathRootString(expr.root, pathType); + } + if (expr.root) { + const code: string[] = []; + code.push(this.translate(expr.root)); + if (expr.root.type === SyntaxType.PATH) { + code.push('.(). '); + } + return code.join(''); + } + return '. '; + } + + translatePathOptions(options?: PathOptions): string { + if (!options) { + return ''; + } + const code: string[] = []; + if (options.item) { + code.push('@'); + code.push(options.item); + } + if (options.index) { + code.push('#'); + code.push(options.index); + } + if (options.toArray) { + code.push('[]'); + } + return code.join(''); + } + + translatePathParts(parts: Expression[]): string { + const code: string[] = []; + if ( + parts.length > 0 && + parts[0].type !== SyntaxType.SELECTOR && + parts[0].type !== SyntaxType.BLOCK_EXPR + ) { + code.push('.'); + } + for (const part of parts) { + if (part.type === SyntaxType.BLOCK_EXPR) { + code.push('.'); + } + code.push(this.translate(part)); + code.push(this.translatePathOptions(part.options)); + } + return code.join(''); + } + + translatePathExpression(expr: PathExpression): string { + const code: string[] = []; + code.push(this.translatePathType(expr.pathType)); + code.push(this.translatePathRoot(expr, expr.inferredPathType)); + code.push(this.translatePathOptions(expr.options)); + code.push(this.translatePathParts(expr.parts)); + if (expr.returnAsArray) { + code.push('[]'); + } + return code.join(''); + } + + translateIncrementExpression(expr: IncrementExpression): string { + if (expr.postfix) { + return `${expr.id}${expr.op}`; + } + return `${expr.op}${expr.id}`; + } + + translateUnaryExpression(expr: UnaryExpression): string { + return `${expr.op} ${this.translate(expr.arg)}`; + } + + translateBlockExpression(expr: BlockExpression): string { + const code: string[] = []; + code.push('('); + code.push(this.translateExpressions(expr.statements, ';')); + code.push(')'); + return code.join(''); + } + + translateSpreadExpression(expr: SpreadExpression): string { + return `...${this.translate(expr.value)}`; + } + + translateObjectExpression(expr: ObjectExpression): string { + const code: string[] = []; + code.push('{\n'); + code.push(this.translateExpressions(expr.props, ',\n')); + code.push('\n}'); + return code.join(''); + } + + translateObjectPropExpression(expr: ObjectPropExpression): string { + const code: string[] = []; + if (expr.key) { + if (typeof expr.key === 'string') { + code.push(expr.key); + } else if (expr.key.type === SyntaxType.LITERAL) { + code.push(this.translate(expr.key)); + } else { + code.push(this.translateWithWrapper(expr.key, '[', ']')); + } + code.push(': '); + } + code.push(this.translate(expr.value)); + return code.join(''); + } + + translateArrayExpression(expr: ArrayExpression): string { + const code: string[] = []; + code.push('['); + code.push(this.translateExpressions(expr.elements, ', ')); + code.push(']'); + return code.join(''); + } + + translateLiteralExpression(expr: LiteralExpression): string { + return translateLiteral(expr.tokenType, expr.value); + } + + translateStatementsExpression(expr: StatementsExpression): string { + return this.translateExpressions(expr.statements, ';\n'); + } + + translateBinaryExpression(expr: BinaryExpression): string { + const left = this.translate(expr.args[0]); + const right = this.translate(expr.args[1]); + return `${left} ${expr.op} ${right}`; + } +} diff --git a/src/translator.ts b/src/translator.ts index 28b6736..9c0a6cd 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -39,9 +39,9 @@ import { LoopExpression, IncrementExpression, LoopControlExpression, - Literal, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; +import { translateLiteral } from './utils/transalator'; export class JsonTemplateTranslator { private vars: string[] = []; @@ -250,7 +250,7 @@ export class JsonTemplateTranslator { code.push(`return ${value};`); this.releaseVars(value); } - code.push(`return ${ctx};`); + code.push(`return;`); return code.join(''); } @@ -377,7 +377,7 @@ export class JsonTemplateTranslator { } private translatePathExpr(expr: PathExpression, dest: string, ctx: string): string { - if (expr.pathType === PathType.SIMPLE) { + if (expr.inferredPathType === PathType.SIMPLE) { return this.translateSimplePathExpr(expr, dest, ctx); } const code: string[] = []; @@ -574,7 +574,7 @@ export class JsonTemplateTranslator { } private translateLiteralExpr(expr: LiteralExpression, dest: string, _ctx: string): string { - const literalCode = this.translateLiteral(expr.tokenType, expr.value); + const literalCode = translateLiteral(expr.tokenType, expr.value); return JsonTemplateTranslator.generateAssignmentCode(dest, literalCode); } @@ -718,14 +718,6 @@ export class JsonTemplateTranslator { return code.join(''); } - private translateLiteral(type: TokenType, val: Literal): string { - if (type === TokenType.STR) { - return escapeStr(String(val)); - } - - return String(val); - } - private translateUnaryExpr(expr: UnaryExpression, dest: string, ctx: string): string { const code: string[] = []; const val = this.acquireVar(); diff --git a/src/types.ts b/src/types.ts index 85c2acc..e76f38a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -89,7 +89,6 @@ export enum SyntaxType { ARRAY_EXPR = 'array_expr', BLOCK_EXPR = 'block_expr', FUNCTION_EXPR = 'function_expr', - FUNCTION_CALL_ARG = 'function_call_arg', FUNCTION_CALL_EXPR = 'function_call_expr', RETURN_EXPR = 'return_expr', THROW_EXPR = 'throw_expr', @@ -102,6 +101,7 @@ export enum PathType { SIMPLE = 'simple', RICH = 'rich', JSON = 'json', + UNKNOWN = 'unknown', } export interface EngineOptions { @@ -127,6 +127,10 @@ export interface Expression { [key: string]: any; } +export interface PathOptionsExpression extends Expression { + options: PathOptions; +} + export interface LambdaArgExpression extends Expression { index: number; } @@ -136,6 +140,7 @@ export interface FunctionExpression extends Expression { body: StatementsExpression; block?: boolean; async?: boolean; + lambda?: boolean; } export interface BlockExpression extends Expression { @@ -203,7 +208,7 @@ export interface ObjectFilterExpression extends Expression { } export interface ArrayFilterExpression extends Expression { - filter: RangeFilterExpression | IndexFilterExpression | AllFilterExpression; + filter: RangeFilterExpression | IndexFilterExpression; } export type Literal = string | number | boolean | null | undefined; @@ -216,6 +221,7 @@ export interface PathExpression extends Expression { root?: Expression | string; returnAsArray?: boolean; pathType: PathType; + inferredPathType: PathType; } export interface IncrementExpression extends Expression { diff --git a/src/utils/common.ts b/src/utils/common.ts index f72c61c..5f9c2b1 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,4 +1,4 @@ -import { type Expression, type StatementsExpression, SyntaxType } from '../types'; +import { type Expression, type StatementsExpression, SyntaxType, BlockExpression } from '../types'; export function toArray(val: T | T[] | undefined): T[] | undefined { if (val === undefined || val === null) { @@ -14,6 +14,13 @@ export function getLastElement(arr: T[]): T | undefined { return arr[arr.length - 1]; } +export function createBlockExpression(expr: Expression): BlockExpression { + return { + type: SyntaxType.BLOCK_EXPR, + statements: [expr], + }; +} + export function convertToStatementsExpr(...expressions: Expression[]): StatementsExpression { return { type: SyntaxType.STATEMENTS_EXPR, diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 4290182..65a9874 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -9,8 +9,9 @@ import { FlatMappingAST, Expression, IndexFilterExpression, + BlockExpression, } from '../types'; -import { getLastElement } from './common'; +import { createBlockExpression, getLastElement } from './common'; function CreateObjectExpression(): ObjectExpression { return { @@ -67,7 +68,7 @@ function processAllFilter( } const matchedInputParts = currentInputAST.parts.splice(0, filterIndex + 1); if (currentOutputPropAST.value.type !== SyntaxType.PATH) { - matchedInputParts.push(currentOutputPropAST.value); + matchedInputParts.push(createBlockExpression(currentOutputPropAST.value)); currentOutputPropAST.value = { type: SyntaxType.PATH, root: currentInputAST.root, @@ -78,7 +79,8 @@ function processAllFilter( } currentInputAST.root = undefined; - return getLastElement(currentOutputPropAST.value.parts) as ObjectExpression; + const blockExpr = getLastElement(currentOutputPropAST.value.parts) as BlockExpression; + return blockExpr.statements[0] as ObjectExpression; } function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpression) { diff --git a/src/utils/transalator.ts b/src/utils/transalator.ts new file mode 100644 index 0000000..2507ac7 --- /dev/null +++ b/src/utils/transalator.ts @@ -0,0 +1,10 @@ +import { TokenType, Literal } from '../types'; +import { escapeStr } from './common'; + +export function translateLiteral(type: TokenType, val: Literal): string { + if (type === TokenType.STR) { + return escapeStr(String(val)); + } + + return String(val); +} diff --git a/test/scenarios/context_variables/filter.jt b/test/scenarios/context_variables/filter.jt index 5fb62bd..ecedf51 100644 --- a/test/scenarios/context_variables/filter.jt +++ b/test/scenarios/context_variables/filter.jt @@ -5,4 +5,5 @@ .{.[].length > 1}.().@item#idx.({ a: ~r item.a, idx: idx -}) \ No newline at end of file +}) + diff --git a/test/scenarios/filters/object_filters.jt b/test/scenarios/filters/object_filters.jt index cc79126..a5fa75e 100644 --- a/test/scenarios/filters/object_filters.jt +++ b/test/scenarios/filters/object_filters.jt @@ -5,3 +5,4 @@ {.a[].length > 1} {3 in .a}.{.a[].includes(4)} {typeof .b === "number"} + diff --git a/test/scenarios/filters/object_indexes.jt b/test/scenarios/filters/object_indexes.jt index caa14f3..60f22cd 100644 --- a/test/scenarios/filters/object_indexes.jt +++ b/test/scenarios/filters/object_indexes.jt @@ -4,4 +4,4 @@ let obj = { c: 3, d: 4 }; -{...obj{["a", "b"]}, ...obj{~["a", "b"]}} \ No newline at end of file +{...obj{["a", "b"]}, ...obj{~["a", "b", "c"]}, ...obj{!["d"]}}; \ No newline at end of file diff --git a/test/scenarios/objects/template.jt b/test/scenarios/objects/template.jt index 35df1f3..be659cf 100644 --- a/test/scenarios/objects/template.jt +++ b/test/scenarios/objects/template.jt @@ -2,7 +2,7 @@ let c = "c key"; let d = 3; let b = 2; let a = { - "a": 1, + "a b": 1, b, // [c] coverts to "c key" [c]: { @@ -11,7 +11,7 @@ let a = { }, }; a.({ - a: .a, + a: ."a b", b: .b, ...(.'c key') }) \ No newline at end of file diff --git a/test/scenarios/return/data.ts b/test/scenarios/return/data.ts index 5487785..b9c5623 100644 --- a/test/scenarios/return/data.ts +++ b/test/scenarios/return/data.ts @@ -12,10 +12,20 @@ export const data: Scenario[] = [ 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', }, { + templatePath: 'return_no_value.jt', + input: 2, + }, + { + templatePath: 'return_no_value.jt', + input: 2, + }, + { + templatePath: 'return_value.jt', input: 3, output: 1, }, { + templatePath: 'return_value.jt', input: 2, output: 1, }, diff --git a/test/scenarios/return/return_no_value.jt b/test/scenarios/return/return_no_value.jt new file mode 100644 index 0000000..309474f --- /dev/null +++ b/test/scenarios/return/return_no_value.jt @@ -0,0 +1,4 @@ +(. % 2 === 0) ? { + return; +} +(. - 1)/2; \ No newline at end of file diff --git a/test/scenarios/return/template.jt b/test/scenarios/return/return_value.jt similarity index 100% rename from test/scenarios/return/template.jt rename to test/scenarios/return/return_value.jt diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts index 53c1afe..ce104c7 100644 --- a/test/utils/scenario.ts +++ b/test/utils/scenario.ts @@ -12,7 +12,11 @@ export class ScenarioUtils { } scenario.options = scenario.options || {}; scenario.options.defaultPathType = scenario.options.defaultPathType || PathType.SIMPLE; - return JsonTemplateEngine.create(template, scenario.options); + const newTemplate = JsonTemplateEngine.reverseTranslate( + JsonTemplateEngine.parse(template, scenario.options), + scenario.options, + ); + return JsonTemplateEngine.create(newTemplate, scenario.options); } static evaluateScenario(templateEngine: JsonTemplateEngine, scenario: Scenario): any { From 71f3b06e7ef1785070a3bd3c297600c1ffab5812 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Thu, 30 May 2024 15:46:03 +0530 Subject: [PATCH 21/29] fix: formatting issue reverse translator --- src/reverse_translator.ts | 64 +++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/reverse_translator.ts b/src/reverse_translator.ts index e813d16..97a8e6a 100644 --- a/src/reverse_translator.ts +++ b/src/reverse_translator.ts @@ -44,6 +44,12 @@ export class JsonTemplateReverseTranslator { } translate(expr: Expression): string { + let code: string = this.translateExpression(expr); + code = code.replace(/\.\s+\./g, '.'); + return code; + } + + translateExpression(expr: Expression): string { switch (expr.type) { case SyntaxType.LITERAL: return this.translateLiteralExpression(expr as LiteralExpression); @@ -110,25 +116,25 @@ export class JsonTemplateReverseTranslator { } translateArrayFilterExpression(expr: ArrayFilterExpression): string { - return this.translate(expr.filter); + return this.translateExpression(expr.filter); } translateRangeFilterExpression(expr: RangeFilterExpression): string { const code: string[] = []; code.push('['); if (expr.fromIdx) { - code.push(this.translate(expr.fromIdx)); + code.push(this.translateExpression(expr.fromIdx)); } code.push(':'); if (expr.toIdx) { - code.push(this.translate(expr.toIdx)); + code.push(this.translateExpression(expr.toIdx)); } code.push(']'); return code.join(''); } translateArrayIndexFilterExpression(expr: IndexFilterExpression): string { - return this.translate(expr.indexes); + return this.translateExpression(expr.indexes); } translateObjectIndexFilterExpression(expr: IndexFilterExpression): string { @@ -137,7 +143,7 @@ export class JsonTemplateReverseTranslator { if (expr.exclude) { code.push('!'); } - code.push(this.translate(expr.indexes)); + code.push(this.translateExpression(expr.indexes)); code.push('}'); return code.join(''); } @@ -156,7 +162,7 @@ export class JsonTemplateReverseTranslator { } translateWithWrapper(expr: Expression, prefix: string, suffix: string): string { - return `${prefix}${this.translate(expr)}${suffix}`; + return `${prefix}${this.translateExpression(expr)}${suffix}`; } translateObjectFilterExpression(expr: ObjectFilterExpression): string { @@ -182,37 +188,37 @@ export class JsonTemplateReverseTranslator { code.push('for'); code.push('('); if (expr.init) { - code.push(this.translate(expr.init)); + code.push(this.translateExpression(expr.init)); } code.push(';'); if (expr.test) { - code.push(this.translate(expr.test)); + code.push(this.translateExpression(expr.test)); } code.push(';'); if (expr.update) { - code.push(this.translate(expr.update)); + code.push(this.translateExpression(expr.update)); } code.push(')'); code.push('{'); - code.push(this.translate(expr.body)); + code.push(this.translateExpression(expr.body)); code.push('}'); return code.join(' '); } translateReturnExpression(expr: ReturnExpression): string { - return `return ${this.translate(expr.value || EMPTY_EXPR)};`; + return `return ${this.translateExpression(expr.value || EMPTY_EXPR)};`; } translateThrowExpression(expr: ThrowExpression): string { - return `throw ${this.translate(expr.value)}`; + return `throw ${this.translateExpression(expr.value)}`; } translateExpressions(exprs: Expression[], sep: string): string { - return exprs.map((expr) => this.translate(expr)).join(sep); + return exprs.map((expr) => this.translateExpression(expr)).join(sep); } translateLambdaFunctionExpression(expr: FunctionExpression): string { - return `lambda ${this.translate(expr.body)}`; + return `lambda ${this.translateExpression(expr.body)}`; } translateRegularFunctionExpression(expr: FunctionExpression): string { @@ -224,14 +230,14 @@ export class JsonTemplateReverseTranslator { } code.push(')'); code.push('{'); - code.push(this.translate(expr.body)); + code.push(this.translateExpression(expr.body)); code.push('}'); return code.join(' '); } translateFunctionExpression(expr: FunctionExpression): string { if (expr.block) { - return this.translate(expr.body.statements[0]); + return this.translateExpression(expr.body.statements[0]); } const code: string[] = []; if (expr.async) { @@ -248,7 +254,7 @@ export class JsonTemplateReverseTranslator { translateFunctionCallExpression(expr: FunctionCallExpression): string { const code: string[] = []; if (expr.object) { - code.push(this.translate(expr.object)); + code.push(this.translateExpression(expr.object)); if (expr.id) { code.push(` .${expr.id}`); } @@ -272,7 +278,7 @@ export class JsonTemplateReverseTranslator { const code: string[] = []; code.push(this.translatePathExpression(expr.path)); code.push(expr.op); - code.push(this.translate(expr.value)); + code.push(this.translateExpression(expr.value)); return code.join(' '); } @@ -287,7 +293,7 @@ export class JsonTemplateReverseTranslator { code.push(' }'); } code.push(' = '); - code.push(this.translate(expr.value)); + code.push(this.translateExpression(expr.value)); return code.join(' '); } @@ -295,12 +301,12 @@ export class JsonTemplateReverseTranslator { if (expr.type === SyntaxType.STATEMENTS_EXPR) { return this.translateWithWrapper(expr, '{', '}'); } - return this.translate(expr); + return this.translateExpression(expr); } translateConditionalExpression(expr: ConditionalExpression): string { const code: string[] = []; - code.push(this.translate(expr.if)); + code.push(this.translateExpression(expr.if)); code.push(' ? '); code.push(this.translateConditionalExpressionBody(expr.then)); if (expr.else) { @@ -339,7 +345,7 @@ export class JsonTemplateReverseTranslator { } if (expr.root) { const code: string[] = []; - code.push(this.translate(expr.root)); + code.push(this.translateExpression(expr.root)); if (expr.root.type === SyntaxType.PATH) { code.push('.(). '); } @@ -380,7 +386,7 @@ export class JsonTemplateReverseTranslator { if (part.type === SyntaxType.BLOCK_EXPR) { code.push('.'); } - code.push(this.translate(part)); + code.push(this.translateExpression(part)); code.push(this.translatePathOptions(part.options)); } return code.join(''); @@ -406,7 +412,7 @@ export class JsonTemplateReverseTranslator { } translateUnaryExpression(expr: UnaryExpression): string { - return `${expr.op} ${this.translate(expr.arg)}`; + return `${expr.op} ${this.translateExpression(expr.arg)}`; } translateBlockExpression(expr: BlockExpression): string { @@ -418,7 +424,7 @@ export class JsonTemplateReverseTranslator { } translateSpreadExpression(expr: SpreadExpression): string { - return `...${this.translate(expr.value)}`; + return `...${this.translateExpression(expr.value)}`; } translateObjectExpression(expr: ObjectExpression): string { @@ -435,13 +441,13 @@ export class JsonTemplateReverseTranslator { if (typeof expr.key === 'string') { code.push(expr.key); } else if (expr.key.type === SyntaxType.LITERAL) { - code.push(this.translate(expr.key)); + code.push(this.translateExpression(expr.key)); } else { code.push(this.translateWithWrapper(expr.key, '[', ']')); } code.push(': '); } - code.push(this.translate(expr.value)); + code.push(this.translateExpression(expr.value)); return code.join(''); } @@ -462,8 +468,8 @@ export class JsonTemplateReverseTranslator { } translateBinaryExpression(expr: BinaryExpression): string { - const left = this.translate(expr.args[0]); - const right = this.translate(expr.args[1]); + const left = this.translateExpression(expr.args[0]); + const right = this.translateExpression(expr.args[1]); return `${left} ${expr.op} ${right}`; } } From 6b5269e64e0f037f2c534e2aea4bf4a041025b43 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Sat, 1 Jun 2024 17:54:12 +0530 Subject: [PATCH 22/29] refactor: add error handling in flat mapping convertor --- src/engine.ts | 5 +++-- src/reverse_translator.ts | 4 ++-- src/types.ts | 6 +++--- src/utils/converter.ts | 11 +++++++---- test/scenarios/mappings/data.ts | 9 ++++++++- test/scenarios/mappings/invalid_mappings.json | 10 ++++++++++ 6 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 test/scenarios/mappings/invalid_mappings.json diff --git a/src/engine.ts b/src/engine.ts index 2036c5f..2a6fe9c 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -42,8 +42,9 @@ export class JsonTemplateEngine { options?: EngineOptions, ): Expression { const flatMappingAST = mappings.map((mapping) => ({ - input: JsonTemplateEngine.parse(mapping.input, options).statements[0], - output: JsonTemplateEngine.parse(mapping.output, options).statements[0], + ...mapping, + inputExpr: JsonTemplateEngine.parse(mapping.input, options).statements[0], + outputExpr: JsonTemplateEngine.parse(mapping.output, options).statements[0], })); return convertToObjectMapping(flatMappingAST); } diff --git a/src/reverse_translator.ts b/src/reverse_translator.ts index 97a8e6a..f53d7d2 100644 --- a/src/reverse_translator.ts +++ b/src/reverse_translator.ts @@ -429,8 +429,8 @@ export class JsonTemplateReverseTranslator { translateObjectExpression(expr: ObjectExpression): string { const code: string[] = []; - code.push('{\n'); - code.push(this.translateExpressions(expr.props, ',\n')); + code.push('{\n\t'); + code.push(this.translateExpressions(expr.props, ',\n\t')); code.push('\n}'); return code.join(''); } diff --git a/src/types.ts b/src/types.ts index e76f38a..53c283f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -279,7 +279,7 @@ export type FlatMappingPaths = { output: string; }; -export type FlatMappingAST = { - input: PathExpression; - output: PathExpression; +export type FlatMappingAST = FlatMappingPaths & { + inputExpr: PathExpression; + outputExpr: PathExpression; }; diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 65a9874..bb14974 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -85,11 +85,14 @@ function processAllFilter( function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpression) { let currentOutputPropsAST = outputAST.props; - const currentInputAST = flatMapping.input; + const currentInputAST = flatMapping.inputExpr; - const numOutputParts = flatMapping.output.parts.length; + const numOutputParts = flatMapping.outputExpr.parts.length; for (let i = 0; i < numOutputParts; i++) { - const outputPart = flatMapping.output.parts[i]; + if (!currentOutputPropsAST) { + throw new Error(`Failed to process output mapping: ${flatMapping.output}`); + } + const outputPart = flatMapping.outputExpr.parts[i]; if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { const key = outputPart.prop.value; @@ -105,7 +108,7 @@ function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpres const currentOutputPropAST = findOrCreateObjectPropExpression(currentOutputPropsAST, key); let objectExpr: ObjectExpression = currentOutputPropAST.value as ObjectExpression; - const nextOutputPart = flatMapping.output.parts[i + 1] as ArrayFilterExpression; + const nextOutputPart = flatMapping.outputExpr.parts[i + 1] as ArrayFilterExpression; if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { objectExpr = processAllFilter(currentInputAST, currentOutputPropAST); } else if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index d125031..a2f27e1 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -143,7 +143,14 @@ export const data: Scenario[] = [ ], }, }, - + { + containsMappings: true, + templatePath: 'invalid_mappings.json', + options: { + defaultPathType: PathType.JSON, + }, + error: 'Failed to process output mapping', + }, { containsMappings: true, templatePath: 'mappings_with_root_fields.json', diff --git a/test/scenarios/mappings/invalid_mappings.json b/test/scenarios/mappings/invalid_mappings.json new file mode 100644 index 0000000..266a1ec --- /dev/null +++ b/test/scenarios/mappings/invalid_mappings.json @@ -0,0 +1,10 @@ +[ + { + "input": "$.events[0]", + "output": "$.events[0].name" + }, + { + "input": "$.discount", + "output": "$.events[0].name[*].discount" + } +] From 9fa7b504d64ed24f001104f0cecc8ac7ee5cb8f8 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 3 Jun 2024 12:30:53 +0530 Subject: [PATCH 23/29] fix: error handling in convertor --- src/utils/converter.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/converter.ts b/src/utils/converter.ts index bb14974..d84b544 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -89,9 +89,6 @@ function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpres const numOutputParts = flatMapping.outputExpr.parts.length; for (let i = 0; i < numOutputParts; i++) { - if (!currentOutputPropsAST) { - throw new Error(`Failed to process output mapping: ${flatMapping.output}`); - } const outputPart = flatMapping.outputExpr.parts[i]; if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { @@ -117,6 +114,13 @@ function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpres nextOutputPart.filter as IndexFilterExpression, ); } + if ( + objectExpr.type !== SyntaxType.OBJECT_EXPR || + !objectExpr.props || + !Array.isArray(objectExpr.props) + ) { + throw new Error(`Failed to process output mapping: ${flatMapping.output}`); + } currentOutputPropsAST = objectExpr.props; } } From 644f7aa56dc90a9f815a4000c310a5462b53ae0b Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 3 Jun 2024 12:34:12 +0530 Subject: [PATCH 24/29] fix: typo in file name --- src/reverse_translator.ts | 2 +- src/translator.ts | 2 +- src/utils/{transalator.ts => translator.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/utils/{transalator.ts => translator.ts} (100%) diff --git a/src/reverse_translator.ts b/src/reverse_translator.ts index f53d7d2..330b789 100644 --- a/src/reverse_translator.ts +++ b/src/reverse_translator.ts @@ -32,7 +32,7 @@ import { TokenType, UnaryExpression, } from './types'; -import { translateLiteral } from './utils/transalator'; +import { translateLiteral } from './utils/translator'; import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants'; import { escapeStr } from './utils'; diff --git a/src/translator.ts b/src/translator.ts index 9c0a6cd..f3add5f 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -41,7 +41,7 @@ import { LoopControlExpression, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; -import { translateLiteral } from './utils/transalator'; +import { translateLiteral } from './utils/translator'; export class JsonTemplateTranslator { private vars: string[] = []; diff --git a/src/utils/transalator.ts b/src/utils/translator.ts similarity index 100% rename from src/utils/transalator.ts rename to src/utils/translator.ts From 0625be453ed4b209ed0bbf0659d215aed5c93e01 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 3 Jun 2024 13:06:54 +0530 Subject: [PATCH 25/29] refactor: address pr comments on template parsing --- src/engine.ts | 26 ++++++++++++++------------ src/parser.ts | 5 ++--- src/utils/common.ts | 14 +++++++++++++- test/scenarios/return/data.ts | 4 ---- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 2a6fe9c..fbf8b68 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -4,7 +4,7 @@ import { JsonTemplateParser } from './parser'; import { JsonTemplateReverseTranslator } from './reverse_translator'; import { JsonTemplateTranslator } from './translator'; import { EngineOptions, Expression, FlatMappingPaths } from './types'; -import { CreateAsyncFunction, convertToObjectMapping } from './utils'; +import { CreateAsyncFunction, convertToObjectMapping, isExpression } from './utils'; export class JsonTemplateEngine { private readonly fn: Function; @@ -63,24 +63,26 @@ export class JsonTemplateEngine { return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options)); } - static parse(template: string | FlatMappingPaths[], options?: EngineOptions): Expression { - if (Array.isArray(template)) { - return this.parseMappingPaths(template, options); + static parse( + template: string | Expression | FlatMappingPaths[], + options?: EngineOptions, + ): Expression { + if (isExpression(template)) { + return template as Expression; } - const lexer = new JsonTemplateLexer(template); - const parser = new JsonTemplateParser(lexer, options); - return parser.parse(); + if (typeof template === 'string') { + const lexer = new JsonTemplateLexer(template); + const parser = new JsonTemplateParser(lexer, options); + return parser.parse(); + } + return this.parseMappingPaths(template as FlatMappingPaths[], options); } static translate( template: string | Expression | FlatMappingPaths[], options?: EngineOptions, ): string { - let templateExpr = template as Expression; - if (typeof template === 'string' || Array.isArray(template)) { - templateExpr = this.parse(template, options); - } - return this.translateExpression(templateExpr); + return this.translateExpression(this.parse(template, options)); } static reverseTranslate(expr: Expression, options?: EngineOptions): string { diff --git a/src/parser.ts b/src/parser.ts index 965d16d..73041c4 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,5 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants'; +import { JsonTemplateEngine } from './engine'; import { JsonTemplateLexerError, JsonTemplateParserError } from './errors'; import { JsonTemplateLexer } from './lexer'; import { @@ -1235,14 +1236,12 @@ export class JsonTemplateParser { const expr = skipJsonify ? this.parseCompileTimeBaseExpr() : this.parseBaseExpr(); this.lexer.expect('}'); this.lexer.expect('}'); - // eslint-disable-next-line global-require - const { JsonTemplateEngine } = require('./engine'); const exprVal = JsonTemplateEngine.createAsSync(expr).evaluate( {}, this.options?.compileTimeBindings, ); const template = skipJsonify ? exprVal : JSON.stringify(exprVal); - return JsonTemplateParser.parseBaseExprFromTemplate(template); + return JsonTemplateParser.parseBaseExprFromTemplate(template as string); } private parseNumber(): LiteralExpression { diff --git a/src/utils/common.ts b/src/utils/common.ts index 5f9c2b1..83bf884 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,4 +1,10 @@ -import { type Expression, type StatementsExpression, SyntaxType, BlockExpression } from '../types'; +import { + type Expression, + type StatementsExpression, + SyntaxType, + BlockExpression, + FlatMappingPaths, +} from '../types'; export function toArray(val: T | T[] | undefined): T[] | undefined { if (val === undefined || val === null) { @@ -33,6 +39,12 @@ export function CreateAsyncFunction(...args) { return async function () {}.constructor(...args); } +export function isExpression(val: string | Expression | FlatMappingPaths[]): boolean { + return ( + typeof val === 'object' && !Array.isArray(val) && Object.values(SyntaxType).includes(val.type) + ); +} + export function escapeStr(s?: string): string { if (typeof s !== 'string') { return ''; diff --git a/test/scenarios/return/data.ts b/test/scenarios/return/data.ts index b9c5623..c2c0d34 100644 --- a/test/scenarios/return/data.ts +++ b/test/scenarios/return/data.ts @@ -15,10 +15,6 @@ export const data: Scenario[] = [ templatePath: 'return_no_value.jt', input: 2, }, - { - templatePath: 'return_no_value.jt', - input: 2, - }, { templatePath: 'return_value.jt', input: 3, From 9ca6100532bdee0a72d02ed98d0bd3e50c94a55d Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 3 Jun 2024 13:13:00 +0530 Subject: [PATCH 26/29] fix: sonar lint issues --- src/engine.ts | 22 +++++----------------- src/parser.ts | 3 ++- src/translator.ts | 2 +- src/types.ts | 2 ++ 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index fbf8b68..2edcae6 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -3,7 +3,7 @@ import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; import { JsonTemplateReverseTranslator } from './reverse_translator'; import { JsonTemplateTranslator } from './translator'; -import { EngineOptions, Expression, FlatMappingPaths } from './types'; +import { EngineOptions, Expression, FlatMappingPaths, TemplateInput } from './types'; import { CreateAsyncFunction, convertToObjectMapping, isExpression } from './utils'; export class JsonTemplateEngine { @@ -21,10 +21,7 @@ export class JsonTemplateEngine { return Function(DATA_PARAM_KEY, BINDINGS_PARAM_KEY, this.translate(templateOrExpr, options)); } - private static compileAsAsync( - templateOrExpr: string | Expression | FlatMappingPaths[], - options?: EngineOptions, - ): Function { + private static compileAsAsync(templateOrExpr: TemplateInput, options?: EngineOptions): Function { return CreateAsyncFunction( DATA_PARAM_KEY, BINDINGS_PARAM_KEY, @@ -49,10 +46,7 @@ export class JsonTemplateEngine { return convertToObjectMapping(flatMappingAST); } - static create( - templateOrExpr: string | Expression | FlatMappingPaths[], - options?: EngineOptions, - ): JsonTemplateEngine { + static create(templateOrExpr: TemplateInput, options?: EngineOptions): JsonTemplateEngine { return new JsonTemplateEngine(this.compileAsAsync(templateOrExpr, options)); } @@ -63,10 +57,7 @@ export class JsonTemplateEngine { return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options)); } - static parse( - template: string | Expression | FlatMappingPaths[], - options?: EngineOptions, - ): Expression { + static parse(template: TemplateInput, options?: EngineOptions): Expression { if (isExpression(template)) { return template as Expression; } @@ -78,10 +69,7 @@ export class JsonTemplateEngine { return this.parseMappingPaths(template as FlatMappingPaths[], options); } - static translate( - template: string | Expression | FlatMappingPaths[], - options?: EngineOptions, - ): string { + static translate(template: TemplateInput, options?: EngineOptions): string { return this.translateExpression(this.parse(template, options)); } diff --git a/src/parser.ts b/src/parser.ts index 73041c4..e673fc6 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-cycle */ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants'; import { JsonTemplateEngine } from './engine'; import { JsonTemplateLexerError, JsonTemplateParserError } from './errors'; @@ -305,7 +306,7 @@ export class JsonTemplateParser { } return { pathType: PathType.UNKNOWN, - inferredPathType: this.options?.defaultPathType || PathType.RICH, + inferredPathType: this.options?.defaultPathType ?? PathType.RICH, }; } diff --git a/src/translator.ts b/src/translator.ts index f3add5f..1d90e16 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -509,7 +509,7 @@ export class JsonTemplateTranslator { code.push(`if(${functionName} && typeof ${functionName} === 'function'){`); code.push(result, '=', functionName, '(', functionArgsStr, ');'); code.push('} else {'); - code.push(result, '=', expr.id, '(', expr.parent || result, ',', functionArgsStr, ');'); + code.push(result, '=', expr.id, '(', expr.parent ?? result, ',', functionArgsStr, ');'); code.push('}'); } else { code.push(result, '=', functionName, '(', functionArgsStr, ');'); diff --git a/src/types.ts b/src/types.ts index 53c283f..ecd16d8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -283,3 +283,5 @@ export type FlatMappingAST = FlatMappingPaths & { inputExpr: PathExpression; outputExpr: PathExpression; }; + +export type TemplateInput = string | Expression | FlatMappingPaths[]; From e386ebbf2c7c0c19e42eb98849c1393ca4572406 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 3 Jun 2024 15:29:26 +0530 Subject: [PATCH 27/29] fix: eslint issue --- src/engine.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine.ts b/src/engine.ts index 2edcae6..bd6a657 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-cycle */ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants'; import { JsonTemplateLexer } from './lexer'; import { JsonTemplateParser } from './parser'; From 71e11c05cced2aa57d99cd3a361afe0dc6498ff5 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 3 Jun 2024 20:19:40 +0530 Subject: [PATCH 28/29] refactor: mappings convertor --- src/utils/converter.ts | 87 +++++++++++++---------- test/scenarios/mappings/all_features.json | 4 ++ test/scenarios/mappings/data.ts | 4 ++ 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/utils/converter.ts b/src/utils/converter.ts index d84b544..48d722c 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -2,7 +2,6 @@ import { SyntaxType, PathExpression, - ArrayFilterExpression, ObjectPropExpression, ArrayExpression, ObjectExpression, @@ -83,48 +82,57 @@ function processAllFilter( return blockExpr.statements[0] as ObjectExpression; } -function processFlatMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpression) { - let currentOutputPropsAST = outputAST.props; - const currentInputAST = flatMapping.inputExpr; +function handleNextPart( + nextOutputPart: Expression, + currentInputAST: PathExpression, + currentOutputPropAST: ObjectPropExpression, +): ObjectExpression { + if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { + return processAllFilter(currentInputAST, currentOutputPropAST); + } + if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { + return processArrayIndexFilter( + currentOutputPropAST, + nextOutputPart.filter as IndexFilterExpression, + ); + } + return currentOutputPropAST.value as ObjectExpression; +} - const numOutputParts = flatMapping.outputExpr.parts.length; - for (let i = 0; i < numOutputParts; i++) { - const outputPart = flatMapping.outputExpr.parts[i]; +function processFlatMappingPart( + flatMapping: FlatMappingAST, + partNum: number, + currentOutputPropsAST: ObjectPropExpression[], +): ObjectPropExpression[] { + const outputPart = flatMapping.outputExpr.parts[partNum]; - if (outputPart.type === SyntaxType.SELECTOR && outputPart.prop?.value) { - const key = outputPart.prop.value; + if (outputPart.type !== SyntaxType.SELECTOR || !outputPart.prop?.value) { + return currentOutputPropsAST; + } + const key = outputPart.prop.value; - if (i === numOutputParts - 1) { - currentOutputPropsAST.push({ - type: SyntaxType.OBJECT_PROP_EXPR, - key, - value: currentInputAST, - } as ObjectPropExpression); - break; - } + if (partNum === flatMapping.outputExpr.parts.length - 1) { + currentOutputPropsAST.push({ + type: SyntaxType.OBJECT_PROP_EXPR, + key, + value: flatMapping.inputExpr, + } as ObjectPropExpression); + return currentOutputPropsAST; + } - const currentOutputPropAST = findOrCreateObjectPropExpression(currentOutputPropsAST, key); - let objectExpr: ObjectExpression = currentOutputPropAST.value as ObjectExpression; - const nextOutputPart = flatMapping.outputExpr.parts[i + 1] as ArrayFilterExpression; - if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { - objectExpr = processAllFilter(currentInputAST, currentOutputPropAST); - } else if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) { - objectExpr = processArrayIndexFilter( - currentOutputPropAST, - nextOutputPart.filter as IndexFilterExpression, - ); - } - if ( - objectExpr.type !== SyntaxType.OBJECT_EXPR || - !objectExpr.props || - !Array.isArray(objectExpr.props) - ) { - throw new Error(`Failed to process output mapping: ${flatMapping.output}`); - } - currentOutputPropsAST = objectExpr.props; - } + const currentOutputPropAST = findOrCreateObjectPropExpression(currentOutputPropsAST, key); + const nextOutputPart = flatMapping.outputExpr.parts[partNum + 1]; + const objectExpr = handleNextPart(nextOutputPart, flatMapping.inputExpr, currentOutputPropAST); + if ( + objectExpr.type !== SyntaxType.OBJECT_EXPR || + !objectExpr.props || + !Array.isArray(objectExpr.props) + ) { + throw new Error(`Failed to process output mapping: ${flatMapping.output}`); } + return objectExpr.props; } + /** * Convert Flat to Object Mappings */ @@ -132,7 +140,10 @@ export function convertToObjectMapping(flatMappingAST: FlatMappingAST[]): Object const outputAST: ObjectExpression = CreateObjectExpression(); for (const flatMapping of flatMappingAST) { - processFlatMapping(flatMapping, outputAST); + let currentOutputPropsAST = outputAST.props; + for (let i = 0; i < flatMapping.outputExpr.parts.length; i++) { + currentOutputPropsAST = processFlatMappingPart(flatMapping, i, currentOutputPropsAST); + } } return outputAST; diff --git a/test/scenarios/mappings/all_features.json b/test/scenarios/mappings/all_features.json index 32c049a..01f55b1 100644 --- a/test/scenarios/mappings/all_features.json +++ b/test/scenarios/mappings/all_features.json @@ -1,4 +1,8 @@ [ + { + "input": "$.userId", + "output": "$.user.id" + }, { "input": "$.discount", "output": "$.events[0].items[*].discount" diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index a2f27e1..0c7972d 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -2,6 +2,7 @@ import { PathType } from '../../../src'; import type { Scenario } from '../../types'; const input = { + userId: 'u1', discount: 10, events: ['purchase', 'custom'], products: [ @@ -99,6 +100,9 @@ export const data: Scenario[] = [ revenue: 14.4, }, ], + user: { + id: 'u1', + }, }, }, { From 84aaabfb117b69fe49e9e98961f74f2966a0bb9c Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 4 Jun 2024 09:36:09 +0530 Subject: [PATCH 29/29] feat: add util function convert mappings to template --- src/engine.ts | 4 ++++ test/types.ts | 1 + test/utils/scenario.ts | 20 +++++++++++++------- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index bd6a657..138b305 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -79,6 +79,10 @@ export class JsonTemplateEngine { return translator.translate(expr); } + static convertMappingsToTemplate(mappings: FlatMappingPaths[], options?: EngineOptions): string { + return this.reverseTranslate(this.parseMappingPaths(mappings, options), options); + } + evaluate(data: unknown, bindings: Record = {}): unknown { return this.fn(data ?? {}, bindings); } diff --git a/test/types.ts b/test/types.ts index 32dc802..69ce1ef 100644 --- a/test/types.ts +++ b/test/types.ts @@ -4,6 +4,7 @@ export type Scenario = { description?: string; input?: unknown; templatePath?: string; + template?: string; containsMappings?: true; options?: EngineOptions; bindings?: Record | undefined; diff --git a/test/utils/scenario.ts b/test/utils/scenario.ts index ce104c7..4785181 100644 --- a/test/utils/scenario.ts +++ b/test/utils/scenario.ts @@ -4,19 +4,25 @@ import { FlatMappingPaths, JsonTemplateEngine, PathType } from '../../src'; import { Scenario } from '../types'; export class ScenarioUtils { - static createTemplateEngine(scenarioDir: string, scenario: Scenario): JsonTemplateEngine { + private static initializeScenario(scenarioDir: string, scenario: Scenario) { + scenario.options = scenario.options || {}; + scenario.options.defaultPathType = scenario.options.defaultPathType || PathType.SIMPLE; const templatePath = join(scenarioDir, Scenario.getTemplatePath(scenario)); - let template: string | FlatMappingPaths[] = readFileSync(templatePath, 'utf-8'); + let template: string = readFileSync(templatePath, 'utf-8'); if (scenario.containsMappings) { - template = JSON.parse(template) as FlatMappingPaths[]; + template = JsonTemplateEngine.convertMappingsToTemplate( + JSON.parse(template) as FlatMappingPaths[], + ); } - scenario.options = scenario.options || {}; - scenario.options.defaultPathType = scenario.options.defaultPathType || PathType.SIMPLE; - const newTemplate = JsonTemplateEngine.reverseTranslate( + scenario.template = JsonTemplateEngine.reverseTranslate( JsonTemplateEngine.parse(template, scenario.options), scenario.options, ); - return JsonTemplateEngine.create(newTemplate, scenario.options); + } + + static createTemplateEngine(scenarioDir: string, scenario: Scenario): JsonTemplateEngine { + this.initializeScenario(scenarioDir, scenario); + return JsonTemplateEngine.create(scenario.template as string, scenario.options); } static evaluateScenario(templateEngine: JsonTemplateEngine, scenario: Scenario): any {