Skip to content

Commit

Permalink
feat: added openapi conversion support (#1500)
Browse files Browse the repository at this point in the history
* add openapi conversion support

* added the `format` flag

---------

Co-authored-by: souvik <[email protected]>
Co-authored-by: asyncapi-bot <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 2836045 commit e204457
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 15 deletions.
8 changes: 5 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,22 +308,24 @@ _See code: [src/commands/config/versions.ts](https://github.com/asyncapi/cli/blo

## `asyncapi convert [SPEC-FILE]`

Convert asyncapi documents older to newer versions
Convert asyncapi documents older to newer versions or or OpenAPI documents to AsyncAPI

```
USAGE
$ asyncapi convert [SPEC-FILE] [-h] [-o <value>] [-t <value>]
$ asyncapi convert [SPEC-FILE] [-h] [-o <value>] [-t <value>] [-p <value>]
ARGUMENTS
SPEC-FILE spec path, url, or context-name
FLAGS
-h, --help Show CLI help.
-o, --output=<value> path to the file where the result is saved
-p, --perspective=<option> [default: server] Perspective to use when converting OpenAPI to AsyncAPI (client or server). Note: This option is only applicable for OpenAPI to AsyncAPI conversions.
<options: client|server>
-t, --target-version=<value> [default: 3.0.0] asyncapi version to convert to
DESCRIPTION
Convert asyncapi documents older to newer versions
Convert asyncapi documents older to newer versions or or OpenAPI documents to AsyncAPI
```

_See code: [src/commands/convert.ts](https://github.com/asyncapi/cli/blob/v2.3.12/src/commands/convert.ts)_
Expand Down
20 changes: 15 additions & 5 deletions src/commands/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import Command from '../core/base';
import { ValidationError } from '../core/errors/validation-error';
import { load } from '../core/models/SpecificationFile';
import { SpecificationFileNotFound } from '../core/errors/specification-file';
import { convert } from '@asyncapi/converter';
import type { AsyncAPIConvertVersion } from '@asyncapi/converter';
import { convert, convertOpenAPI } from '@asyncapi/converter';
import type { AsyncAPIConvertVersion, OpenAPIConvertVersion } from '@asyncapi/converter';
import { cyan, green } from 'picocolors';

// @ts-ignore
Expand All @@ -16,7 +16,7 @@ import { convertFlags } from '../core/flags/convert.flags';
const latestVersion = Object.keys(specs.schemas).pop() as string;

export default class Convert extends Command {
static description = 'Convert asyncapi documents older to newer versions';
static description = 'Convert asyncapi documents older to newer versions or OpenAPI documents to AsyncAPI';

static flags = convertFlags(latestVersion);

Expand All @@ -36,9 +36,19 @@ export default class Convert extends Command {
// eslint-disable-next-line sonarjs/no-duplicate-string
this.metricsMetadata.to_version = flags['target-version'];

// Determine if the input is OpenAPI or AsyncAPI
const specJson = this.specFile.toJson();
const isOpenAPI = flags['format'] === 'openapi';
const isAsyncAPI = flags['format'] === 'asyncapi';

// CONVERSION
convertedFile = convert(this.specFile.text(), flags['target-version'] as AsyncAPIConvertVersion);
if (convertedFile) {
if (isOpenAPI) {
convertedFile = convertOpenAPI(this.specFile.text(), specJson.openapi as OpenAPIConvertVersion, {
perspective: flags['perspective'] as 'client' | 'server'
});
this.log(`🎉 The OpenAPI document has been successfully converted to AsyncAPI version ${green(flags['target-version'])}!`);
} else if (isAsyncAPI) {
convertedFile = convert(this.specFile.text(), flags['target-version'] as AsyncAPIConvertVersion);
if (this.specFile.getFilePath()) {
this.log(`🎉 The ${cyan(this.specFile.getFilePath())} file has been successfully converted to version ${green(flags['target-version'])}!!`);
} else if (this.specFile.getFileURL()) {
Expand Down
15 changes: 14 additions & 1 deletion src/core/flags/convert.flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ export const convertFlags = (latestVersion: string) => {
return {
help: Flags.help({ char: 'h' }),
output: Flags.string({ char: 'o', description: 'path to the file where the result is saved' }),
'target-version': Flags.string({ char: 't', description: 'asyncapi version to convert to', default: latestVersion })
format: Flags.string({
char: 'f',
description: 'Specify the format to convert from (openapi or asyncapi)',
options: ['openapi', 'asyncapi'],
required: true,
default: 'asyncapi',
}),
'target-version': Flags.string({ char: 't', description: 'asyncapi version to convert to', default: latestVersion }),
perspective: Flags.string({
char: 'p',
description: 'Perspective to use when converting OpenAPI to AsyncAPI (client or server). Note: This option is only applicable for OpenAPI to AsyncAPI conversions.',
options: ['client', 'server'],
default: 'server',
}),
};
};
137 changes: 137 additions & 0 deletions test/fixtures/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
openapi: 3.0.0
info:
title: Callbacks, Links, and Content Types API
version: 1.0.0
description: An API showcasing callbacks, links, and various content types
servers:
- url: https://api.example.com/v1
paths:
/webhooks:
post:
summary: Subscribe to webhook
operationId: subscribeWebhook
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
callbackUrl:
type: string
format: uri
responses:
'201':
description: Subscription created
callbacks:
onEvent:
'{$request.body#/callbackUrl}':
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
eventType:
type: string
eventData:
type: object
responses:
'200':
description: Webhook processed
/users/{userId}:
get:
summary: Get a user
operationId: getUser
parameters:
- in: path
name: userId
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
links:
userPosts:
operationId: getUserPosts
parameters:
userId: '$response.body#/id'
/users/{userId}/posts:
get:
summary: Get user posts
operationId: getUserPosts
parameters:
- in: path
name: userId
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
/upload:
post:
summary: Upload a file
operationId: uploadFile
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
'200':
description: Successful upload
content:
application/json:
schema:
type: object
properties:
fileId:
type: string
/stream:
get:
summary: Get a data stream
operationId: getStream
responses:
'200':
description: Successful response
content:
application/octet-stream:
schema:
type: string
format: binary
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
Post:
type: object
properties:
id:
type: string
title:
type: string
content:
type: string
75 changes: 69 additions & 6 deletions test/integration/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { expect } from '@oclif/test';
const testHelper = new TestHelper();
const filePath = './test/fixtures/specification.yml';
const JSONFilePath = './test/fixtures/specification.json';
const openAPIFilePath = './test/fixtures/openapi.yml';

describe('convert', () => {
describe('with file paths', () => {
Expand Down Expand Up @@ -85,7 +86,7 @@ describe('convert', () => {
testHelper.unsetCurrentContext();
testHelper.createDummyContextFile();
})
.command(['convert'])
.command(['convert', '-f', 'asyncapi'])
.it('throws error message if no current context', (ctx, done) => {
expect(ctx.stdout).to.equal('');
expect(ctx.stderr).to.equal('ContextError: No context is set as current, please set a current context.\n');
Expand All @@ -107,7 +108,7 @@ describe('convert', () => {
test
.stderr()
.stdout()
.command(['convert'])
.command(['convert', '-f', 'asyncapi'])
.it('throws error message if no context file exists', (ctx, done) => {
expect(ctx.stdout).to.equal('');
expect(ctx.stderr).to.equal(`error locating AsyncAPI document: ${NO_CONTEXTS_SAVED}\n`);
Expand All @@ -127,7 +128,7 @@ describe('convert', () => {
test
.stderr()
.stdout()
.command(['convert', filePath, '-t=2.3.0'])
.command(['convert', filePath, '-f', 'asyncapi', '-t=2.3.0'])
.it('works when supported target-version is passed', (ctx, done) => {
expect(ctx.stdout).to.contain('asyncapi: 2.3.0');
expect(ctx.stderr).to.equal('');
Expand All @@ -137,7 +138,7 @@ describe('convert', () => {
test
.stderr()
.stdout()
.command(['convert', filePath, '-t=2.95.0'])
.command(['convert', filePath, '-f', 'asyncapi', '-t=2.95.0'])
.it('should throw error if non-supported target-version is passed', (ctx, done) => {
expect(ctx.stdout).to.equal('');
expect(ctx.stderr).to.contain('Error: Cannot convert');
Expand All @@ -157,7 +158,7 @@ describe('convert', () => {
test
.stderr()
.stdout()
.command(['convert', filePath, '-o=./test/fixtures/specification_output.yml'])
.command(['convert', filePath, '-f', 'asyncapi', '-o=./test/fixtures/specification_output.yml'])
.it('works when .yml file is passed', (ctx, done) => {
expect(ctx.stdout).to.contain(`The ${filePath} file has been successfully converted to version 3.0.0!!`);
expect(fs.existsSync('./test/fixtures/specification_output.yml')).to.equal(true);
Expand All @@ -169,7 +170,7 @@ describe('convert', () => {
test
.stderr()
.stdout()
.command(['convert', JSONFilePath, '-o=./test/fixtures/specification_output.json'])
.command(['convert', JSONFilePath, '-f', 'asyncapi', '-o=./test/fixtures/specification_output.json'])
.it('works when .json file is passed', (ctx, done) => {
expect(ctx.stdout).to.contain(`The ${JSONFilePath} file has been successfully converted to version 3.0.0!!`);
expect(fs.existsSync('./test/fixtures/specification_output.json')).to.equal(true);
Expand All @@ -178,4 +179,66 @@ describe('convert', () => {
done();
});
});

describe('with OpenAPI input', () => {
beforeEach(() => {
testHelper.createDummyContextFile();
});

afterEach(() => {
testHelper.deleteDummyContextFile();
});

test
.stderr()
.stdout()
.command(['convert', openAPIFilePath, '-f', 'openapi'])
.it('works when OpenAPI file path is passed', (ctx, done) => {
expect(ctx.stdout).to.contain('The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
expect(ctx.stderr).to.equal('');
done();
});

test
.stderr()
.stdout()
.command(['convert', openAPIFilePath, '-f', 'openapi', '-p=client'])
.it('works when OpenAPI file path is passed with client perspective', (ctx, done) => {
expect(ctx.stdout).to.contain('The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
expect(ctx.stderr).to.equal('');
done();
});

test
.stderr()
.stdout()
.command(['convert', openAPIFilePath, '-f', 'openapi','-p=server'])
.it('works when OpenAPI file path is passed with server perspective', (ctx, done) => {
expect(ctx.stdout).to.contain('The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
expect(ctx.stderr).to.equal('');
done();
});

test
.stderr()
.stdout()
.command(['convert', openAPIFilePath, '-f', 'openapi', '-p=invalid'])
.it('should throw error if invalid perspective is passed', (ctx, done) => {
expect(ctx.stdout).to.equal('');
expect(ctx.stderr).to.contain('Error: Expected --perspective=invalid to be one of: client, server');
done();
});

test
.stderr()
.stdout()
.command(['convert', openAPIFilePath, '-f', 'openapi', '-o=./test/fixtures/openapi_converted_output.yml'])
.it('works when OpenAPI file is converted and output is saved', (ctx, done) => {
expect(ctx.stdout).to.contain('🎉 The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
expect(fs.existsSync('./test/fixtures/openapi_converted_output.yml')).to.equal(true);
expect(ctx.stderr).to.equal('');
fs.unlinkSync('./test/fixtures/openapi_converted_output.yml');
done();
});
});
});

0 comments on commit e204457

Please sign in to comment.