diff --git a/package-lock.json b/package-lock.json index 2afaf56..30c0833 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,13 @@ { "name": "asl-path-validator", - "version": "0.16.1", + "version": "0.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "asl-path-validator", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", - "dependencies": { - "jsonata": "^2.1.0" - }, "devDependencies": { "@types/jest": "^28.1.3", "@typescript-eslint/eslint-plugin": "^5.30.0", @@ -4351,15 +4348,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsonata": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.1.0.tgz", - "integrity": "sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", diff --git a/package.json b/package.json index 032344a..9e176ce 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -50,6 +50,5 @@ "typescript": "^4.7.4" }, "dependencies": { - "jsonata": "^2.1.0" } } diff --git a/src/__tests__/ajv.test.ts b/src/__tests__/ajv.test.ts index 0d214b4..37e60b6 100644 --- a/src/__tests__/ajv.test.ts +++ b/src/__tests__/ajv.test.ts @@ -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"; @@ -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); @@ -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<{ @@ -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: { @@ -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]); + }); }); diff --git a/src/__tests__/validatePath.test.ts b/src/__tests__/validatePath.test.ts index d4b855b..7f470d8 100644 --- a/src/__tests__/validatePath.test.ts +++ b/src/__tests__/validatePath.test.ts @@ -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(); diff --git a/src/ajv.ts b/src/ajv.ts index 4f11952..c53de10 100644 --- a/src/ajv.ts +++ b/src/ajv.ts @@ -16,11 +16,8 @@ export const registerAll = ( ajv: Ajv, config = AslPathValidatorConfig ): void => { - const validateAdapter = async ( - path: string, - pathType: AslPathContext - ): Promise => { - 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}` @@ -29,31 +26,31 @@ export const registerAll = ( return result.isValid; }; - ajv.addFormat(config.format_names[AslPathContext.REFERENCE_PATH], { - async: true, - validate: (path: string): Promise => { + 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 => { + 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 => { + 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 => { + ajv.addFormat( + config.format_names[AslPathContext.RESULT_PATH], + (path: string): boolean => { return validateAdapter(path, AslPathContext.RESULT_PATH); - }, - }); + } + ); }; diff --git a/src/ast.ts b/src/ast.ts index 0c6c2a4..3bfcc9f 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,33 +1,54 @@ -import jsonata from "jsonata"; - -const find = async (fields: string[], ast: unknown): Promise => { - 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 => { - 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 => { - 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; + 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 => { - 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, acc); + } + } + } else if (value && typeof value === "object") { + gather(value as Record, acc); + } + } + } + return acc; }; diff --git a/src/index.ts b/src/index.ts index 6a95be5..72e1dc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 => { let ast: unknown | null = null; try { ast = parse(path); @@ -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, @@ -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,