Skip to content

Commit

Permalink
feat: support for postman-collection conversion to asyncapi (#1527)
Browse files Browse the repository at this point in the history
Co-authored-by: Ashish Padhy <[email protected]>%0ACo-authored-by: asyncapi-bot <[email protected]>
  • Loading branch information
Gmin2 and asyncapi-bot authored Oct 2, 2024
1 parent 8f7b019 commit ec996f4
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 43 deletions.
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();
});
});
});

0 comments on commit ec996f4

Please sign in to comment.