forked from asyncapi/server-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add convert controller and service
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
1 parent
8517b38
commit 4e465eb
Showing
6 changed files
with
301 additions
and
3 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |