From 43cf30d4f08b141edc10e6f4840c3b430934eed1 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Wed, 30 Mar 2022 12:54:05 +0200 Subject: [PATCH 1/3] feat: add /parse path --- openapi.yaml | 124 +++++++++++++----- src/controllers/convert.controller.ts | 2 +- src/controllers/parse.controller.ts | 36 +++++ .../tests/parse.controller.test.ts | 79 +++++++++++ src/middlewares/validation.middleware.ts | 16 ++- src/server-api.d.ts | 5 +- src/server.ts | 8 +- 7 files changed, 227 insertions(+), 43 deletions(-) create mode 100644 src/controllers/parse.controller.ts create mode 100644 src/controllers/tests/parse.controller.test.ts diff --git a/openapi.yaml b/openapi.yaml index 52dd65b1..ab83ad93 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -15,38 +15,34 @@ servers: - url: https://api.asyncapi.com/v1 paths: - /generate: + /validate: post: - summary: Generate the given AsyncAPI template. - operationId: generate + summary: Validate the given AsyncAPI document. + operationId: validate tags: - - generate - - generator + - validate + - validator externalDocs: - name: Github Repository for the AsyncAPI Generator - url: https://github.com/asyncapi/generator + name: Github Repository for the AsyncAPI Parser + url: https://github.com/asyncapi/parser-js requestBody: - description: Template details to be generated. + description: Validate the given AsyncAPI document. required: true content: application/json: schema: - $ref: '#/components/schemas/GenerateRequest' + $ref: '#/components/schemas/ValidateRequest' responses: - "200": - description: Template successfully generated. - content: - application/zip: - schema: - $ref: '#/components/schemas/GenerateResponse' - '400': - description: Failed to generate the given template due to invalid AsyncAPI document. + "204": + description: The given AsyncAPI document is valid. + "400": + description: The given AsyncAPI document is not valid. content: application/json: schema: $ref: "#/components/schemas/Problem" "422": - description: Failed to generate the given template due to invalid parameters. + description: The given AsyncAPI document is not valid due to invalid parameters in the request. content: application/json: schema: @@ -58,26 +54,30 @@ paths: schema: $ref: "#/components/schemas/Problem" - /validate: + /parse: post: - summary: Validate the given AsyncAPI document. + summary: Parse the given AsyncAPI document. operationId: validate tags: - - validate - - validator + - parse + - parser externalDocs: name: Github Repository for the AsyncAPI Parser url: https://github.com/asyncapi/parser-js requestBody: - description: Validate the given AsyncAPI document with the AsyncAPI parser. + description: Parse the given AsyncAPI document. required: true content: application/json: schema: - $ref: '#/components/schemas/ValidateRequest' + $ref: '#/components/schemas/ParseRequest' responses: "204": - description: The given AsyncAPI document is valid. + description: AsyncAPI document successfully parsed. + content: + application/json: + schema: + $ref: "#/components/schemas/ParseResponse" "400": description: The given AsyncAPI document is not valid. content: @@ -97,6 +97,49 @@ paths: schema: $ref: "#/components/schemas/Problem" + /generate: + post: + summary: Generate the given AsyncAPI template. + operationId: generate + tags: + - generate + - generator + externalDocs: + name: Github Repository for the AsyncAPI Generator + url: https://github.com/asyncapi/generator + requestBody: + description: Template details to be generated. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateRequest' + responses: + "200": + description: Template successfully generated. + content: + application/zip: + schema: + $ref: '#/components/schemas/GenerateResponse' + '400': + description: Failed to generate the given template due to invalid AsyncAPI document. + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "422": + description: Failed to generate the given template due to invalid parameters. + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + default: + description: Unexpected problem. + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + /convert: post: summary: Convert the given AsyncAPI document to the specified version. @@ -257,6 +300,29 @@ components: - '2.3.0' - 'latest' + ValidateRequest: + type: object + required: + - asyncapi + properties: + asyncapi: + $ref: '#/components/schemas/AsyncAPIDocument' + + ParseRequest: + type: object + required: + - asyncapi + properties: + asyncapi: + $ref: '#/components/schemas/AsyncAPIDocument' + ParseResponse: + type: object + required: + - parsed + properties: + parsed: + type: string + GenerateRequest: type: object required: @@ -291,14 +357,6 @@ components: type: string format: binary - ValidateRequest: - type: object - required: - - asyncapi - properties: - asyncapi: - $ref: '#/components/schemas/AsyncAPIDocument' - ConvertRequest: type: object required: diff --git a/src/controllers/convert.controller.ts b/src/controllers/convert.controller.ts index 09a52aaa..06a669f1 100644 --- a/src/controllers/convert.controller.ts +++ b/src/controllers/convert.controller.ts @@ -38,7 +38,7 @@ export class ConvertController implements Controller { language, ); - res.json({ + res.status(200).json({ converted: convertedSpec }); } catch (err: unknown) { diff --git a/src/controllers/parse.controller.ts b/src/controllers/parse.controller.ts new file mode 100644 index 00000000..f19653f1 --- /dev/null +++ b/src/controllers/parse.controller.ts @@ -0,0 +1,36 @@ +import { Request, Response, Router } from 'express'; +import { AsyncAPIDocument } from '@asyncapi/parser'; + +import { validationMiddleware } from '../middlewares/validation.middleware'; + +import { Controller} from '../interfaces'; + +/** + * Controller which exposes the Parser functionality, to parse the AsyncAPI document. + */ +export class ParseController implements Controller { + public basepath = '/parse'; + + private async parse(req: Request, res: Response) { + const stringified = AsyncAPIDocument.stringify(req.asyncapi?.parsedDocument); + res.status(200).json({ + parsed: stringified, + }); + } + + public async boot(): Promise { + const router = Router(); + + router.post( + `${this.basepath}`, + await validationMiddleware({ + path: this.basepath, + method: 'post', + documents: ['asyncapi'], + }), + this.parse.bind(this) + ); + + return router; + } +} \ No newline at end of file diff --git a/src/controllers/tests/parse.controller.test.ts b/src/controllers/tests/parse.controller.test.ts new file mode 100644 index 00000000..1774fa92 --- /dev/null +++ b/src/controllers/tests/parse.controller.test.ts @@ -0,0 +1,79 @@ +import request from 'supertest'; + +import { App } from '../../app'; +import { ProblemException } from '../../exceptions/problem.exception'; + +import { ParseController } from '../parse.controller'; + +const validJSONAsyncAPI = { + asyncapi: '2.0.0', + info: { + title: 'My API', + version: '1.0.0' + }, + channels: {} +}; +const invalidJSONAsyncAPI = { + asyncapi: '2.0.0', + info: { + tite: 'My API', // spelled wrong on purpose to throw an error in the test + version: '1.0.0' + }, + channels: {} +}; + +describe('ParseController', () => { + describe('[POST] /parse', () => { + it('should return stringified AsyncAPI document', async () => { + const app = new App([new ParseController()]); + await app.init(); + + return request(app.getServer()) + .post('/v1/parse') + .send({ + asyncapi: validJSONAsyncAPI + }) + .expect(200, { + parsed: '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{},"x-parser-spec-parsed":true,"x-parser-spec-stringified":true}', + }); + }); + + it('should throw error when sent an invalid AsyncAPI document', async () => { + const app = new App([new ParseController()]); + await app.init(); + + return request(app.getServer()) + .post('/v1/parse') + .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: {} + } + }); + }); + }); +}); diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts index bc7fd44b..3d730d64 100644 --- a/src/middlewares/validation.middleware.ts +++ b/src/middlewares/validation.middleware.ts @@ -1,4 +1,5 @@ import { Request, Response, NextFunction } from 'express'; +import { AsyncAPIDocument } from '@asyncapi/parser'; import { ProblemException } from '../exceptions/problem.exception'; import { createAjvInstance } from '../utils/ajv'; @@ -6,7 +7,6 @@ import { getAppOpenAPI } from '../utils/app-openapi'; import { parse, prepareParserConfig, tryConvertToProblemException } from '../utils/parser'; import type { ValidateFunction } from 'ajv'; -import type { AsyncAPIDocument } from '../interfaces'; export interface ValidationMiddlewareOptions { path: string; @@ -78,13 +78,16 @@ async function validateSingleDocument(asyncapi: string | AsyncAPIDocument, parse if (typeof asyncapi === 'object') { asyncapi = JSON.parse(JSON.stringify(asyncapi)); } - await parse(asyncapi, parserConfig); + return await parse(asyncapi, parserConfig); } async function validateListDocuments(asyncapis: Array, parserConfig: ReturnType) { + const parsedDocuments: Array = []; for (const asyncapi of asyncapis) { - await validateSingleDocument(asyncapi, parserConfig); + const parsed = await validateSingleDocument(asyncapi, parserConfig); + parsedDocuments.push(parsed); } + return parsedDocuments; } /** @@ -106,12 +109,15 @@ export async function validationMiddleware(options: ValidationMiddlewareOptions) // validate AsyncAPI document(s) const parserConfig = prepareParserConfig(req); try { + req.asyncapi = req.asyncapi || {}; for (const field of documents) { const body = req.body[String(field)]; if (Array.isArray(body)) { - await validateListDocuments(body, parserConfig); + const parsed = await validateListDocuments(body, parserConfig); + req.asyncapi.parsedDocuments = parsed; } else { - await validateSingleDocument(body, parserConfig); + const parsed = await validateSingleDocument(body, parserConfig); + req.asyncapi.parsedDocument = parsed; } } diff --git a/src/server-api.d.ts b/src/server-api.d.ts index 9ed1a0f3..95fca912 100644 --- a/src/server-api.d.ts +++ b/src/server-api.d.ts @@ -2,6 +2,9 @@ import { AsyncAPIDocument } from '@asyncapi/parser'; declare module 'express' { export interface Request { - parsedDocument?: AsyncAPIDocument; + asyncapi?: { + parsedDocument?: AsyncAPIDocument; + parsedDocuments?: Array; + }, } } \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 618e9ec9..5a9446d0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,8 +5,9 @@ import './configs/production.json'; process.env['NODE_CONFIG_DIR'] = `${__dirname }/configs`; import { App } from './app'; -import { GenerateController } from './controllers/generate.controller'; import { ValidateController } from './controllers/validate.controller'; +import { ParseController } from './controllers/parse.controller'; +import { GenerateController } from './controllers/generate.controller'; import { ConvertController } from './controllers/convert.controller'; import { BundleController } from './controllers/bundle.controller'; import { DiffController } from './controllers/diff.controller'; @@ -14,12 +15,13 @@ import { DocsController } from './controllers/docs.controller'; async function main() { const app = new App([ - new GenerateController(), new ValidateController(), + new ParseController(), + new GenerateController(), new ConvertController(), new BundleController(), new DiffController(), - new DocsController() + new DocsController(), ]); await app.init(); app.listen(); From 78a6f5ad9991e5926e6792255d9b2d238cd6574d Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Wed, 30 Mar 2022 14:30:11 +0200 Subject: [PATCH 2/3] fix sonarcloud errors --- src/middlewares/validation.middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts index 3d730d64..d6bd778f 100644 --- a/src/middlewares/validation.middleware.ts +++ b/src/middlewares/validation.middleware.ts @@ -78,7 +78,7 @@ async function validateSingleDocument(asyncapi: string | AsyncAPIDocument, parse if (typeof asyncapi === 'object') { asyncapi = JSON.parse(JSON.stringify(asyncapi)); } - return await parse(asyncapi, parserConfig); + return parse(asyncapi, parserConfig); } async function validateListDocuments(asyncapis: Array, parserConfig: ReturnType) { From ce8a3e0c26a3bdd38d6cac496e8d0336b0ab3bb2 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Mon, 4 Apr 2022 10:51:19 +0200 Subject: [PATCH 3/3] after review --- openapi.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index ab83ad93..162f73fe 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -21,7 +21,7 @@ paths: operationId: validate tags: - validate - - validator + - parser externalDocs: name: Github Repository for the AsyncAPI Parser url: https://github.com/asyncapi/parser-js @@ -57,7 +57,7 @@ paths: /parse: post: summary: Parse the given AsyncAPI document. - operationId: validate + operationId: parse tags: - parse - parser @@ -72,7 +72,7 @@ paths: schema: $ref: '#/components/schemas/ParseRequest' responses: - "204": + "200": description: AsyncAPI document successfully parsed. content: application/json: