Skip to content

Commit

Permalink
feat: add validator utils
Browse files Browse the repository at this point in the history
  • Loading branch information
koladilip committed Jun 21, 2024
1 parent cb50968 commit a6fc5d9
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 24 deletions.
69 changes: 69 additions & 0 deletions src/engine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { JsonTemplateEngine } from './engine';

describe('engine', () => {
describe('isValidJSONPath', () => {
it('should return true for valid JSON root path', () => {
expect(JsonTemplateEngine.isValidJSONPath('$.user.name')).toBeTruthy();
});

it('should return true for valid JSON relative path', () => {
expect(JsonTemplateEngine.isValidJSONPath('.user.name')).toBeTruthy();

expect(JsonTemplateEngine.isValidJSONPath('@.user.name')).toBeTruthy();
});

it('should return false for invalid JSON path', () => {
expect(JsonTemplateEngine.isValidJSONPath('userId')).toBeFalsy();
});

it('should return false for invalid template', () => {
expect(JsonTemplateEngine.isValidJSONPath('a=')).toBeFalsy();
});

it('should return false for empty path', () => {
expect(JsonTemplateEngine.isValidJSONPath('')).toBeFalsy();
});
});
describe('validateMappings', () => {
it('should validate mappings', () => {
expect(() =>
JsonTemplateEngine.validateMappings([
{
input: '$.userId',
output: '$.user.id',
},
{
input: '$.discount',
output: '$.events[0].items[*].discount',
},
]),
).not.toThrow();
});

it('should throw error for mappings which are not compatible with each other', () => {
expect(() =>
JsonTemplateEngine.validateMappings([
{
input: '$.events[0]',
output: '$.events[0].name',
},
{
input: '$.discount',
output: '$.events[0].name[*].discount',
},
]),
).toThrowError('Invalid mapping');
});

it('should throw error for mappings with invalid json paths', () => {
expect(() =>
JsonTemplateEngine.validateMappings([
{
input: 'events[0]',
output: 'events[0].name',
},
]),
).toThrowError('Invalid mapping');
});
});
});
55 changes: 48 additions & 7 deletions src/engine.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
/* eslint-disable import/no-cycle */
import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants';
import { JsonTemplateMappingError } from './errors/mapping';
import { JsonTemplateLexer } from './lexer';
import { JsonTemplateParser } from './parser';
import { JsonTemplateReverseTranslator } from './reverse_translator';
import { JsonTemplateTranslator } from './translator';
import { EngineOptions, Expression, FlatMappingPaths, TemplateInput } from './types';
import {
EngineOptions,
Expression,
FlatMappingPaths,
PathType,
SyntaxType,
TemplateInput,
} from './types';
import { CreateAsyncFunction, convertToObjectMapping, isExpression } from './utils';

export class JsonTemplateEngine {
Expand Down Expand Up @@ -36,13 +44,46 @@ export class JsonTemplateEngine {
return translator.translate();
}

static isValidJSONPath(path: string = ''): boolean {
try {
const expression = JsonTemplateEngine.parse(path, { defaultPathType: PathType.JSON });
const statement = expression.statements?.[0];
return (
statement &&
statement.type === SyntaxType.PATH &&
(!statement.root || statement.root === DATA_PARAM_KEY)
);
} catch (e) {
return false;
}
}

private static prepareMappings(mappings: FlatMappingPaths[]): FlatMappingPaths[] {
return mappings.map((mapping) => ({
...mapping,
input: mapping.input ?? mapping.from,

Check warning on line 64 in src/engine.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
output: mapping.output ?? mapping.to,

Check warning on line 65 in src/engine.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}));
}

static validateMappings(mappings: FlatMappingPaths[]) {
JsonTemplateEngine.prepareMappings(mappings).forEach((mapping) => {
if (
!JsonTemplateEngine.isValidJSONPath(mapping.input) ||
!JsonTemplateEngine.isValidJSONPath(mapping.output)
) {
throw new JsonTemplateMappingError(
'Invalid mapping',
mapping.input as string,
mapping.output as string,
);
}
});
JsonTemplateEngine.parseMappingPaths(mappings);
}

static parseMappingPaths(mappings: FlatMappingPaths[], options?: EngineOptions): Expression {
const flatMappingAST = mappings
.map((mapping) => ({
...mapping,
input: mapping.input ?? mapping.from,
output: mapping.output ?? mapping.to,
}))
const flatMappingAST = JsonTemplateEngine.prepareMappings(mappings)
.filter((mapping) => mapping.input && mapping.output)
.map((mapping) => ({
...mapping,
Expand Down
5 changes: 0 additions & 5 deletions src/errors.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './lexer';
export * from './parser';
export * from './translator';
1 change: 1 addition & 0 deletions src/errors/lexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class JsonTemplateLexerError extends Error {}
11 changes: 11 additions & 0 deletions src/errors/mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class JsonTemplateMappingError extends Error {
inputMapping: string;

outputMapping: string;

constructor(message: string, inputMapping: string, outputMapping: string) {
super(`${message}. Input: ${inputMapping}, Output: ${outputMapping}`);
this.inputMapping = inputMapping;
this.outputMapping = outputMapping;
}
}
1 change: 1 addition & 0 deletions src/errors/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class JsonTemplateParserError extends Error {}
1 change: 1 addition & 0 deletions src/errors/translator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class JsonTemplateTranslatorError extends Error {}
2 changes: 1 addition & 1 deletion src/lexer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { VARS_PREFIX } from './constants';
import { JsonTemplateLexerError } from './errors';
import { JsonTemplateLexerError } from './errors/lexer';
import { Keyword, Token, TokenType } from './types';

const MESSAGES = {
Expand Down
3 changes: 2 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* 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';
import { JsonTemplateParserError } from './errors/parser';
import { JsonTemplateLexerError } from './errors/lexer';
import { JsonTemplateLexer } from './lexer';
import {
ArrayExpression,
Expand Down
2 changes: 1 addition & 1 deletion src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
RESULT_KEY,
VARS_PREFIX,
} from './constants';
import { JsonTemplateTranslatorError } from './errors';
import { JsonTemplateTranslatorError } from './errors/translator';
import { binaryOperators, isStandardFunction, standardFunctions } from './operators';
import {
ArrayExpression,
Expand Down
45 changes: 39 additions & 6 deletions src/utils/converter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign */
import { JsonTemplateMappingError } from '../errors/mapping';
import { EMPTY_EXPR } from '../constants';
import {
SyntaxType,
Expand Down Expand Up @@ -67,10 +68,34 @@ function processAllFilter(

if (filterIndex === -1) {
if (currentOutputPropAST.value.type === SyntaxType.OBJECT_EXPR) {
return currentOutputPropAST.value as ObjectExpression;
const currObjectExpr = currentOutputPropAST.value as ObjectExpression;
currentOutputPropAST.value = {
type: SyntaxType.PATH,
root: currObjectExpr,
pathType: currentInputAST.pathType,
inferredPathType: currentInputAST.inferredPathType,
parts: [],
returnAsArray: true,
} as PathExpression;
return currObjectExpr;
}

if (
currentOutputPropAST.value.type === SyntaxType.PATH &&
currentOutputPropAST.value.root?.type === SyntaxType.OBJECT_EXPR
) {
return currentOutputPropAST.value.root as ObjectExpression;

Check warning on line 87 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 87 in src/utils/converter.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/converter.ts#L87

Added line #L87 was not covered by tests
}

Check warning on line 88 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
} else {
const matchedInputParts = currentInputAST.parts.splice(0, filterIndex + 1);
if (
currentOutputPropAST.value.type === SyntaxType.PATH &&
currentOutputPropAST.value.parts.length === 0 &&
currentOutputPropAST.value.root?.type === SyntaxType.OBJECT_EXPR
) {
currentOutputPropAST.value = currentOutputPropAST.value.root;
}

if (currentOutputPropAST.value.type !== SyntaxType.PATH) {
matchedInputParts.push(createBlockExpression(currentOutputPropAST.value));
currentOutputPropAST.value = {
Expand All @@ -92,7 +117,11 @@ function processAllFilter(
!objectExpr.props ||
!Array.isArray(objectExpr.props)
) {
throw new Error(`Failed to process output mapping: ${flatMapping.output}`);
throw new JsonTemplateMappingError(
'Invalid mapping',
flatMapping.input as string,
flatMapping.output as string,
);
}
return objectExpr;
}
Expand All @@ -110,8 +139,10 @@ function processWildCardSelector(
const filterIndex = currentInputAST.parts.findIndex(isWildcardSelector);

if (filterIndex === -1) {
throw new Error(
`Invalid object mapping: input=${flatMapping.input} and output=${flatMapping.output}`,
throw new JsonTemplateMappingError(
'Invalid mapping',
flatMapping.input as string,
flatMapping.output as string,
);
}
const matchedInputParts = currentInputAST.parts.splice(0, filterIndex);
Expand Down Expand Up @@ -252,8 +283,10 @@ function handleRootOnlyOutputMapping(flatMapping: FlatMappingAST, outputAST: Obj

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

Check warning on line 286 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
'Invalid mapping: should be a path expression',

Check warning on line 287 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
flatMapping.input as string,

Check warning on line 288 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
flatMapping.output as string,

Check warning on line 289 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 289 in src/utils/converter.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/converter.ts#L286-L289

Added lines #L286 - L289 were not covered by tests
);
}

Check warning on line 291 in src/utils/converter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
Expand Down
4 changes: 4 additions & 0 deletions test/scenarios/mappings/all_features.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"input": "$.products[?(@.category)].id",
"output": "$.events[0].items[*].product_id"
},
{
"input": "$.coupon",
"output": "$.events[0].items[*].coupon_code"
},
{
"input": "$.events[0]",
"output": "$.events[0].name"
Expand Down
23 changes: 20 additions & 3 deletions test/scenarios/mappings/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const data: Scenario[] = [
{
discount: 10,
product_id: 1,
coupon_code: 'DISCOUNT',
product_name: 'p1',
product_category: 'baby',
options: [
Expand All @@ -92,6 +93,7 @@ export const data: Scenario[] = [
{
discount: 10,
product_id: 3,
coupon_code: 'DISCOUNT',
product_name: 'p3',
product_category: 'home',
options: [
Expand Down Expand Up @@ -150,15 +152,15 @@ export const data: Scenario[] = [
},
{
mappingsPath: 'invalid_array_mappings.json',
error: 'Failed to process output mapping',
error: 'Invalid mapping',
},
{
mappingsPath: 'invalid_object_mappings.json',
error: 'Invalid object mapping',
error: 'Invalid mapping',
},
{
mappingsPath: 'invalid_output_mapping.json',
error: 'Invalid object mapping',
error: 'Invalid mapping',
},
{
mappingsPath: 'mappings_with_root_fields.json',
Expand Down Expand Up @@ -399,6 +401,21 @@ export const data: Scenario[] = [
},
},
},
{
mappingsPath: 'simple_array_mappings.json',
input: {
user_id: 1,
user_name: 'John Doe',
},
output: {
users: [
{
id: 1,
name: 'John Doe',
},
],
},
},
{
input: {
a: [
Expand Down
10 changes: 10 additions & 0 deletions test/scenarios/mappings/simple_array_mappings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"input": "$.user_id",
"output": "$.users[*].id"
},
{
"input": "$.user_name",
"output": "$.users[*].name"
}
]

0 comments on commit a6fc5d9

Please sign in to comment.