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: integrate new ParserJS #178

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ module.exports = {
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
// Module file extensions for importing
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^nimma/legacy$': '<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js',
'^nimma/(.*)': '<rootDir>/node_modules/nimma/dist/cjs/$1',
},

testTimeout: 20000,

Expand Down
2,867 changes: 2,714 additions & 153 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"@asyncapi/nodejs-template": "^0.11.4",
"@asyncapi/nodejs-ws-template": "^0.9.25",
"@asyncapi/openapi-schema-parser": "^2.0.1",
"@asyncapi/parser": "^1.17.0",
"@asyncapi/parser": "^2.0.0-next-major.2",
"@asyncapi/python-paho-template": "^0.2.13",
"@asyncapi/raml-dt-schema-parser": "^2.0.1",
"@asyncapi/specs": "^3.2.1",
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/generate.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ArchiverService } from '../services/archiver.service';
import { GeneratorService } from '../services/generator.service';

import { ProblemException } from '../exceptions/problem.exception';
import { prepareParserConfig } from '../utils/parser';
import { prepareParseOptions } from '../utils/parser';

/**
* Controller which exposes the Generator functionality
Expand Down Expand Up @@ -43,7 +43,7 @@ export class GenerateController implements Controller {
template,
parameters,
tmpDir,
prepareParserConfig(req),
prepareParseOptions(req),
);
} catch (genErr: unknown) {
return next(new ProblemException({
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/parse.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response, Router } from 'express';
import { AsyncAPIDocument } from '@asyncapi/parser';
import { stringify } from '@asyncapi/parser';

import { validationMiddleware } from '../middlewares/validation.middleware';

Expand All @@ -12,7 +12,7 @@ export class ParseController implements Controller {
public basepath = '/parse';

private async parse(req: Request, res: Response) {
const stringified = AsyncAPIDocument.stringify(req.asyncapi?.parsedDocument);
const stringified = stringify(req.asyncapi?.parsedDocument);
res.status(200).json({
parsed: stringified,
});
Expand Down
35 changes: 7 additions & 28 deletions src/controllers/tests/validate.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,38 +131,17 @@ describe('ValidateController', () => {
const app = new App([new ValidateController()]);
await app.init();

return request(app.getServer())
const res = await request(app.getServer())
.post('/v1/validate')
.send({
asyncapi: invalidJSONAsyncAPI
})
.expect(422, {
type: ProblemException.createType('validation-errors'),
title: 'There were errors validating the AsyncAPI document.',
status: 422,
validationErrors: [
{
title: '/info should NOT have additional properties',
location: {
jsonPointer: '/info'
}
},
{
title: '/info should have required property \'title\'',
location: {
jsonPointer: '/info'
}
}
],
parsedJSON: {
asyncapi: '2.0.0',
info: {
tite: 'My API',
version: '1.0.0'
},
channels: {}
}
});
.expect(400);

expect(res.body.type).toEqual('https://api.asyncapi.com/problem/invalid-asyncapi-document(s)');
expect(res.body.title).toEqual('The provided AsyncAPI Document(s) is invalid');
expect(res.body.status).toEqual(400);
expect(res.body.diagnostics).toBeTruthy();
});
});
});
56 changes: 36 additions & 20 deletions src/middlewares/validation.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Request, Response, NextFunction } from 'express';
import { AsyncAPIDocument } from '@asyncapi/parser';

import { hasErrorDiagnostic } from '@asyncapi/parser/cjs/utils';
import { ProblemException } from '../exceptions/problem.exception';
import { createAjvInstance } from '../utils/ajv';
import { getAppOpenAPI } from '../utils/app-openapi';
import { parse, prepareParserConfig, tryConvertToProblemException } from '../utils/parser';
import { parser, prepareParseOptions, tryConvertParserExceptionToProblem } from '../utils/parser';

import type { Request, Response, NextFunction } from 'express';
import type { AsyncAPIDocumentInterface, ParseOptions, Diagnostic } from '@asyncapi/parser';
import type { ValidateFunction } from 'ajv';

export interface ValidationMiddlewareOptions {
Expand All @@ -25,12 +25,17 @@ async function compileAjv(options: ValidationMiddlewareOptions) {
const paths = appOpenAPI.paths || {};

const pathName = options.path;
const methodName = options.method;

const validatorKey = `${pathName}->${methodName}`;
const validate = ajvInstance.getSchema(validatorKey);
if (validate) return validate;

const path = paths[String(pathName)];
if (!path) {
throw new Error(`Path "${pathName}" doesn't exist in the OpenAPI document.`);
}

const methodName = options.method;
const method = path[String(methodName)];
if (!method) {
throw new Error(`Method "${methodName}" for "${pathName}" path doesn't exist in the OpenAPI document.`);
Expand All @@ -42,7 +47,7 @@ async function compileAjv(options: ValidationMiddlewareOptions) {
let schema = requestBody.content['application/json'].schema;
if (!schema) return;

schema = { ...schema };
schema = { ...schema }; // shallow copy
schema['$schema'] = 'http://json-schema.org/draft-07/schema';

if (options.documents && schema.properties) {
Expand All @@ -57,7 +62,8 @@ async function compileAjv(options: ValidationMiddlewareOptions) {
});
}

return ajvInstance.compile(schema);
ajvInstance.addSchema(schema, validatorKey);
return ajvInstance.getSchema(validatorKey);
}

async function validateRequestBody(validate: ValidateFunction, body: any) {
Expand All @@ -74,20 +80,22 @@ async function validateRequestBody(validate: ValidateFunction, body: any) {
}
}

async function validateSingleDocument(asyncapi: string | AsyncAPIDocument, parserConfig: ReturnType<typeof prepareParserConfig>) {
async function validateSingleDocument(asyncapi: string | AsyncAPIDocumentInterface, parseConfig: ParseOptions) {
if (typeof asyncapi === 'object') {
asyncapi = JSON.parse(JSON.stringify(asyncapi));
}
return parse(asyncapi, parserConfig);
return parser.parse(asyncapi, parseConfig);
}

async function validateListDocuments(asyncapis: Array<string | AsyncAPIDocument>, parserConfig: ReturnType<typeof prepareParserConfig>) {
const parsedDocuments: Array<AsyncAPIDocument> = [];
async function validateListDocuments(asyncapis: Array<string | AsyncAPIDocumentInterface>, parseConfig: ParseOptions) {
const parsedDocuments: Array<AsyncAPIDocumentInterface> = [];
const allDiagnostics: Array<Diagnostic> = [];
for (const asyncapi of asyncapis) {
const parsed = await validateSingleDocument(asyncapi, parserConfig);
parsedDocuments.push(parsed);
const { document, diagnostics } = await validateSingleDocument(asyncapi, parseConfig);
parsedDocuments.push(document);
allDiagnostics.push(...diagnostics);
}
return parsedDocuments;
return { documents: parsedDocuments, diagnostics: allDiagnostics };
}

/**
Expand All @@ -107,23 +115,31 @@ export async function validationMiddleware(options: ValidationMiddlewareOptions)
}

// validate AsyncAPI document(s)
const parserConfig = prepareParserConfig(req);
const parserConfig = prepareParseOptions(req);
let _diagnostics: Diagnostic[] = [];
try {
req.asyncapi = req.asyncapi || {};
for (const field of documents) {
const body = req.body[String(field)];

if (Array.isArray(body)) {
const parsed = await validateListDocuments(body, parserConfig);
req.asyncapi.parsedDocuments = parsed;
const { documents, diagnostics } = await validateListDocuments(body, parserConfig);
req.asyncapi.parsedDocuments = documents;
_diagnostics = diagnostics;
} else {
const parsed = await validateSingleDocument(body, parserConfig);
req.asyncapi.parsedDocument = parsed;
const { document, diagnostics } = await validateSingleDocument(body, parserConfig);
req.asyncapi.parsedDocument = document;
_diagnostics = diagnostics;
}
}

if (hasErrorDiagnostic(_diagnostics)) {
return next(tryConvertParserExceptionToProblem(_diagnostics));
}

next();
} catch (err: unknown) {
return next(tryConvertToProblemException(err));
return next(tryConvertParserExceptionToProblem(err));
}
};
}
6 changes: 3 additions & 3 deletions src/server-api.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AsyncAPIDocument } from '@asyncapi/parser';
import { AsyncAPIDocumentInterface } from '@asyncapi/parser';

declare module 'express' {
export interface Request {
asyncapi?: {
parsedDocument?: AsyncAPIDocument;
parsedDocuments?: Array<AsyncAPIDocument>;
parsedDocument?: AsyncAPIDocumentInterface;
parsedDocuments?: Array<AsyncAPIDocumentInterface>;
},
}
}
2 changes: 1 addition & 1 deletion src/services/tests/convert.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('ConvertService', () => {

describe('.convert()', () => {
it('should throw error that the converter cannot convert to a lower version', async () => {
let err: ProblemException;
let err: ProblemException | undefined;
try {
await convertService.convert(validJsonAsyncAPI2_0_0, '1.2.0');
} catch (e) {
Expand Down
121 changes: 46 additions & 75 deletions src/utils/parser.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,62 @@
import { registerSchemaParser, parse, ParserError } from '@asyncapi/parser';
import { Request } from 'express';
import { Parser } from '@asyncapi/parser';
import { OpenAPISchemaParser } from '@asyncapi/parser/cjs/schema-parser/openapi-schema-parser';
import { AvroSchemaParser } from '@asyncapi/parser/cjs/schema-parser/avro-schema-parser';
import { RamlSchemaParser } from '@asyncapi/parser/cjs/schema-parser/raml-schema-parser';
import { ProblemException } from '../exceptions/problem.exception';

import ramlDtParser from '@asyncapi/raml-dt-schema-parser';
import openapiSchemaParser from '@asyncapi/openapi-schema-parser';
import avroSchemaParser from '@asyncapi/avro-schema-parser';
import type { Request } from 'express';
import type { ParseOptions } from '@asyncapi/parser';

function createParser(): Parser {
return new Parser({
schemaParsers: [
OpenAPISchemaParser(),
AvroSchemaParser(),
RamlSchemaParser(),
],
__unstable: {
resolver: {
cache: false,
resolvers: [
{
schema: 'file',
canRead: true,
read: () => '',
}
]
}
}
});
}

registerSchemaParser(openapiSchemaParser);
registerSchemaParser(ramlDtParser);
registerSchemaParser(avroSchemaParser);
const parser = createParser();

function prepareParserConfig(req?: Request) {
function prepareParseOptions(req?: Request): ParseOptions {
if (!req) {
return {
resolve: {
file: false,
},
};
return;
}

return {
resolve: {
file: false,
http: {
headers: {
Cookie: req.header('Cookie'),
},
withCredentials: true,
}
},
path: req.header('x-asyncapi-base-url') || req.header('referer') || req.header('origin'),
source: req.header('x-asyncapi-base-url') || req.header('referer') || req.header('origin'),
};
}

const TYPES_400 = [
'null-or-falsey-document',
'impossible-to-convert-to-json',
'invalid-document-type',
'invalid-json',
'invalid-yaml',
];

/**
* Some error types have to be treated as 400 HTTP Status Code, another as 422.
*/
function retrieveStatusCode(type: string): number {
if (TYPES_400.includes(type)) {
return 400;
}
return 422;
}

