Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added openapi conversion support #1500

Merged
merged 6 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});
Loading