Skip to content

Commit

Permalink
fix: object mappings nested filters (#97)
Browse files Browse the repository at this point in the history
* fix: object mappings nested filters

* fix: object mappings nested filters

* feat: add support for using mappings in templates

* fix: sonar issue

* refactor: convertToObjectMapping

* refactor: convertToObjectMapping

* fix: root mappings

* refactor: mappings converter
  • Loading branch information
koladilip authored Jun 17, 2024
1 parent 8f3df1e commit f7e7bee
Show file tree
Hide file tree
Showing 16 changed files with 389 additions and 95 deletions.
3 changes: 2 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
. "$(dirname -- "$0")/_/husky.sh"

npm test
npx lint-staged
npm run lint:check
npm run lint-staged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"lint:check": "eslint . || exit 1",
"format": "prettier --write '**/*.ts' '**/*.js' '**/*.json'",
"lint": "npm run format && npm run lint:fix",
"lint-staged": "lint-staged",
"prepare": "husky install",
"jest:scenarios": "jest e2e.test.ts --verbose",
"test:scenario": "jest test/scenario.test.ts --verbose",
Expand Down
5 changes: 1 addition & 4 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ export class JsonTemplateEngine {
return translator.translate();
}

private static parseMappingPaths(
mappings: FlatMappingPaths[],
options?: EngineOptions,
): Expression {
static parseMappingPaths(mappings: FlatMappingPaths[], options?: EngineOptions): Expression {
const flatMappingAST = mappings.map((mapping) => ({
...mapping,
inputExpr: JsonTemplateEngine.parse(mapping.input, options).statements[0],
Expand Down
6 changes: 5 additions & 1 deletion src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export class JsonTemplateLexer {
return this.match('{') && this.match('{', 1);
}

matchMappings(): boolean {
return this.match('~m');
}

matchSimplePath(): boolean {
return this.match('~s');
}
Expand Down Expand Up @@ -636,7 +640,7 @@ export class JsonTemplateLexer {
const ch1 = this.codeChars[this.idx];
const ch2 = this.codeChars[this.idx + 1];

if (ch1 === '~' && 'rsj'.includes(ch2)) {
if (ch1 === '~' && 'rsjm'.includes(ch2)) {
this.idx += 2;
return {
type: TokenType.PUNCT,
Expand Down
48 changes: 48 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DefinitionExpression,
EngineOptions,
Expression,
FlatMappingPaths,
FunctionCallExpression,
FunctionExpression,
IncrementExpression,
Expand Down Expand Up @@ -1334,6 +1335,49 @@ export class JsonTemplateParser {
}
}

private static isValidMapping(mapping: ObjectPropExpression): boolean {
return (
typeof mapping.key === 'string' &&
mapping.value.type === SyntaxType.LITERAL &&
mapping.value.tokenType === TokenType.STR
);
}

private static convertMappingsToFlatPaths(mappings: ObjectExpression): FlatMappingPaths {
const flatPaths: Record<string, string> = {};
for (const mappingProp of mappings.props) {
if (!JsonTemplateParser.isValidMapping(mappingProp)) {
throw new JsonTemplateParserError(
`Invalid mapping key=${JSON.stringify(mappingProp.key)} or value=${JSON.stringify(
mappingProp.value,
)}, expected string key and string value`,
);
}
flatPaths[mappingProp.key as string] = mappingProp.value.value;
}
if (!flatPaths.input || !flatPaths.output) {
throw new JsonTemplateParserError(
`Invalid mapping: ${JSON.stringify(flatPaths)}, missing input or output`,
);
}
return flatPaths as FlatMappingPaths;
}

private parseMappings(): Expression {
this.lexer.expect('~m');
const mappings: ArrayExpression = this.parseArrayExpr();
const flatMappings: FlatMappingPaths[] = [];
for (const mapping of mappings.elements) {
if (mapping.type !== SyntaxType.OBJECT_EXPR) {
throw new JsonTemplateParserError(
`Invalid mapping=${JSON.stringify(mapping)}, expected object`,
);
}
flatMappings.push(JsonTemplateParser.convertMappingsToFlatPaths(mapping as ObjectExpression));
}
return JsonTemplateEngine.parseMappingPaths(flatMappings);
}

private parsePrimaryExpr(): Expression {
if (this.lexer.match(';')) {
return EMPTY_EXPR;
Expand Down Expand Up @@ -1382,6 +1426,10 @@ export class JsonTemplateParser {
return this.parsePathTypeExpr();
}

if (this.lexer.matchMappings()) {
return this.parseMappings();
}

if (this.lexer.matchPath()) {
return this.parsePath();
}
Expand Down
115 changes: 81 additions & 34 deletions src/utils/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,17 @@ function processArrayIndexFilter(
}

function processAllFilter(
currentInputAST: PathExpression,
flatMapping: FlatMappingAST,
currentOutputPropAST: ObjectPropExpression,
): Expression {
): ObjectExpression {
const currentInputAST = flatMapping.inputExpr;
const filterIndex = currentInputAST.parts.findIndex(
(part) => part.type === SyntaxType.OBJECT_FILTER_EXPR,
);

if (filterIndex === -1) {
if (currentOutputPropAST.value.type === SyntaxType.OBJECT_EXPR) {
return currentOutputPropAST.value;
return currentOutputPropAST.value as ObjectExpression;
}
} else {
const matchedInputParts = currentInputAST.parts.splice(0, filterIndex + 1);
Expand All @@ -84,7 +85,15 @@ function processAllFilter(
}

const blockExpr = getLastElement(currentOutputPropAST.value.parts) as Expression;
return blockExpr?.statements?.[0] || EMPTY_EXPR;
const objectExpr = blockExpr?.statements?.[0] || EMPTY_EXPR;
if (
objectExpr.type !== SyntaxType.OBJECT_EXPR ||
!objectExpr.props ||
!Array.isArray(objectExpr.props)
) {
throw new Error(`Failed to process output mapping: ${flatMapping.output}`);
}
return objectExpr;
}

function isWildcardSelector(expr: Expression): boolean {
Expand Down Expand Up @@ -146,10 +155,10 @@ function handleNextPart(
flatMapping: FlatMappingAST,
partNum: number,
currentOutputPropAST: ObjectPropExpression,
): Expression {
): ObjectExpression | undefined {
const nextOutputPart = flatMapping.outputExpr.parts[partNum];
if (nextOutputPart.filter?.type === SyntaxType.ALL_FILTER_EXPR) {
return processAllFilter(flatMapping.inputExpr, currentOutputPropAST);
return processAllFilter(flatMapping, currentOutputPropAST);
}
if (nextOutputPart.filter?.type === SyntaxType.ARRAY_INDEX_FILTER_EXPR) {
return processArrayIndexFilter(
Expand All @@ -164,7 +173,32 @@ function handleNextPart(
partNum === flatMapping.outputExpr.parts.length - 1,
);
}
return currentOutputPropAST.value;
}

function handleNextParts(
flatMapping: FlatMappingAST,
partNum: number,
currentOutputPropAST: ObjectPropExpression,
): ObjectExpression {
let objectExpr = currentOutputPropAST.value as ObjectExpression;
let newPartNum = partNum;
while (newPartNum < flatMapping.outputExpr.parts.length) {
const nextObjectExpr = handleNextPart(flatMapping, newPartNum, currentOutputPropAST);
if (!nextObjectExpr) {
break;
}
newPartNum++;
objectExpr = nextObjectExpr;
}
return objectExpr;
}

function isOutputPartRegularSelector(outputPart: Expression) {
return (
outputPart.type === SyntaxType.SELECTOR &&
outputPart.prop?.value &&
outputPart.prop.value !== '*'
);
}

function processFlatMappingPart(
Expand All @@ -173,12 +207,7 @@ function processFlatMappingPart(
currentOutputPropsAST: ObjectPropExpression[],
): ObjectPropExpression[] {
const outputPart = flatMapping.outputExpr.parts[partNum];

if (
outputPart.type !== SyntaxType.SELECTOR ||
!outputPart.prop?.value ||
outputPart.prop.value === '*'
) {
if (!isOutputPartRegularSelector(outputPart)) {
return currentOutputPropsAST;
}
const key = outputPart.prop.value;
Expand All @@ -193,42 +222,60 @@ function processFlatMappingPart(
}

const currentOutputPropAST = findOrCreateObjectPropExpression(currentOutputPropsAST, key);
const objectExpr = handleNextPart(flatMapping, partNum + 1, currentOutputPropAST);
if (
objectExpr.type !== SyntaxType.OBJECT_EXPR ||
!objectExpr.props ||
!Array.isArray(objectExpr.props)
) {
throw new Error(`Failed to process output mapping: ${flatMapping.output}`);
}
const objectExpr = handleNextParts(flatMapping, partNum + 1, currentOutputPropAST);
return objectExpr.props;
}

function processFlatMapping(flatMapping, currentOutputPropsAST) {
if (flatMapping.outputExpr.parts.length === 0) {
currentOutputPropsAST.push({
type: SyntaxType.OBJECT_PROP_EXPR,
value: {
type: SyntaxType.SPREAD_EXPR,
value: flatMapping.inputExpr,
},
} as ObjectPropExpression);
return;
function handleRootOnlyOutputMapping(flatMapping: FlatMappingAST, outputAST: ObjectExpression) {
outputAST.props.push({
type: SyntaxType.OBJECT_PROP_EXPR,
value: {
type: SyntaxType.SPREAD_EXPR,
value: flatMapping.inputExpr,
},
} as ObjectPropExpression);
}

function validateMapping(flatMapping: FlatMappingAST) {
if (flatMapping.outputExpr.type !== SyntaxType.PATH) {
throw new Error(
`Invalid object mapping: output=${flatMapping.output} should be a path expression`,
);
}
}

function processFlatMappingParts(flatMapping: FlatMappingAST, objectExpr: ObjectExpression) {
let currentOutputPropsAST = objectExpr.props;
for (let i = 0; i < flatMapping.outputExpr.parts.length; i++) {
currentOutputPropsAST = processFlatMappingPart(flatMapping, i, currentOutputPropsAST);
}
}

/**
* Convert Flat to Object Mappings
*/
export function convertToObjectMapping(
flatMappingASTs: FlatMappingAST[],
): ObjectExpression | PathExpression {
const outputAST: ObjectExpression = createObjectExpression();
let pathAST: PathExpression | undefined;
for (const flatMapping of flatMappingASTs) {
processFlatMapping(flatMapping, outputAST.props);
validateMapping(flatMapping);
let objectExpr = outputAST;
if (flatMapping.outputExpr.parts.length > 0) {
if (!isOutputPartRegularSelector(flatMapping.outputExpr.parts[0])) {
const objectPropExpr = {
type: SyntaxType.OBJECT_PROP_EXPR,
key: '',
value: objectExpr as Expression,
};
objectExpr = handleNextParts(flatMapping, 0, objectPropExpr);
pathAST = objectPropExpr.value as PathExpression;
}
processFlatMappingParts(flatMapping, objectExpr);
} else {
handleRootOnlyOutputMapping(flatMapping, outputAST);
}
}

return outputAST;
return pathAST ?? outputAST;
}
9 changes: 3 additions & 6 deletions test/scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@ describe(`${scenarioName}:`, () => {
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 () => {
const templatePath = Scenario.getTemplatePath(scenario);
it(`Scenario ${index}: ${templatePath}`, async () => {
let result;
try {
console.log(
`Executing scenario: ${scenarioName}, test: ${index}, template: ${
scenario.templatePath || 'template.jt'
}`,
);
console.log(`Executing scenario: ${scenarioName}, test: ${index}, template: ${templatePath}`);
result = await ScenarioUtils.evaluateScenario(scenarioDir, scenario);
expect(result).toEqual(scenario.output);
} catch (error: any) {
Expand Down
Loading

0 comments on commit f7e7bee

Please sign in to comment.