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: support for postman-collection conversion to asyncapi #1527

Merged
merged 5 commits into from
Oct 2, 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: 4 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ _See code: [src/commands/config/versions.ts](https://github.com/asyncapi/cli/blo

## `asyncapi convert [SPEC-FILE]`

Convert asyncapi documents older to newer versions or OpenAPI documents to AsyncAPI
Convert asyncapi documents older to newer versions or OpenAPI | postman-collection documents to AsyncAPI

```
USAGE
Expand All @@ -318,12 +318,12 @@ ARGUMENTS
SPEC-FILE spec path, url, or context-name

FLAGS
-f, --format=<option> (required) [default: asyncapi] Specify the format to convert from (openapi or asyncapi)
<options: openapi|asyncapi>
-f, --format=<option> (required) [default: asyncapi] Specify the format to convert from (openapi or asyncapi or postman-collection)
<options: openapi|asyncapi|postman-collection>
-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.
server). Note: This option is only applicable for OpenAPI | postman-collection to AsyncAPI conversions.
<options: client|server>
-t, --target-version=<value> [default: 3.0.0] asyncapi version to convert to

Expand Down
117 changes: 79 additions & 38 deletions src/commands/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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, convertOpenAPI } from '@asyncapi/converter';
import { convert, convertOpenAPI, convertPostman } from '@asyncapi/converter';
import type { AsyncAPIConvertVersion, OpenAPIConvertVersion } from '@asyncapi/converter';
import { cyan, green } from 'picocolors';

Expand All @@ -16,7 +16,9 @@ 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 or OpenAPI documents to AsyncAPI';
static specFile: any;
static metricsMetadata: any = {};
static description = 'Convert asyncapi documents older to newer versions or OpenAPI/postman-collection documents to AsyncAPI';

static flags = convertFlags(latestVersion);

Expand All @@ -37,47 +39,86 @@ export default class Convert extends Command {
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
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()) {
this.log(`🎉 The URL ${cyan(this.specFile.getFileURL())} has been successfully converted to version ${green(flags['target-version'])}!!`);
}
}

if (typeof convertedFile === 'object') {
convertedFileFormatted = JSON.stringify(convertedFile, null, 4);
} else {
convertedFileFormatted = convertedFile;
}

if (flags.output) {
await fPromises.writeFile(`${flags.output}`, convertedFileFormatted, { encoding: 'utf8' });
} else {
this.log(convertedFileFormatted);
}
convertedFile = this.handleConversion(isOpenAPI, isAsyncAPI, flags);

// Handle file output or log the result
convertedFileFormatted = this.formatConvertedFile(convertedFile);
await this.handleOutput(flags.output, convertedFileFormatted);
} catch (err) {
if (err instanceof SpecificationFileNotFound) {
this.error(new ValidationError({
type: 'invalid-file',
filepath: filePath
}));
} else if (this.specFile?.toJson().asyncapi > flags['target-version']) {
this.error(`The ${cyan(filePath)} file cannot be converted to an older version. Downgrading is not supported.`);
} else {
this.error(err as Error);
}
this.handleError(err, filePath ?? 'unknown', flags);
}
}

// Helper function to handle conversion logic
private handleConversion(isOpenAPI: boolean, isAsyncAPI: boolean, flags: any) {
const specJson = this.specFile?.toJson();
if (isOpenAPI) {
return this.convertOpenAPI(specJson, flags);
} else if (isAsyncAPI) {
return this.convertAsyncAPI(flags);
}
return this.convertPostman(flags);
}

private convertOpenAPI(specJson: any, flags: any) {
const 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'])}!`);
return convertedFile;
}

private convertAsyncAPI(flags: any) {
const 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()) {
this.log(`🎉 The URL ${cyan(this.specFile?.getFileURL())} has been successfully converted to version ${green(flags['target-version'])}!!`);
}
return convertedFile;
}

private convertPostman(flags: any) {
const convertedFile = convertPostman(this.specFile?.text() ?? '', '3.0.0', {
perspective: flags['perspective'] as 'client' | 'server'
});
if (this.specFile?.getFilePath()) {
this.log(`🎉 The ${cyan(this.specFile?.getFilePath())} file has been successfully converted to asyncapi of version ${green(flags['target-version'])}!!`);
} else if (this.specFile?.getFileURL()) {
this.log(`🎉 The URL ${cyan(this.specFile?.getFileURL())} has been successfully converted to asyncapi of version ${green(flags['target-version'])}!!`);
}
return convertedFile;
}

