Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 2 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "asl-path-validator",
"version": "1.0.0",
"version": "0.17.0",
"description": "Validates the path expressions for the Amazon States Language",
"main": "./dist/index.js",
"scripts": {
Expand Down Expand Up @@ -50,6 +50,5 @@
"typescript": "^4.7.4"
},
"dependencies": {
"jsonata": "^2.1.0"
}
}
66 changes: 26 additions & 40 deletions src/__tests__/ajv.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Ajv from "ajv";
import Ajv, { type ErrorObject } from "ajv";
import example from "./json/example-schema.json";
import payloadTemplateSchema from "./json/payload-template.json";
import fs from "fs";
Expand All @@ -11,10 +11,7 @@ describe("tests for the ajv custom formatters", () => {

beforeAll(() => {
ajv = new Ajv({
schemas: [
{ ...example, $async: true },
{ ...payloadTemplateSchema, $async: true },
],
schemas: [example, payloadTemplateSchema],
allowUnionTypes: true,
});
registerAll(ajv);
Expand All @@ -41,18 +38,17 @@ describe("tests for the ajv custom formatters", () => {
},
];

it.each(valid_inputs)("$label", async (inputWithLabel) => {
it.each(valid_inputs)("$label", (inputWithLabel) => {
expect.hasAssertions();
must(ajv);
const { label, ...input } = inputWithLabel;
const validator = ajv.getSchema(
"https://asl-path-validator.cloud/example.json#"
const result = ajv.validate(
"https://asl-path-validator.cloud/example.json#",
input
);
must(validator);
const result = await validator(input);
expect(label).toBeTruthy();
expect(ajv.errors ?? []).toStrictEqual([]);
expect(result).toBeTruthy();
expect(result).toBe(true);
});

const invalid_shapes: Array<{
Expand Down Expand Up @@ -82,7 +78,7 @@ describe("tests for the ajv custom formatters", () => {
"dynamic.path1.$": "not a valid path",
static2: "ok",
},
label: "field matching path pattern doesn't have a valid path",
label: "field matching path pattern doesn't have a valud path",
},
{
Parameters: {
Expand All @@ -108,32 +104,22 @@ describe("tests for the ajv custom formatters", () => {
},
];

it.each(invalid_shapes)(
"$label should be rejected",
async (inputWithLabel) => {
expect.hasAssertions();
must(ajv);
const { label, ...input } = inputWithLabel;
expect(label).toBeTruthy();
const inputFields = Object.keys(input);
expect(inputFields).toHaveLength(1);
const validator = ajv.getSchema(
"https://asl-path-validator.cloud/example.json#"
);
must(validator);
try {
await validator({ ...input, Type: "Example" });
fail("expected validation to fail");
} catch (e: unknown) {
expect(e).toBeTruthy();
}
// const instancePath: string = JSONPath({
// json: ajv,
// path: "$.errors.[0].instancePath",
// wrap: false,
// });
//
// expect(instancePath.split("/")[1]).toStrictEqual(inputFields[0]);
}
);
it.each(invalid_shapes)("$label should be rejected", (inputWithLabel) => {
expect.hasAssertions();
must(ajv);
const { label, ...input } = inputWithLabel;
expect(label).toBeTruthy();
const inputFields = Object.keys(input);
expect(inputFields).toHaveLength(1);
const result = ajv.validate(
"https://asl-path-validator.cloud/example.json#",
{ ...input, Type: "Example" }
);
expect(result).toBe(false);
const instancePath: string = (
(ajv.errors as ErrorObject[])[0] as ErrorObject
).instancePath;

expect(instancePath.split("/")[1]).toStrictEqual(inputFields[0]);
});
});
4 changes: 2 additions & 2 deletions src/__tests__/validatePath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,10 @@ describe("unit tests for the parser", () => {
describe("valid paths", () => {
it.each(toInput())(
"$path as $context expected: $expected_outcome",
async ({ path, context, expected_outcome }) => {
({ path, context, expected_outcome }) => {
expect.hasAssertions();
must(context);
const result = await validatePath(path, context);
const result = validatePath(path, context);
if (!result.isValid && expected_outcome) {
// gets a better error message
expect(result.message).toBeFalsy();
Expand Down
47 changes: 22 additions & 25 deletions src/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ export const registerAll = (
ajv: Ajv,
config = AslPathValidatorConfig
): void => {
const validateAdapter = async (
path: string,
pathType: AslPathContext
): Promise<boolean> => {
const result = await validatePath(path, pathType);
const validateAdapter = (path: string, pathType: AslPathContext): boolean => {
const result = validatePath(path, pathType);
if (!config.silent && !result.isValid) {
ajv.logger.error(
`asl_path_validator: code:${result.code}. pathType:${pathType}. input: ${path}`
Expand All @@ -29,31 +26,31 @@ export const registerAll = (
return result.isValid;
};

ajv.addFormat(config.format_names[AslPathContext.REFERENCE_PATH], {
async: true,
validate: (path: string): Promise<boolean> => {
ajv.addFormat(
config.format_names[AslPathContext.REFERENCE_PATH],
(path: string): boolean => {
return validateAdapter(path, AslPathContext.REFERENCE_PATH);
},
});
}
);

ajv.addFormat(config.format_names[AslPathContext.PATH], {
async: true,
validate: (path: string): Promise<boolean> => {
ajv.addFormat(
config.format_names[AslPathContext.PATH],
(path: string): boolean => {
return validateAdapter(path, AslPathContext.PATH);
},
});
}
);

ajv.addFormat(config.format_names[AslPathContext.PAYLOAD_TEMPLATE], {
async: true,
validate: (path: string): Promise<boolean> => {
ajv.addFormat(
config.format_names[AslPathContext.PAYLOAD_TEMPLATE],
(path: string): boolean => {
return validateAdapter(path, AslPathContext.PAYLOAD_TEMPLATE);
},
});
}
);

ajv.addFormat(config.format_names[AslPathContext.RESULT_PATH], {
async: true,
validate: (path: string): Promise<boolean> => {
ajv.addFormat(
config.format_names[AslPathContext.RESULT_PATH],
(path: string): boolean => {
return validateAdapter(path, AslPathContext.RESULT_PATH);
},
});
}
);
};
77 changes: 49 additions & 28 deletions src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import jsonata from "jsonata";

const find = async (fields: string[], ast: unknown): Promise<boolean> => {
for (const field of fields) {
const expr = jsonata(`**.${field}`);
const result: unknown = await expr.evaluate(ast);
if (result) {
return true;
}
}
return false;
export type FieldsUsed = {
hasFunc?: boolean;
hasVar?: boolean;
hasInvalidReferencePathOps?: boolean;
};

export const referencePathChecks = async (ast: unknown): Promise<boolean> => {
const names = [
"atmark",
"wildcard",
"negOffset",
"slice",
"recursiveDescent",
"multipleIndex",
"filter",
];
return !(await find(names, ast));
};
const invalidReferencePathOps = [
"atmark",
"wildcard",
"negOffset",
"slice",
"recursiveDescent",
"multipleIndex",
"filter",
];

export const hasFunctions = async (ast: unknown): Promise<boolean> => {
return find(["func"], ast);
};
export const gather = (
json: unknown,
accum?: FieldsUsed | null | undefined
): FieldsUsed => {
const acc: FieldsUsed = accum ?? {};
if (typeof json !== "object") {
return acc;
}
const ast = json as Record<string, unknown>;
for (const key of Object.keys(ast)) {
if (key === "func") {
acc.hasFunc = true;
} else if (key === "var") {
acc.hasVar = true;
} else if (invalidReferencePathOps.includes(key)) {
acc.hasInvalidReferencePathOps = true;
}

// if all the acc fields are set, stop traversing
if (acc.hasFunc && acc.hasVar && acc.hasInvalidReferencePathOps) {
return acc;
}

export const hasVariable = async (ast: unknown): Promise<boolean> => {
return find(["var"], ast);
const value = ast[key];
if (value && typeof value === "object") {
if (Array.isArray(value)) {
for (const item of value) {
if (item && typeof item === "object") {
gather(item as Record<string, unknown>, acc);
}
}
} else if (value && typeof value === "object") {
gather(value as Record<string, unknown>, acc);
}
}
}
return acc;
};
15 changes: 8 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// @ts-ignore
import { parse } from "./generated/aslPaths";
import { AslPathContext, ErrorCodes, ValidationResult } from "./types";
import { hasFunctions, hasVariable, referencePathChecks } from "./ast";
import { gather } from "./ast";

export const validatePath = async (
export const validatePath = (
path: string,
context: AslPathContext
): Promise<ValidationResult> => {
): ValidationResult => {
let ast: unknown | null = null;
try {
ast = parse(path);
Expand All @@ -25,11 +25,12 @@ export const validatePath = async (
message: "no ast returned",
};
}
const fields = gather(ast);
switch (context) {
case AslPathContext.PAYLOAD_TEMPLATE:
break;
case AslPathContext.PATH:
if (await hasFunctions(ast)) {
if (fields.hasFunc) {
return {
isValid: false,
code: ErrorCodes.exp_has_functions,
Expand All @@ -38,20 +39,20 @@ export const validatePath = async (
break;
case AslPathContext.REFERENCE_PATH:
case AslPathContext.RESULT_PATH:
if (await hasFunctions(ast)) {
if (fields.hasFunc) {
return {
isValid: false,
code: ErrorCodes.exp_has_functions,
};
}
if (!(await referencePathChecks(ast))) {
if (fields.hasInvalidReferencePathOps) {
return {
isValid: false,
code: ErrorCodes.exp_has_non_reference_path_ops,
};
}
if (context === AslPathContext.RESULT_PATH) {
if (await hasVariable(ast)) {
if (fields.hasVar) {
return {
isValid: false,
code: ErrorCodes.exp_has_variable,
Expand Down