diff --git a/src/lexer.ts b/src/lexer.ts index 37444ff..307ccf0 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -93,7 +93,7 @@ export class JsonTemplateLexer { return this.matchPathSelector() || this.matchID(); } - matchObjectWildCardPropValue(): boolean { + matchObjectContextProp(): boolean { return this.match('@') && this.matchID(1); } diff --git a/src/parser.ts b/src/parser.ts index e18b91b..f82d587 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -25,7 +25,6 @@ import { ObjectExpression, ObjectFilterExpression, ObjectPropExpression, - ObjectWildcardValueExpression, OperatorType, PathExpression, PathOptions, @@ -1088,18 +1087,6 @@ export class JsonTemplateParser { }; } - private parseObjectPropWildcardValueExpr(): ObjectWildcardValueExpression { - const idToken = this.lexer.lookahead(1); - if (!['key', 'value'].includes(idToken.value)) { - throw new JsonTemplateParserError(`Invalid object wildcard prop value @${idToken.value}`); - } - this.lexer.ignoreTokens(2); - return { - type: SyntaxType.OBJECT_PROP_WILD_CARD_VALUE_EXPR, - value: idToken.value, - }; - } - private parseObjectKeyExpr(): Expression | string { let key: Expression | string; if (this.lexer.match('[')) { @@ -1109,9 +1096,7 @@ export class JsonTemplateParser { } else if (this.lexer.matchID() || this.lexer.matchKeyword()) { key = this.lexer.value(); } else if (this.lexer.matchLiteral() && !this.lexer.matchTokenType(TokenType.REGEXP)) { - key = this.parseLiteralExpr(); - } else if (this.lexer.matchObjectWildCardPropValue()) { - key = this.parseObjectPropWildcardValueExpr(); + key = this.lexer.value().toString(); } else { this.lexer.throwUnexpectedToken(); } @@ -1142,19 +1127,26 @@ export class JsonTemplateParser { } } - private static isWildcardPropKey(expr: any): boolean { - return typeof expr === 'object' && expr?.type === SyntaxType.OBJECT_PROP_WILD_CARD_VALUE_EXPR; + private getObjectPropContextVar(): string | undefined { + if (this.lexer.matchObjectContextProp()) { + this.lexer.ignoreTokens(1); + return this.lexer.value(); + } } private parseNormalObjectPropExpr(): ObjectPropExpression { + const contextVar = this.getObjectPropContextVar(); const key = this.parseObjectKeyExpr(); + if (contextVar && typeof key === 'string') { + throw new JsonTemplateParserError('Context prop should be used with a key expression'); + } this.lexer.expect(':'); const value = this.parseBaseExpr(); return { type: SyntaxType.OBJECT_PROP_EXPR, key, value, - wildcard: JsonTemplateParser.isWildcardPropKey(key), + contextVar, }; } @@ -1390,9 +1382,6 @@ export class JsonTemplateParser { return this.parsePathTypeExpr(); } - if (this.lexer.matchObjectWildCardPropValue()) { - return this.parseObjectPropWildcardValueExpr() as Expression; - } if (this.lexer.matchPath()) { return this.parsePath(); } diff --git a/src/reverse_translator.ts b/src/reverse_translator.ts index a2e8b78..4699539 100644 --- a/src/reverse_translator.ts +++ b/src/reverse_translator.ts @@ -19,7 +19,6 @@ import { ObjectExpression, ObjectFilterExpression, ObjectPropExpression, - ObjectWildcardValueExpression, PathExpression, PathOptions, PathType, @@ -111,19 +110,11 @@ export class JsonTemplateReverseTranslator { return this.translateArrayIndexFilterExpression(expr as IndexFilterExpression); case SyntaxType.RANGE_FILTER_EXPR: return this.translateRangeFilterExpression(expr as RangeFilterExpression); - case SyntaxType.OBJECT_PROP_WILD_CARD_VALUE_EXPR: - return this.translateWildcardObjectPropValueExpression( - expr as ObjectWildcardValueExpression, - ); default: return ''; } } - translateWildcardObjectPropValueExpression(expr: ObjectWildcardValueExpression): string { - return `@${expr.value}`; - } - translateArrayFilterExpression(expr: ArrayFilterExpression): string { return this.translateExpression(expr.filter); } @@ -446,14 +437,12 @@ export class JsonTemplateReverseTranslator { translateObjectPropExpression(expr: ObjectPropExpression): string { const code: string[] = []; + if (expr.contextVar) { + code.push(`@${expr.contextVar} `); + } if (expr.key) { if (typeof expr.key === 'string') { - code.push(expr.key); - } else if ( - expr.key.type === SyntaxType.LITERAL || - expr.key.type === SyntaxType.OBJECT_PROP_WILD_CARD_VALUE_EXPR - ) { - code.push(this.translateExpression(expr.key)); + code.push(escapeStr(expr.key)); } else { code.push(this.translateWithWrapper(expr.key, '[', ']')); } diff --git a/src/translator.ts b/src/translator.ts index a81652f..9d6dfce 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -40,7 +40,6 @@ import { IncrementExpression, LoopControlExpression, ObjectPropExpression, - ObjectWildcardValueExpression, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; import { translateLiteral } from './utils/translator'; @@ -190,13 +189,6 @@ export class JsonTemplateTranslator { case SyntaxType.THROW_EXPR: return this.translateThrowExpr(expr as ThrowExpression, dest, ctx); - case SyntaxType.OBJECT_PROP_WILD_CARD_VALUE_EXPR: - return this.translateObjectWildcardValueExpr( - expr as ObjectWildcardValueExpression, - dest, - ctx, - ); - default: return ''; } @@ -542,28 +534,27 @@ export class JsonTemplateTranslator { return code.join(''); } - private translateObjectWildcardValueExpr( - expr: ObjectWildcardValueExpression, - dest: string, - _ctx: string, - ): string { - return JsonTemplateTranslator.generateAssignmentCode(dest, expr.value); - } - - private translateObjectWildcardProp( + private translateObjectContextProp( expr: ObjectPropExpression, dest: string, ctx: string, vars: string[] = [], ): string { const code: string[] = []; - const keyExpr = expr.key as ObjectWildcardValueExpression; code.push(JsonTemplateTranslator.generateAssignmentCode(dest, '{}')); + const keyVar = this.acquireVar(); const valueVar = this.acquireVar(); - vars.push(valueVar); - code.push(`for(let [key, value] of Object.entries(${ctx})){`); + vars.push(keyVar, valueVar); + code.push(`for(let [${VARS_PREFIX}key, ${VARS_PREFIX}value] of Object.entries(${ctx})){`); + code.push( + JsonTemplateTranslator.generateAssignmentCode( + expr.contextVar as string, + `{key:${VARS_PREFIX}key,value:${VARS_PREFIX}value}`, + ), + ); + code.push(this.translateExpr(expr.key as Expression, keyVar, ctx)); code.push(this.translateExpr(expr.value, valueVar, ctx)); - code.push(`${dest}[${keyExpr.value}] = ${valueVar};`); + code.push(`${dest}[${keyVar}] = ${valueVar};`); code.push('}'); return code.join(''); } @@ -574,10 +565,10 @@ export class JsonTemplateTranslator { const vars: string[] = []; for (const prop of expr.props) { const propParts: string[] = []; - if (prop.wildcard) { - const wildCardPropVar = this.acquireVar(); - code.push(this.translateObjectWildcardProp(prop, wildCardPropVar, ctx)); - propExprs.push(`...${wildCardPropVar}`); + if (prop.contextVar) { + const propWithContextVar = this.acquireVar(); + code.push(this.translateObjectContextProp(prop, propWithContextVar, ctx)); + propExprs.push(`...${propWithContextVar}`); } else { if (prop.key) { if (typeof prop.key !== 'string') { @@ -586,7 +577,7 @@ export class JsonTemplateTranslator { propParts.push(`[${keyVar}]`); vars.push(keyVar); } else { - propParts.push(prop.key); + propParts.push(escapeStr(prop.key)); } propParts.push(':'); } diff --git a/src/types.ts b/src/types.ts index 49c9ca3..b2d6d37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,7 +84,6 @@ export enum SyntaxType { ARRAY_FILTER_EXPR = 'array_filter_expr', DEFINITION_EXPR = 'definition_expr', ASSIGNMENT_EXPR = 'assignment_expr', - OBJECT_PROP_WILD_CARD_VALUE_EXPR = 'object_prop_wild_card_value_expr', OBJECT_PROP_EXPR = 'object_prop_expr', OBJECT_EXPR = 'object_expr', ARRAY_EXPR = 'array_expr', @@ -148,14 +147,10 @@ export interface BlockExpression extends Expression { statements: Expression[]; } -export interface ObjectWildcardValueExpression extends Expression { - value: string; -} - export interface ObjectPropExpression extends Expression { key?: Expression | string; value: Expression; - wildcard?: boolean; + contextVar?: string; } export interface ObjectExpression extends Expression { diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 6e37276..caef8ab 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import { EMPTY_EXPR } from '../constants'; import { SyntaxType, PathExpression, @@ -9,7 +10,7 @@ import { Expression, IndexFilterExpression, BlockExpression, - ObjectWildcardValueExpression, + TokenType, } from '../types'; import { createBlockExpression, getLastElement } from './common'; @@ -58,42 +59,38 @@ function processArrayIndexFilter( function processAllFilter( currentInputAST: PathExpression, currentOutputPropAST: ObjectPropExpression, -): ObjectExpression { +): Expression { const filterIndex = currentInputAST.parts.findIndex( (part) => part.type === SyntaxType.OBJECT_FILTER_EXPR, ); if (filterIndex === -1) { - return currentOutputPropAST.value as ObjectExpression; - } - const matchedInputParts = currentInputAST.parts.splice(0, filterIndex + 1); - if (currentOutputPropAST.value.type !== SyntaxType.PATH) { - matchedInputParts.push(createBlockExpression(currentOutputPropAST.value)); - currentOutputPropAST.value = { - type: SyntaxType.PATH, - root: currentInputAST.root, - pathType: currentInputAST.pathType, - parts: matchedInputParts, - returnAsArray: true, - } as PathExpression; + if (currentOutputPropAST.value.type === SyntaxType.OBJECT_EXPR) { + return currentOutputPropAST.value; + } + } else { + const matchedInputParts = currentInputAST.parts.splice(0, filterIndex + 1); + if (currentOutputPropAST.value.type !== SyntaxType.PATH) { + matchedInputParts.push(createBlockExpression(currentOutputPropAST.value)); + currentOutputPropAST.value = { + type: SyntaxType.PATH, + root: currentInputAST.root, + pathType: currentInputAST.pathType, + parts: matchedInputParts, + returnAsArray: true, + } as PathExpression; + } + currentInputAST.root = undefined; } - currentInputAST.root = undefined; - const blockExpr = getLastElement(currentOutputPropAST.value.parts) as BlockExpression; - return blockExpr.statements[0] as ObjectExpression; + const blockExpr = getLastElement(currentOutputPropAST.value.parts) as Expression; + return blockExpr?.statements?.[0] || EMPTY_EXPR; } function isWildcardSelector(expr: Expression): boolean { return expr.type === SyntaxType.SELECTOR && expr.prop?.value === '*'; } -function createWildcardObjectPropValueExpression(value: string): ObjectWildcardValueExpression { - return { - type: SyntaxType.OBJECT_PROP_WILD_CARD_VALUE_EXPR, - value, - }; -} - function processWildCardSelector( flatMapping: FlatMappingAST, currentOutputPropAST: ObjectPropExpression, @@ -118,16 +115,29 @@ function processWildCardSelector( parts: matchedInputParts, } as PathExpression; } - currentInputAST.root = createWildcardObjectPropValueExpression('value'); + currentInputAST.root = 'e.value'; const blockExpr = getLastElement(currentOutputPropAST.value.parts) as BlockExpression; const blockObjectExpr = blockExpr.statements[0] as ObjectExpression; const objectExpr = createObjectExpression(); blockObjectExpr.props.push({ type: SyntaxType.OBJECT_PROP_EXPR, - key: createWildcardObjectPropValueExpression('key'), + key: { + type: SyntaxType.PATH, + root: 'e', + parts: [ + { + type: SyntaxType.SELECTOR, + selector: '.', + prop: { + type: TokenType.ID, + value: 'key', + }, + }, + ], + }, value: isLastPart ? currentInputAST : objectExpr, - wildcard: true, + contextVar: 'e', }); return objectExpr; } @@ -136,7 +146,7 @@ function handleNextPart( flatMapping: FlatMappingAST, partNum: number, currentOutputPropAST: ObjectPropExpression, -): ObjectExpression { +): Expression { const nextOutputPart = flatMapping.outputExpr.parts[partNum]; if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) { return processAllFilter(flatMapping.inputExpr, currentOutputPropAST); @@ -154,7 +164,7 @@ function handleNextPart( partNum === flatMapping.outputExpr.parts.length - 1, ); } - return currentOutputPropAST.value as ObjectExpression; + return currentOutputPropAST.value; } function processFlatMappingPart( diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index abb8df8..921333a 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -4,6 +4,7 @@ import type { Scenario } from '../../types'; const input = { userId: 'u1', discount: 10, + coupon: 'DISCOUNT', events: ['purchase', 'custom'], context: { traits: { @@ -185,17 +186,20 @@ export const data: Scenario[] = [ product_name: 'p1', product_category: 'baby', discount: 10, + coupon_code: 'DISCOUNT', }, { product_id: 2, product_name: 'p2', discount: 10, + coupon_code: 'DISCOUNT', }, { product_id: 3, product_name: 'p3', product_category: 'home', discount: 10, + coupon_code: 'DISCOUNT', }, ], }, diff --git a/test/scenarios/mappings/mappings_with_root_fields.json b/test/scenarios/mappings/mappings_with_root_fields.json index 1e80449..a3000b1 100644 --- a/test/scenarios/mappings/mappings_with_root_fields.json +++ b/test/scenarios/mappings/mappings_with_root_fields.json @@ -11,6 +11,10 @@ "input": "$.products[*].name", "output": "$.items[*].product_name" }, + { + "input": "$.coupon", + "output": "$.items[*].coupon_code" + }, { "input": "$.products[*].category", "output": "$.items[*].product_category" diff --git a/test/scenarios/objects/context_props.jt b/test/scenarios/objects/context_props.jt new file mode 100644 index 0000000..f0e0a47 --- /dev/null +++ b/test/scenarios/objects/context_props.jt @@ -0,0 +1,16 @@ +{ + user: { + props: .traits.({ + @e [e.key]: { + value: e.value + }, + someKey: { + value: 'someValue' + } + }), + events: .events.({ + @e [e.value]: e.key, + someEventValue: 'someEventName' + }) + } +} \ No newline at end of file diff --git a/test/scenarios/objects/data.ts b/test/scenarios/objects/data.ts index 0c94522..7092096 100644 --- a/test/scenarios/objects/data.ts +++ b/test/scenarios/objects/data.ts @@ -2,18 +2,7 @@ import { Scenario } from '../../types'; export const data: Scenario[] = [ { - templatePath: 'invalid_wild_cards.jt', - error: 'Invalid object wildcard prop value', - }, - { - output: { - a: 1, - b: 2, - d: 3, - }, - }, - { - templatePath: 'wild_cards.jt', + templatePath: 'context_props.jt', input: { traits: { name: 'John Doe', @@ -33,12 +22,27 @@ export const data: Scenario[] = [ age: { value: 30, }, + someKey: { + value: 'someValue', + }, }, events: { bar: 'foo', 'something else': 'something', + someEventValue: 'someEventName', }, }, }, }, + { + templatePath: 'invalid_context_prop.jt', + error: 'Context prop should be used with a key expression', + }, + { + output: { + a: 1, + b: 2, + d: 3, + }, + }, ]; diff --git a/test/scenarios/objects/invalid_wild_cards.jt b/test/scenarios/objects/invalid_context_prop.jt similarity index 81% rename from test/scenarios/objects/invalid_wild_cards.jt rename to test/scenarios/objects/invalid_context_prop.jt index 2101b68..7b76602 100644 --- a/test/scenarios/objects/invalid_wild_cards.jt +++ b/test/scenarios/objects/invalid_context_prop.jt @@ -1,7 +1,7 @@ { user: { props: .traits.({ - @foo: { + @e key: { value: @bar } }) diff --git a/test/scenarios/objects/wild_cards.jt b/test/scenarios/objects/wild_cards.jt deleted file mode 100644 index b1b5c3e..0000000 --- a/test/scenarios/objects/wild_cards.jt +++ /dev/null @@ -1,12 +0,0 @@ -{ - user: { - props: .traits.({ - @key: { - value: @value - } - }), - events: .events.({ - @value: @key - }) - } -} \ No newline at end of file