// Helper function to format the converted file
private formatConvertedFile(convertedFile: any) {
return typeof convertedFile === 'object' ? JSON.stringify(convertedFile, null, 4) : convertedFile;
}

// Helper function to handle output
private async handleOutput(outputPath: string | undefined, convertedFileFormatted: string) {
if (outputPath) {
await fPromises.writeFile(`${outputPath}`, convertedFileFormatted, { encoding: 'utf8' });
} else {
this.log(convertedFileFormatted);
}
}

// Helper function to handle errors
private handleError(err: any, filePath: string, flags: any) {
if (err instanceof SpecificationFileNotFound) {
this.error(new ValidationError({
type: 'invalid-file',
filepath: filePath
}));
} else if (this.specFile?.toJson().asyncapi > flags['target-version']) {
this.error(`The ${cyan(filePath)} file cannot be converted to an older version. Downgrading is not supported.`);
} else {
this.error(err as Error);
}
}
}
2 changes: 1 addition & 1 deletion src/core/flags/convert.flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const convertFlags = (latestVersion: string) => {
format: Flags.string({
char: 'f',
description: 'Specify the format to convert from (openapi or asyncapi)',
options: ['openapi', 'asyncapi'],
options: ['openapi', 'asyncapi', 'postman-collection'],
required: true,
default: 'asyncapi',
}),
Expand Down
38 changes: 38 additions & 0 deletions test/fixtures/postman-collection.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
info:
name: Sample Postman Collection
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
item:
- name: Sample Request
request:
method: GET
header: []
url:
raw: 'https://jsonplaceholder.typicode.com/posts/1'
protocol: https
host:
- jsonplaceholder
- typicode
- com
path:
- posts
- '1'
response: []
- name: Sample POST Request
request:
method: POST
header:
- key: Content-Type
value: application/json
body:
mode: raw
raw: '{ "title": "foo", "body": "bar", "userId": 1 }'
url:
raw: 'https://jsonplaceholder.typicode.com/posts'
protocol: https
host:
- jsonplaceholder
- typicode
- com
path:
- posts
response: []
63 changes: 63 additions & 0 deletions test/integration/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const testHelper = new TestHelper();
const filePath = './test/fixtures/specification.yml';
const JSONFilePath = './test/fixtures/specification.json';
const openAPIFilePath = './test/fixtures/openapi.yml';
const postmanFilePath = './test/fixtures/postman-collection.yml';

describe('convert', () => {
describe('with file paths', () => {
Expand Down Expand Up @@ -241,4 +242,66 @@ describe('convert', () => {
done();
});
});

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

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

test
.stderr()
.stdout()
.command(['convert', postmanFilePath, '-f', 'postman-collection'])
.it('works when Postman file path is passed', (ctx, done) => {
expect(ctx.stdout).to.contain(`🎉 The ${postmanFilePath} file has been successfully converted to asyncapi of version 3.0.0!!`);
expect(ctx.stderr).to.equal('');
done();
});

test
.stderr()
.stdout()
.command(['convert', postmanFilePath, '-f', 'postman-collection', '-p=client'])
.it('works when Postman file path is passed with client perspective', (ctx, done) => {
expect(ctx.stdout).to.contain(`🎉 The ${postmanFilePath} file has been successfully converted to asyncapi of version 3.0.0!!`);
expect(ctx.stderr).to.equal('');
done();
});

test
.stderr()
.stdout()
.command(['convert', postmanFilePath, '-f', 'postman-collection', '-p=server'])
.it('works when Postman file path is passed with server perspective', (ctx, done) => {
expect(ctx.stdout).to.contain(`🎉 The ${postmanFilePath} file has been successfully converted to asyncapi of version 3.0.0!!`);
expect(ctx.stderr).to.equal('');
done();
});

test
.stderr()
.stdout()
.command(['convert', postmanFilePath, '-f', 'postman-collection', '-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', postmanFilePath, '-f', 'postman-collection', '-o=./test/fixtures/postman_converted_output.yml'])
.it('works when Postman file is converted and output is saved', (ctx, done) => {
expect(ctx.stdout).to.contain(`🎉 The ${postmanFilePath} file has been successfully converted to asyncapi of version 3.0.0!!`);
expect(fs.existsSync('./test/fixtures/postman_converted_output.yml')).to.equal(true);
expect(ctx.stderr).to.equal('');
fs.unlinkSync('./test/fixtures/postman_converted_output.yml');
done();
});
});
});
Loading