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 6d452fb
Show file tree
Hide file tree
Showing 14 changed files with 154 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,
output: mapping.output ?? mapping.to,
}));
}

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 {}
9 changes: 9 additions & 0 deletions src/errors/mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './constants';
export * from './engine';
export * from './errors';
export * from './errors/';
export * from './lexer';
export * from './operators';
export * from './parser';
Expand Down
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
19 changes: 14 additions & 5 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 @@ -92,7 +93,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 +115,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 +259,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(
'Invalid mapping: should be a path expression',
flatMapping.input as string,
flatMapping.output as string,

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

View check run for this annotation

Codecov / codecov/patch

src/utils/converter.ts#L262-L265

Added lines #L262 - L265 were not covered by tests
);
}
}
Expand Down
6 changes: 3 additions & 3 deletions test/scenarios/mappings/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,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

0 comments on commit 6d452fb

Please sign in to comment.