Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add validator utils #107

Merged
merged 5 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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;
}
koladilip marked this conversation as resolved.
Show resolved Hide resolved
}

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
koladilip marked this conversation as resolved.
Show resolved Hide resolved
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
}));
}
koladilip marked this conversation as resolved.
Show resolved Hide resolved

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);
}
koladilip marked this conversation as resolved.
Show resolved Hide resolved

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;
}
}
koladilip marked this conversation as resolved.
Show resolved Hide resolved
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 {}
koladilip marked this conversation as resolved.
Show resolved Hide resolved
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
46 changes: 40 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';
koladilip marked this conversation as resolved.
Show resolved Hide resolved
import { EMPTY_EXPR } from '../constants';
import {
SyntaxType,
Expand Down Expand Up @@ -67,10 +68,35 @@

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;
}
koladilip marked this conversation as resolved.
Show resolved Hide resolved

if (
currentOutputPropAST.value.type === SyntaxType.PATH &&
currentOutputPropAST.value.parts.length === 0 &&
currentOutputPropAST.value.root?.type === SyntaxType.OBJECT_EXPR

Check warning on line 86 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

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

View check run for this annotation

Codecov / codecov/patch

src/utils/converter.ts#L86

Added line #L86 was not covered by tests
) {
return currentOutputPropAST.value.root as ObjectExpression;

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

View check run for this annotation

Codecov / codecov/patch

src/utils/converter.ts#L88

Added line #L88 was not covered by tests
}
} 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 +118,11 @@
!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,
);
koladilip marked this conversation as resolved.
Show resolved Hide resolved
}
return objectExpr;
}
Expand All @@ -110,8 +140,10 @@
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,
koladilip marked this conversation as resolved.
Show resolved Hide resolved
);
}
const matchedInputParts = currentInputAST.parts.splice(0, filterIndex);
Expand Down Expand Up @@ -252,8 +284,10 @@

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 290 in src/utils/converter.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/converter.ts#L287-L290

Added lines #L287 - L290 were not covered by tests
koladilip marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
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"
}
]
Loading