Skip to content

Commit

Permalink
feat: add convert controller and service
Browse files Browse the repository at this point in the history
Added the "yaml" package to convert JSON to YAML, since convert-js only seems to support YAML specs.
If we don't want to support a JSON spec on the HTTP request, we can delete this dependency.

If we do want that capability, then perhaps we should choose just one YAML package (yaml or js-yaml) that can do these conversions.

asyncapi#13
  • Loading branch information
BOLT04 authored and magicmatatjahu committed Mar 24, 2022
1 parent 8517b38 commit 4e465eb
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 3 deletions.
3 changes: 2 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"js-yaml": "^4.1.0",
"redoc-express": "^1.0.0",
"uuid": "^8.3.2",
"winston": "^3.3.3"
"winston": "^3.3.3",
"yaml": "^1.10.2"
},
"devDependencies": {
"@semantic-release-plus/docker": "^3.1.0",
Expand Down
74 changes: 74 additions & 0 deletions src/controllers/convert.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextFunction, Request, Response, Router } from 'express';
import YAML from 'js-yaml';

import { ALL_SPECS, Controller, ConvertRequestDto } from '../interfaces';

import { documentValidationMiddleware } from '../middlewares/document-validation.middleware';

import { ConvertService } from '../services/convert.service';

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

/**
* Controller which exposes the Convert functionality
*/
export class ConvertController implements Controller {
public basepath = '/convert';

private convertService = new ConvertService();

private async convert(req: Request, res: Response, next: NextFunction) {
try {
const { version, language, asyncapi } = req.body as ConvertRequestDto;
// Validate input
if (!ALL_SPECS.includes(version)) {
return next(new ProblemException({
type: 'invalid-json',
title: 'Bad Request',
status: 400,
detail: 'Invalid version parameter'
}));
}

await parse(asyncapi, prepareParserConfig(req));
const convertedSpec = await this.convertService.convertSpec(
asyncapi,
language,
version.toString(),
);

if (!convertedSpec) {
return next(new ProblemException({
type: 'invalid-json',
title: 'Bad Request',
status: 400,
detail: 'Couldn\'t convert the spec to the requested version.'
}));
}
const convertedSpecObject = YAML.load(convertedSpec);
res.json({
asyncapi: convertedSpecObject
});
} catch (err: unknown) {
return next(new ProblemException({
type: 'internal-server-error',
title: 'Internal server error',
status: 500,
detail: (err as Error).message,
}));
}
}

public boot(): Router {
const router = Router();

router.post(
`${this.basepath}`,
documentValidationMiddleware,
this.convert.bind(this)
);

return router;
}
}
134 changes: 134 additions & 0 deletions src/controllers/tests/convert.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import request from 'supertest';

import { App } from '../../app';
import { ProblemException } from '../../exceptions/problem.exception';

import { ConvertController } from '../convert.controller';

const validAsyncAPI2_0_0 = `
asyncapi: 2.0.0
info:
title: Super test
version: 1.0.0
servers:
default:
url: 'test:{port}'
description: Test broker
variables:
port:
description: test
protocol: mqtt
components:
messages:
lightMeasured:
summary: >-
Inform about environmental lighting conditions for a particular
streetlight.
payload:
schemas:
lightMeasuredPayload:
type: object
properties:
lumens:
type: integer
minimum: 0
description: Light intensity measured in lumens.
channels:
'test':
publish:
message:
$ref: '#/components/messages/lightMeasured'
`;

describe('ConvertController', () => {
describe('[POST] /convert', () => {
it('should throw error with invalid version', async () => {
const app = new App([new ConvertController()]);

return await request(app.getServer())
.post('/convert')
.send({
asyncapi: {
asyncapi: '2.2.0',
info: {
title: 'Test Service',
version: '1.0.0',
},
channels: {},
},
version: '1'
})
.expect(400, {
type: ProblemException.createType('invalid-json'),
title: 'Bad Request',
status: 400,
detail: 'Invalid version parameter'
});
});

it('should pass when converting to latest version', async () => {
const app = new App([new ConvertController()]);

return await request(app.getServer())
.post('/convert')
.send({
asyncapi: validAsyncAPI2_0_0,
version: '2.2.0'
})
.expect(200, {
asyncapi: {
asyncapi: '2.2.0',
info: {
title: 'Super test',
version: '1.0.0'
},
servers: {
default: {
url: 'test:{port}',
description: 'Test broker',
variables: {
port: {
description: 'test'
}
},
protocol: 'mqtt'
}
},
components: {
messages: {
lightMeasured: {
summary: 'Inform about environmental lighting conditions for a particular streetlight.',
payload: null
}
},
schemas: {
lightMeasuredPayload: {
type: 'object',
properties: {
lumens: {
type: 'integer',
minimum: 0,
description: 'Light intensity measured in lumens.'
}
}
}
}
},
channels: {
test: {
publish: {
message: {
$ref: '#/components/messages/lightMeasured'
}
}
}
}
}
});
});
});
});
29 changes: 28 additions & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsyncAPIDocument } from '@asyncapi/parser';
import { Router } from 'express';

export interface Controller {
basepath: string;
boot(): Router;
Expand All @@ -13,3 +13,30 @@ export interface Problem {
instance?: string;
[key: string]: any;
}

// Note: Spec versions are defined in @asyncapi/specs
export const ALL_SPECS = [
'1.0.0',
'1.1.0',
'1.2.0',
'2.0.0-rc1',
'2.0.0-rc2',
'2.0.0',
'2.1.0',
'2.2.0',
'latest'
] as const;
export type SpecsEnum = typeof ALL_SPECS[number];

export type ConvertRequestDto = {
/**
* Spec version to upgrade to.
* Default is 'latest'.
*/
version?: SpecsEnum;
/**
* Language to convert the file to.
*/
language?: string,
asyncapi: AsyncAPIDocument
}
61 changes: 61 additions & 0 deletions src/services/convert.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { convert } from '@asyncapi/converter';
import { AsyncAPIDocument } from '@asyncapi/parser';
import specs from '@asyncapi/specs';

import * as JSYAML from 'js-yaml';
import YAML from 'yaml';

import { logger } from '../utils/logger';

/**
* Service providing `@asyncapi/converter` functionality.
*/
export class ConvertService {
/**
* Convert the given spec to the desired language.
* @param spec AsyncAPI spec
* @param language Language to convert to, YAML or JSON
* @param version [version] AsyncAPI spec version
* @returns converted spec
*/
public async convertSpec(
spec: AsyncAPIDocument | string,
language = '',
version: string = this.getLastVersion(),
): Promise<string> {
try {
let asyncapiSpec: string;
if (typeof spec === 'object') { // TODO: can we check if it's an instance of AsyncAPIDocument?
// Convert JSON object to YAML
const doc = new YAML.Document();
doc.contents = spec;
asyncapiSpec = doc.toString();
} else {
asyncapiSpec = spec;
}

const convertedSpec = convert(asyncapiSpec, version);

return language === 'json'
? this.convertToJSON(convertedSpec)
: convertedSpec;
} catch (err) {
logger.error('[ConvertService] An error has occurred while converting spec to version: {0}. Error: {1}', version, err);
throw err;
}
}

private getLastVersion = () => Object.keys(specs).pop();

private convertToJSON(spec: string) {
try {
// JSON or YAML String -> JS object
const jsonContent = JSYAML.load(spec);
// JS Object -> pretty JSON string
return JSON.stringify(jsonContent, null, 2);
} catch (err) {
logger.error('[ConvertService.convertToJSON] Error: {0}', err);
throw err;
}
}
}

0 comments on commit 4e465eb

Please sign in to comment.