Skip to content

Commit e204457

Browse files
Gmin2Souviknsasyncapi-bot
authored
feat: added openapi conversion support (#1500)
* add openapi conversion support * added the `format` flag --------- Co-authored-by: souvik <souvikde.ns@gmail.com> Co-authored-by: asyncapi-bot <bot+chan@asyncapi.io>
1 parent 2836045 commit e204457

File tree

5 files changed

+240
-15
lines changed

5 files changed

+240
-15
lines changed

docs/usage.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,22 +308,24 @@ _See code: [src/commands/config/versions.ts](https://github.com/asyncapi/cli/blo
308308

309309
## `asyncapi convert [SPEC-FILE]`
310310

311-
Convert asyncapi documents older to newer versions
311+
Convert asyncapi documents older to newer versions or or OpenAPI documents to AsyncAPI
312312

313313
```
314314
USAGE
315-
$ asyncapi convert [SPEC-FILE] [-h] [-o <value>] [-t <value>]
315+
$ asyncapi convert [SPEC-FILE] [-h] [-o <value>] [-t <value>] [-p <value>]
316316
317317
ARGUMENTS
318318
SPEC-FILE spec path, url, or context-name
319319
320320
FLAGS
321321
-h, --help Show CLI help.
322322
-o, --output=<value> path to the file where the result is saved
323+
-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.
324+
<options: client|server>
323325
-t, --target-version=<value> [default: 3.0.0] asyncapi version to convert to
324326
325327
DESCRIPTION
326-
Convert asyncapi documents older to newer versions
328+
Convert asyncapi documents older to newer versions or or OpenAPI documents to AsyncAPI
327329
```
328330

329331
_See code: [src/commands/convert.ts](https://github.com/asyncapi/cli/blob/v2.3.12/src/commands/convert.ts)_

src/commands/convert.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import Command from '../core/base';
55
import { ValidationError } from '../core/errors/validation-error';
66
import { load } from '../core/models/SpecificationFile';
77
import { SpecificationFileNotFound } from '../core/errors/specification-file';
8-
import { convert } from '@asyncapi/converter';
9-
import type { AsyncAPIConvertVersion } from '@asyncapi/converter';
8+
import { convert, convertOpenAPI } from '@asyncapi/converter';
9+
import type { AsyncAPIConvertVersion, OpenAPIConvertVersion } from '@asyncapi/converter';
1010
import { cyan, green } from 'picocolors';
1111

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

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

2121
static flags = convertFlags(latestVersion);
2222

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

39+
// Determine if the input is OpenAPI or AsyncAPI
40+
const specJson = this.specFile.toJson();
41+
const isOpenAPI = flags['format'] === 'openapi';
42+
const isAsyncAPI = flags['format'] === 'asyncapi';
43+
3944
// CONVERSION
40-
convertedFile = convert(this.specFile.text(), flags['target-version'] as AsyncAPIConvertVersion);
41-
if (convertedFile) {
45+
if (isOpenAPI) {
46+
convertedFile = convertOpenAPI(this.specFile.text(), specJson.openapi as OpenAPIConvertVersion, {
47+
perspective: flags['perspective'] as 'client' | 'server'
48+
});
49+
this.log(`🎉 The OpenAPI document has been successfully converted to AsyncAPI version ${green(flags['target-version'])}!`);
50+
} else if (isAsyncAPI) {
51+
convertedFile = convert(this.specFile.text(), flags['target-version'] as AsyncAPIConvertVersion);
4252
if (this.specFile.getFilePath()) {
4353
this.log(`🎉 The ${cyan(this.specFile.getFilePath())} file has been successfully converted to version ${green(flags['target-version'])}!!`);
4454
} else if (this.specFile.getFileURL()) {

src/core/flags/convert.flags.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ export const convertFlags = (latestVersion: string) => {
44
return {
55
help: Flags.help({ char: 'h' }),
66
output: Flags.string({ char: 'o', description: 'path to the file where the result is saved' }),
7-
'target-version': Flags.string({ char: 't', description: 'asyncapi version to convert to', default: latestVersion })
7+
format: Flags.string({
8+
char: 'f',
9+
description: 'Specify the format to convert from (openapi or asyncapi)',
10+
options: ['openapi', 'asyncapi'],
11+
required: true,
12+
default: 'asyncapi',
13+
}),
14+
'target-version': Flags.string({ char: 't', description: 'asyncapi version to convert to', default: latestVersion }),
15+
perspective: Flags.string({
16+
char: 'p',
17+
description: 'Perspective to use when converting OpenAPI to AsyncAPI (client or server). Note: This option is only applicable for OpenAPI to AsyncAPI conversions.',
18+
options: ['client', 'server'],
19+
default: 'server',
20+
}),
821
};
922
};

test/fixtures/openapi.yml

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Callbacks, Links, and Content Types API
4+
version: 1.0.0
5+
description: An API showcasing callbacks, links, and various content types
6+
servers:
7+
- url: https://api.example.com/v1
8+
paths:
9+
/webhooks:
10+
post:
11+
summary: Subscribe to webhook
12+
operationId: subscribeWebhook
13+
requestBody:
14+
required: true
15+
content:
16+
application/json:
17+
schema:
18+
type: object
19+
properties:
20+
callbackUrl:
21+
type: string
22+
format: uri
23+
responses:
24+
'201':
25+
description: Subscription created
26+
callbacks:
27+
onEvent:
28+
'{$request.body#/callbackUrl}':
29+
post:
30+
requestBody:
31+
required: true
32+
content:
33+
application/json:
34+
schema:
35+
type: object
36+
properties:
37+
eventType:
38+
type: string
39+
eventData:
40+
type: object
41+
responses:
42+
'200':
43+
description: Webhook processed
44+
/users/{userId}:
45+
get:
46+
summary: Get a user
47+
operationId: getUser
48+
parameters:
49+
- in: path
50+
name: userId
51+
required: true
52+
schema:
53+
type: string
54+
responses:
55+
'200':
56+
description: Successful response
57+
content:
58+
application/json:
59+
schema:
60+
$ref: '#/components/schemas/User'
61+
links:
62+
userPosts:
63+
operationId: getUserPosts
64+
parameters:
65+
userId: '$response.body#/id'
66+
/users/{userId}/posts:
67+
get:
68+
summary: Get user posts
69+
operationId: getUserPosts
70+
parameters:
71+
- in: path
72+
name: userId
73+
required: true
74+
schema:
75+
type: string
76+
responses:
77+
'200':
78+
description: Successful response
79+
content:
80+
application/json:
81+
schema:
82+
type: array
83+
items:
84+
$ref: '#/components/schemas/Post'
85+
/upload:
86+
post:
87+
summary: Upload a file
88+
operationId: uploadFile
89+
requestBody:
90+
content:
91+
multipart/form-data:
92+
schema:
93+
type: object
94+
properties:
95+
file:
96+
type: string
97+
format: binary
98+
responses:
99+
'200':
100+
description: Successful upload
101+
content:
102+
application/json:
103+
schema:
104+
type: object
105+
properties:
106+
fileId:
107+
type: string
108+
/stream:
109+
get:
110+
summary: Get a data stream
111+
operationId: getStream
112+
responses:
113+
'200':
114+
description: Successful response
115+
content:
116+
application/octet-stream:
117+
schema:
118+
type: string
119+
format: binary
120+
components:
121+
schemas:
122+
User:
123+
type: object
124+
properties:
125+
id:
126+
type: string
127+
name:
128+
type: string
129+
Post:
130+
type: object
131+
properties:
132+
id:
133+
type: string
134+
title:
135+
type: string
136+
content:
137+
type: string

test/integration/convert.test.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { expect } from '@oclif/test';
88
const testHelper = new TestHelper();
99
const filePath = './test/fixtures/specification.yml';
1010
const JSONFilePath = './test/fixtures/specification.json';
11+
const openAPIFilePath = './test/fixtures/openapi.yml';
1112

1213
describe('convert', () => {
1314
describe('with file paths', () => {
@@ -85,7 +86,7 @@ describe('convert', () => {
8586
testHelper.unsetCurrentContext();
8687
testHelper.createDummyContextFile();
8788
})
88-
.command(['convert'])
89+
.command(['convert', '-f', 'asyncapi'])
8990
.it('throws error message if no current context', (ctx, done) => {
9091
expect(ctx.stdout).to.equal('');
9192
expect(ctx.stderr).to.equal('ContextError: No context is set as current, please set a current context.\n');
@@ -107,7 +108,7 @@ describe('convert', () => {
107108
test
108109
.stderr()
109110
.stdout()
110-
.command(['convert'])
111+
.command(['convert', '-f', 'asyncapi'])
111112
.it('throws error message if no context file exists', (ctx, done) => {
112113
expect(ctx.stdout).to.equal('');
113114
expect(ctx.stderr).to.equal(`error locating AsyncAPI document: ${NO_CONTEXTS_SAVED}\n`);
@@ -127,7 +128,7 @@ describe('convert', () => {
127128
test
128129
.stderr()
129130
.stdout()
130-
.command(['convert', filePath, '-t=2.3.0'])
131+
.command(['convert', filePath, '-f', 'asyncapi', '-t=2.3.0'])
131132
.it('works when supported target-version is passed', (ctx, done) => {
132133
expect(ctx.stdout).to.contain('asyncapi: 2.3.0');
133134
expect(ctx.stderr).to.equal('');
@@ -137,7 +138,7 @@ describe('convert', () => {
137138
test
138139
.stderr()
139140
.stdout()
140-
.command(['convert', filePath, '-t=2.95.0'])
141+
.command(['convert', filePath, '-f', 'asyncapi', '-t=2.95.0'])
141142
.it('should throw error if non-supported target-version is passed', (ctx, done) => {
142143
expect(ctx.stdout).to.equal('');
143144
expect(ctx.stderr).to.contain('Error: Cannot convert');
@@ -157,7 +158,7 @@ describe('convert', () => {
157158
test
158159
.stderr()
159160
.stdout()
160-
.command(['convert', filePath, '-o=./test/fixtures/specification_output.yml'])
161+
.command(['convert', filePath, '-f', 'asyncapi', '-o=./test/fixtures/specification_output.yml'])
161162
.it('works when .yml file is passed', (ctx, done) => {
162163
expect(ctx.stdout).to.contain(`The ${filePath} file has been successfully converted to version 3.0.0!!`);
163164
expect(fs.existsSync('./test/fixtures/specification_output.yml')).to.equal(true);
@@ -169,7 +170,7 @@ describe('convert', () => {
169170
test
170171
.stderr()
171172
.stdout()
172-
.command(['convert', JSONFilePath, '-o=./test/fixtures/specification_output.json'])
173+
.command(['convert', JSONFilePath, '-f', 'asyncapi', '-o=./test/fixtures/specification_output.json'])
173174
.it('works when .json file is passed', (ctx, done) => {
174175
expect(ctx.stdout).to.contain(`The ${JSONFilePath} file has been successfully converted to version 3.0.0!!`);
175176
expect(fs.existsSync('./test/fixtures/specification_output.json')).to.equal(true);
@@ -178,4 +179,66 @@ describe('convert', () => {
178179
done();
179180
});
180181
});
182+
183+
describe('with OpenAPI input', () => {
184+
beforeEach(() => {
185+
testHelper.createDummyContextFile();
186+
});
187+
188+
afterEach(() => {
189+
testHelper.deleteDummyContextFile();
190+
});
191+
192+
test
193+
.stderr()
194+
.stdout()
195+
.command(['convert', openAPIFilePath, '-f', 'openapi'])
196+
.it('works when OpenAPI file path is passed', (ctx, done) => {
197+
expect(ctx.stdout).to.contain('The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
198+
expect(ctx.stderr).to.equal('');
199+
done();
200+
});
201+
202+
test
203+
.stderr()
204+
.stdout()
205+
.command(['convert', openAPIFilePath, '-f', 'openapi', '-p=client'])
206+
.it('works when OpenAPI file path is passed with client perspective', (ctx, done) => {
207+
expect(ctx.stdout).to.contain('The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
208+
expect(ctx.stderr).to.equal('');
209+
done();
210+
});
211+
212+
test
213+
.stderr()
214+
.stdout()
215+
.command(['convert', openAPIFilePath, '-f', 'openapi','-p=server'])
216+
.it('works when OpenAPI file path is passed with server perspective', (ctx, done) => {
217+
expect(ctx.stdout).to.contain('The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
218+
expect(ctx.stderr).to.equal('');
219+
done();
220+
});
221+
222+
test
223+
.stderr()
224+
.stdout()
225+
.command(['convert', openAPIFilePath, '-f', 'openapi', '-p=invalid'])
226+
.it('should throw error if invalid perspective is passed', (ctx, done) => {
227+
expect(ctx.stdout).to.equal('');
228+
expect(ctx.stderr).to.contain('Error: Expected --perspective=invalid to be one of: client, server');
229+
done();
230+
});
231+
232+
test
233+
.stderr()
234+
.stdout()
235+
.command(['convert', openAPIFilePath, '-f', 'openapi', '-o=./test/fixtures/openapi_converted_output.yml'])
236+
.it('works when OpenAPI file is converted and output is saved', (ctx, done) => {
237+
expect(ctx.stdout).to.contain('🎉 The OpenAPI document has been successfully converted to AsyncAPI version 3.0.0!');
238+
expect(fs.existsSync('./test/fixtures/openapi_converted_output.yml')).to.equal(true);
239+
expect(ctx.stderr).to.equal('');
240+
fs.unlinkSync('./test/fixtures/openapi_converted_output.yml');
241+
done();
242+
});
243+
});
181244
});

0 commit comments

Comments
 (0)