/**
* Merges fields from ParserError to ProblemException.
*/
function mergeParserError(error: ProblemException, parserError: any): ProblemException {
if (parserError.detail) {
error.detail = parserError.detail;
}
if (parserError.validationErrors) {
error.validationErrors = parserError.validationErrors;
}
if (parserError.parsedJSON) {
error.parsedJSON = parserError.parsedJSON;
}
if (parserError.location) {
error.location = parserError.location;
}
if (parserError.refs) {
error.refs = parserError.refs;
}
return error;
}

function tryConvertToProblemException(err: any) {
let error = err;
if (error instanceof ParserError) {
const typeName = err.type.replace('https://github.com/asyncapi/parser-js/', '');
error = new ProblemException({
type: typeName,
title: err.title,
status: retrieveStatusCode(typeName),
function tryConvertParserExceptionToProblem(diagnosticsOrError: unknown) {
if (Array.isArray(diagnosticsOrError)) {
return new ProblemException({
type: 'invalid-asyncapi-document(s)',
title: 'The provided AsyncAPI Document(s) is invalid',
status: 400,
diagnostics: diagnosticsOrError,
});
mergeParserError(error, err);
}

return error;
return new ProblemException({
type: 'internal-parser-error',
title: 'Internal parser error',
status: 500,
detail: (diagnosticsOrError as Error).message,
});
}

export { prepareParserConfig, parse, mergeParserError, retrieveStatusCode, tryConvertToProblemException };
export { parser, prepareParseOptions, tryConvertParserExceptionToProblem };