Skip to content

Commit

Permalink
rework
Browse files Browse the repository at this point in the history
  • Loading branch information
smoya committed Jul 27, 2022
1 parent 712db4e commit 30c0cb3
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 79 deletions.
23 changes: 17 additions & 6 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"typescript": "^4.6.2"
},
"dependencies": {
"@asyncapi/specs": "^3.1.0",
"@stoplight/spectral-core": "^1.10.1",
"@stoplight/spectral-functions": "^1.5.1",
"@stoplight/spectral-parsers": "^1.0.1",
Expand Down
113 changes: 64 additions & 49 deletions src/schema-parser/asyncapi-schema-parser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { SchemaParser, ParseSchemaInput, ValidateSchemaInput } from "../schema-parser";
import Ajv from "ajv";
import { JSONSchema7 } from "json-schema"
import Ajv, { ErrorObject, ValidateFunction } from "ajv";
import type { AsyncAPISchema, SchemaValidateResult } from '../types';
// @ts-ignore
import specs from '@asyncapi/specs';

const ajv = new Ajv({
allErrors: true,
})
strict: false,
});

const specVersions = Object.keys(specs).filter((version: string) => !['1.0.0', '1.1.0', '1.2.0', '2.0.0-rc1', '2.0.0-rc2'].includes(version));

export function AsyncAPISchemaParser(): SchemaParser {
return {
Expand All @@ -16,62 +20,72 @@ export function AsyncAPISchemaParser(): SchemaParser {
}

async function validate(input: ValidateSchemaInput<unknown, unknown>): Promise<SchemaValidateResult[]> {
const schema = input.data as JSONSchema7;
let errors: SchemaValidateResult[] = [];

try {
ajv.compile(schema);
} catch (error: any) {
if (error! instanceof Error) {
errors = ajvToSpectralErrors(error);
} else {
// Unknown and unexpected error
throw error;
}
const version = input.asyncapi.semver.version
const validator = findSchemaValidator(version);

let result: SchemaValidateResult[] = []
const valid = validator(input.data);
if (!valid && validator.errors) {
result = ajvToSpectralResult(validator.errors, input.path);
}

return errors;
return result;
}

function ajvToSpectralErrors(error: Error): SchemaValidateResult[] {
let errors: SchemaValidateResult[] = [];
let errorMessage = error.message;

// Validation errors.
// See related AJV function where the error message is generated:
// https://github.com/ajv-validator/ajv/blob/99e884dc4bbb828cf47771b7bbdb14f23193b0b1/lib/core.ts#L501-L522
const validationErrorPrefix = "schema is invalid: ";
if (error.message.startsWith(validationErrorPrefix)) {
// remove prefix
errorMessage = errorMessage.substring(validationErrorPrefix.length);

// message can contain multiple validation errors separated by ',' (comma)
errorMessage.split(", ").forEach((message: string) => {
const splitIndex = message.indexOf(" ");
const path = message.slice(0, splitIndex);
const error = message.slice(splitIndex + 1);

const resultErr: SchemaValidateResult = {
message: error,
path: path.split("/")
};

errors.push(resultErr);
});
} else {
// Not a validation error
const resultErr: SchemaValidateResult = {
function ajvToSpectralResult(errors: ErrorObject[], parentPath: Array<string | number>): SchemaValidateResult[] {
if (parentPath === undefined) {
parentPath = [];
}

return errors.map(error => {
const errorPath = error.instancePath.replace(/^\//, '').split('/'); // TODO: Instance Path or Schema Path?

return {
message: error.message,
};
path: parentPath.concat(errorPath),
} as SchemaValidateResult;
});
}

errors.push(resultErr);
function findSchemaValidator(version: string): ValidateFunction {
let validator = ajv.getSchema(version);
if (!validator) {
const schema = preparePayloadSchema2(specs[version], version);

ajv.addSchema(schema, version);
validator = ajv.getSchema(version);
}

return errors;
return validator as ValidateFunction;
}

async function parse(input: ParseSchemaInput<unknown, unknown>): Promise<AsyncAPISchema> {
return input.data as JSONSchema7;
return input.data as AsyncAPISchema;
}

/**
* To validate schema of the payload we just need a small portion of official AsyncAPI spec JSON Schema, the definition of the schema must be
* a main part of the JSON Schema
*
* @private
* @param {Object} asyncapiSchema AsyncAPI specification JSON Schema
* @param {Object} version AsyncAPI version.
* @returns {Object} valid JSON Schema document describing format of AsyncAPI-valid schema for message payload
*/
function preparePayloadSchema2(asyncapiSchema: AsyncAPISchema, version: string) {
const payloadSchema = `http://asyncapi.com/definitions/${version}/schema.json`;
const definitions = asyncapiSchema.definitions;
if (definitions === undefined) {
throw new Error("AsyncAPI schema must contain definitions");
}

// Remove the meta schemas because it is already present within Ajv, and it's not possible to add duplicate schemas.
delete definitions['http://json-schema.org/draft-07/schema'];
delete definitions['http://json-schema.org/draft-04/schema'];
return {
$ref: payloadSchema,
definitions
};
}

function getMimeTypes() {
Expand All @@ -80,7 +94,8 @@ function getMimeTypes() {
'application/schema+json;version=draft-07',
'application/schema+yaml;version=draft-07',
];
['2.0.0', '2.1.0', '2.2.0', '2.3.0'].forEach(version => {

specVersions.forEach((version: string) => {
mimeTypes.push(
`application/vnd.aai.asyncapi;version=${version}`,
`application/vnd.aai.asyncapi+json;version=${version}`,
Expand Down
33 changes: 9 additions & 24 deletions test/schema-parser/asyncapi-schema-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('AsyncAPISchemaParser', function () {
const validSchema = {
asyncapi: {
semver: {
major: 2
version: "2.4.0",
}
},
data: {
Expand All @@ -28,6 +28,10 @@ describe('AsyncAPISchemaParser', function () {

const parser = AsyncAPISchemaParser();

it('should return Mime Types', async function () {
expect(parser.getMimeTypes()).not.toEqual([]);
});

it('should parse valid AsyncAPI Schema', async function () {
const schema = <ParseSchemaInput<object>>validSchema;
const parsed = await parser.parse(schema);
Expand All @@ -45,9 +49,10 @@ describe('AsyncAPISchemaParser', function () {
const schema = <ValidateSchemaInput<object>>{
asyncapi: {
semver: {
major: 2
version: "2.4.0",
}
},
path: ["components", "schemas", "schema1", "payload"],
data: {
oneOf: "this should be an array",
properties: {
Expand All @@ -60,28 +65,8 @@ describe('AsyncAPISchemaParser', function () {

const result = await parser.validate(schema);
const expectedResult: SchemaValidateResult[] = [
{ "message": "must be object,boolean", "path": ["data", "properties", "name", "if"] },
{ "message": "must be array", "path": ["data", "oneOf"] }
];

expect(result).toEqual(expectedResult);
});

it('should validate invalid AsyncAPI Schema with invalid meta schema', async function () {
const schema = <ValidateSchemaInput<object>>{
asyncapi: {
semver: {
major: 2
}
},
data: {
$schema: "non-existent-meta-schema",
}
};

const result = await parser.validate(schema);
const expectedResult: SchemaValidateResult[] = [
{ "message": "no schema with key or ref \"non-existent-meta-schema\"" },
{ "message": "must be object,boolean", "path": ["components", "schemas", "schema1", "payload", "properties", "name", "if"] },
{ "message": "must be array", "path": ["components", "schemas", "schema1", "payload", "oneOf"] }
];

expect(result).toEqual(expectedResult);
Expand Down

0 comments on commit 30c0cb3

Please sign in to comment.