diff --git a/src/engine.test.ts b/src/engine.test.ts new file mode 100644 index 0000000..a4ec40d --- /dev/null +++ b/src/engine.test.ts @@ -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'); + }); + }); +}); diff --git a/src/engine.ts b/src/engine.ts index e864c7e..0884faf 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -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 { @@ -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, diff --git a/src/errors.ts b/src/errors.ts deleted file mode 100644 index 95e9638..0000000 --- a/src/errors.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class JsonTemplateLexerError extends Error {} - -export class JsonTemplateParserError extends Error {} - -export class JsonTemplateTranslatorError extends Error {} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..8068227 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,3 @@ +export * from './lexer'; +export * from './parser'; +export * from './translator'; diff --git a/src/errors/lexer.ts b/src/errors/lexer.ts new file mode 100644 index 0000000..25f71c6 --- /dev/null +++ b/src/errors/lexer.ts @@ -0,0 +1 @@ +export class JsonTemplateLexerError extends Error {} diff --git a/src/errors/mapping.ts b/src/errors/mapping.ts new file mode 100644 index 0000000..cec4a6c --- /dev/null +++ b/src/errors/mapping.ts @@ -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; + } +} diff --git a/src/errors/parser.ts b/src/errors/parser.ts new file mode 100644 index 0000000..efcb39d --- /dev/null +++ b/src/errors/parser.ts @@ -0,0 +1 @@ +export class JsonTemplateParserError extends Error {} diff --git a/src/errors/translator.ts b/src/errors/translator.ts new file mode 100644 index 0000000..5e825c2 --- /dev/null +++ b/src/errors/translator.ts @@ -0,0 +1 @@ +export class JsonTemplateTranslatorError extends Error {} diff --git a/src/index.ts b/src/index.ts index 1198776..7b394ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/lexer.ts b/src/lexer.ts index 6f10fae..97a9d58 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -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 = { diff --git a/src/parser.ts b/src/parser.ts index 8943781..54e1ed2 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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, diff --git a/src/translator.ts b/src/translator.ts index 912ed53..1e2b99c 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -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, diff --git a/src/utils/converter.ts b/src/utils/converter.ts index d418114..9a5942c 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import { JsonTemplateMappingError } from '../errors/mapping'; import { EMPTY_EXPR } from '../constants'; import { SyntaxType, @@ -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; } @@ -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); @@ -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, ); } } diff --git a/test/scenarios/mappings/data.ts b/test/scenarios/mappings/data.ts index 4887c6d..ba039a8 100644 --- a/test/scenarios/mappings/data.ts +++ b/test/scenarios/mappings/data.ts @@ -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',