From 92cfd9a0877ca2f9483d8bca0b67d8f0479aca0e Mon Sep 17 00:00:00 2001 From: Mohan Kumar <121867882+ItshMoh@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:21:59 +0530 Subject: [PATCH] feat: completed the conversion from postman to asyncapi (#276) --- README.md | 42 +++++++++++ package-lock.json | 39 +++++++++- package.json | 4 +- src/convert.ts | 17 ++++- src/interfaces.ts | 7 +- src/openapi.ts | 2 +- src/postman-collection.ts | 14 ++++ src/utils.ts | 2 +- test/input/postman/basic-collection.yml | 38 ++++++++++ test/input/postman/header-authentication.yml | 22 ++++++ .../postman-to-asyncapi/basic-collection.yml | 72 +++++++++++++++++++ .../header-authentication.yml | 32 +++++++++ .../header-option-client.yml | 32 +++++++++ test/postman_to_asyncapi.spec.ts | 26 +++++++ 14 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 src/postman-collection.ts create mode 100644 test/input/postman/basic-collection.yml create mode 100644 test/input/postman/header-authentication.yml create mode 100644 test/output/postman-to-asyncapi/basic-collection.yml create mode 100644 test/output/postman-to-asyncapi/header-authentication.yml create mode 100644 test/output/postman-to-asyncapi/header-option-client.yml create mode 100644 test/postman_to_asyncapi.spec.ts diff --git a/README.md b/README.md index 7ebfa2fb..3309b025 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,48 @@ The perspective option can be set to either 'server' (default) or 'client'. - External to internal references: The converter does not support scenarios where an external schema file references internal components of the OpenAPI document. In such cases, manual adjustment of the converted document may be necessary. +### Postman Collection to AsyncAPI conversion + +The converter now also supports conversion from postman collection to AsyncAPI 3.0. This feature enables easy transition of existing postman collection to any AsyncAPI 3.0 documents. + +To use this new conversion feature: + +```js +const fs = require('fs'); +const { convertPostman } = require('@asyncapi/converter') +try { + const postman = fs.readFileSync('postman-collection.yml', 'utf-8') + const asyncapi = convertPostman(postman, '3.0.0'); + console.log(asyncapi); +} catch (e) { + console.error(e); +} +``` + +When converting from postman collection to AsyncAPI you can now specify the perspective of the conversion using the `perspective` option. This allows you to choose whether the conversion should be from an application or client point of view + +```js +const { convertPostman } = require('@asyncapi/converter') +try { + const postman = fs.readFileSync('postman-collection.yml', 'utf-8') + const asyncapi = convertPostman(postman, '3.0.0', { perspective: 'client' }); + console.log(asyncapi); +} catch (e) { + console.error(e); +} +``` + +The perspective option can be set to either 'server' (default) or 'client'. + +- With `server` perspective: `action` becomes `receive` + +- With `client` perspective: `action` becomes `send` + +#### Limitations + +- External to internal references: The converter does not support scenarios where an external schema file references internal components of the OpenAPI document. In such cases, manual adjustment of the converted document may be necessary. + + ## Development 1. Setup project by installing dependencies `npm install` diff --git a/package-lock.json b/package-lock.json index bfcb3545..2b20a381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "license": "Apache-2.0", "dependencies": { "@asyncapi/parser": "^3.1.0", - "js-yaml": "^3.14.1" + "js-yaml": "^3.14.1", + "path": "^0.12.7", + "postman2openapi": "^1.2.1" }, "devDependencies": { "@jest/types": "^27.5.1", @@ -4878,6 +4880,15 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4958,6 +4969,11 @@ "node": ">=12.0.0" } }, + "node_modules/postman2openapi": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/postman2openapi/-/postman2openapi-1.2.1.tgz", + "integrity": "sha512-+TaKfRhln6/TDPT8c0C5qMkJq+DqyybQlHL9RZx2INBNd0jNj3ufhk3VGUmXnX8rPabbKdvt6ojtxVQGMdg9zQ==" + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -5002,6 +5018,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5997,12 +6021,25 @@ "requires-port": "^1.0.0" } }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, "node_modules/utility-types": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", diff --git a/package.json b/package.json index 1b28e8eb..94ef1c91 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "license": "Apache-2.0", "dependencies": { "@asyncapi/parser": "^3.1.0", - "js-yaml": "^3.14.1" + "js-yaml": "^3.14.1", + "path": "^0.12.7", + "postman2openapi": "^1.2.1" }, "devDependencies": { "@jest/types": "^27.5.1", diff --git a/src/convert.ts b/src/convert.ts index 9a6ea63d..cf1e0cdd 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -4,10 +4,11 @@ import { converters as firstConverters } from "./first-version"; import { converters as secondConverters } from "./second-version"; import { converters as thirdConverters } from "./third-version"; import { converters as openapiConverters } from "./openapi"; +import { converters as postmanConverters } from "./postman-collection"; import { serializeInput } from "./utils"; -import type { AsyncAPIDocument, AsyncAPIConvertVersion, OpenAPIConvertVersion, ConvertOptions, ConvertFunction, ConvertOpenAPIFunction, OpenAPIDocument, OpenAPIToAsyncAPIOptions } from './interfaces'; +import type { AsyncAPIDocument, AsyncAPIConvertVersion, OpenAPIConvertVersion, ConvertOptions, ConvertFunction, ConvertOpenAPIFunction, OpenAPIDocument, OpenAPIToAsyncAPIOptions, PostmanToAsyncAPIOptions } from './interfaces'; /** * Value for key (version) represents the function which converts specification from previous version to the given as key. @@ -78,3 +79,17 @@ export function convertOpenAPI(input: string | OpenAPIDocument, version: OpenAPI } return convertedAsyncAPI; } + +export function convertPostman(input: string, version: OpenAPIConvertVersion, options?: PostmanToAsyncAPIOptions ): string; +export function convertPostman(input: Record, version: OpenAPIConvertVersion, options?: PostmanToAsyncAPIOptions): AsyncAPIDocument; +export function convertPostman(input: string | Record, version: OpenAPIConvertVersion, options: PostmanToAsyncAPIOptions={}): string | AsyncAPIDocument { + const { format, document } = serializeInput(input); + const postmantoAsyncapiConverter = postmanConverters[version]; + + const convertedAsyncAPI = postmantoAsyncapiConverter(document as any, options); + + if (format === "yaml") { + return dump(convertedAsyncAPI, { skipInvalid: true }); + } + return convertedAsyncAPI; +} diff --git a/src/interfaces.ts b/src/interfaces.ts index 779c6a53..ba5c2650 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -20,6 +20,11 @@ export type OpenAPIToAsyncAPIOptions = { export type ConvertOptions = { v2tov3?: ConvertV2ToV3Options; openAPIToAsyncAPI?: OpenAPIToAsyncAPIOptions; + postmanToAsyncAPI?: PostmanToAsyncAPIOptions; +} + +export type PostmanToAsyncAPIOptions = { + perspective?: 'client' | 'server'; } /** @@ -27,4 +32,4 @@ export type ConvertOptions = { */ export type ConvertFunction = (asyncapi: AsyncAPIDocument, options: ConvertOptions) => AsyncAPIDocument; export type ConvertOpenAPIFunction = (openapi: OpenAPIDocument, options: OpenAPIToAsyncAPIOptions) => AsyncAPIDocument; - +export type ConvertPostmanFunction = (postman: any , options: PostmanToAsyncAPIOptions) => AsyncAPIDocument; diff --git a/src/openapi.ts b/src/openapi.ts index a8afb3aa..3f882562 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -11,7 +11,7 @@ export const converters: Record = { * @param {ConvertOptions} options - Conversion options. * @returns {AsyncAPIDocument} The converted AsyncAPI document. */ -function from_openapi_to_asyncapi(openapi: OpenAPIDocument, options: OpenAPIToAsyncAPIOptions = {}): AsyncAPIDocument { +export function from_openapi_to_asyncapi(openapi: OpenAPIDocument, options: OpenAPIToAsyncAPIOptions = {}): AsyncAPIDocument { const perspective = options.perspective || 'server'; const asyncapi: Partial = { asyncapi: '3.0.0', diff --git a/src/postman-collection.ts b/src/postman-collection.ts new file mode 100644 index 00000000..c780a09f --- /dev/null +++ b/src/postman-collection.ts @@ -0,0 +1,14 @@ +import { transpile } from 'postman2openapi'; +import { from_openapi_to_asyncapi } from './openapi'; +import { ConvertPostmanFunction, PostmanToAsyncAPIOptions } from 'interfaces'; + +export const converters: Record = { + '3.0.0': from_postman_to_asyncapi +} + +function from_postman_to_asyncapi(postman: any ,options:PostmanToAsyncAPIOptions ={}) { + const perspective = options.perspective; + const openapi = transpile(postman); + const asyncapi = from_openapi_to_asyncapi(openapi , {perspective:perspective}); + return asyncapi; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 7ef17ae3..30714250 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import { load } from 'js-yaml'; import type { AsyncAPIDocument, OpenAPIDocument } from "./interfaces"; -export function serializeInput(document: string | AsyncAPIDocument | OpenAPIDocument): { format: 'json' | 'yaml', document: AsyncAPIDocument | OpenAPIDocument } | never { +export function serializeInput(document: string | AsyncAPIDocument | OpenAPIDocument | Record): { format: 'json' | 'yaml', document: AsyncAPIDocument | OpenAPIDocument } | never { let triedConvertToYaml = false; try { if (typeof document === 'object') { diff --git a/test/input/postman/basic-collection.yml b/test/input/postman/basic-collection.yml new file mode 100644 index 00000000..88e4ee5b --- /dev/null +++ b/test/input/postman/basic-collection.yml @@ -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: [] diff --git a/test/input/postman/header-authentication.yml b/test/input/postman/header-authentication.yml new file mode 100644 index 00000000..6a0eb4be --- /dev/null +++ b/test/input/postman/header-authentication.yml @@ -0,0 +1,22 @@ +info: + name: Headers and Authentication Test Collection + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' +item: + - name: GET Request with Headers and Authentication + request: + method: GET + header: + - key: Authorization + value: Bearer your_token + - key: Accept + value: application/json + url: + raw: 'https://example.com/api/authenticated' + protocol: https + host: + - example + - com + path: + - api + - authenticated + response: [] diff --git a/test/output/postman-to-asyncapi/basic-collection.yml b/test/output/postman-to-asyncapi/basic-collection.yml new file mode 100644 index 00000000..0e482212 --- /dev/null +++ b/test/output/postman-to-asyncapi/basic-collection.yml @@ -0,0 +1,72 @@ +asyncapi: 3.0.0 +info: + title: Sample Postman Collection + version: 1.0.0 +servers: + typicode_com: + host: jsonplaceholder.typicode.com + protocol: https +channels: + posts_1: + address: /posts/1 + messages: + sampleRequestResponse200: + name: sampleRequestResponse200 + title: GET response 200 + summary: '' + posts: + address: /posts + messages: + samplePostRequestRequest: + name: samplePostRequestRequest + title: POST request + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + body: + type: string + example: bar + title: + type: string + example: foo + userId: + type: number + example: 1 + samplePostRequestResponse200: + name: samplePostRequestResponse200 + title: POST response 200 + summary: '' +operations: + sampleRequest: + action: receive + channel: + $ref: '#/channels/posts_1' + summary: Sample Request + description: Sample Request + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/posts_1' + messages: + - $ref: '#/channels/posts_1/messages/sampleRequestResponse200' + samplePostRequest: + action: receive + channel: + $ref: '#/channels/posts' + summary: Sample POST Request + description: Sample POST Request + bindings: + http: + method: POST + messages: + - $ref: '#/channels/posts/messages/samplePostRequestRequest' + reply: + channel: + $ref: '#/channels/posts' + messages: + - $ref: '#/channels/posts/messages/samplePostRequestResponse200' \ No newline at end of file diff --git a/test/output/postman-to-asyncapi/header-authentication.yml b/test/output/postman-to-asyncapi/header-authentication.yml new file mode 100644 index 00000000..daad3627 --- /dev/null +++ b/test/output/postman-to-asyncapi/header-authentication.yml @@ -0,0 +1,32 @@ +asyncapi: 3.0.0 +info: + title: Headers and Authentication Test Collection + version: 1.0.0 +servers: + example_com: + host: example.com + protocol: https +channels: + api_authenticated: + address: /api/authenticated + messages: + getRequestWithHeadersAndAuthenticationResponse200: + name: getRequestWithHeadersAndAuthenticationResponse200 + title: GET response 200 + summary: '' +operations: + getRequestWithHeadersAndAuthentication: + action: receive + channel: + $ref: '#/channels/api_authenticated' + summary: GET Request with Headers and Authentication + description: GET Request with Headers and Authentication + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/api_authenticated' + messages: + - $ref: >- + #/channels/api_authenticated/messages/getRequestWithHeadersAndAuthenticationResponse200 \ No newline at end of file diff --git a/test/output/postman-to-asyncapi/header-option-client.yml b/test/output/postman-to-asyncapi/header-option-client.yml new file mode 100644 index 00000000..7d15c057 --- /dev/null +++ b/test/output/postman-to-asyncapi/header-option-client.yml @@ -0,0 +1,32 @@ +asyncapi: 3.0.0 +info: + title: Headers and Authentication Test Collection + version: 1.0.0 +servers: + example_com: + host: example.com + protocol: https +channels: + api_authenticated: + address: /api/authenticated + messages: + getRequestWithHeadersAndAuthenticationResponse200: + name: getRequestWithHeadersAndAuthenticationResponse200 + title: GET response 200 + summary: '' +operations: + getRequestWithHeadersAndAuthentication: + action: send + channel: + $ref: '#/channels/api_authenticated' + summary: GET Request with Headers and Authentication + description: GET Request with Headers and Authentication + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/api_authenticated' + messages: + - $ref: >- + #/channels/api_authenticated/messages/getRequestWithHeadersAndAuthenticationResponse200 \ No newline at end of file diff --git a/test/postman_to_asyncapi.spec.ts b/test/postman_to_asyncapi.spec.ts new file mode 100644 index 00000000..6a1cac68 --- /dev/null +++ b/test/postman_to_asyncapi.spec.ts @@ -0,0 +1,26 @@ +import fs from 'fs'; +import path from 'path'; +import { convertPostman } from '../src/convert'; +import { assertResults } from './helpers'; + +describe("convert() - postman to asyncapi", () => { + it("should convert the basic structure of postman collection to asyncapi", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "postman", "basic-collection.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "postman-to-asyncapi", "basic-collection.yml"), "utf8"); + const result = convertPostman(input, '3.0.0'); + assertResults(output, result); + }); + + it("should convert headers and authentication from postman collection to asyncapi", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "postman", "header-authentication.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "postman-to-asyncapi", "header-authentication.yml"), "utf8"); + const result = convertPostman(input, '3.0.0'); + assertResults(output, result); + }); + it("should convert headers and authentication from postman collection to asyncapi with perspective option client", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "postman", "header-authentication.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "postman-to-asyncapi", "header-option-client.yml"), "utf8"); + const result = convertPostman(input, '3.0.0', { perspective: 'client' }); + assertResults(output, result); + }); +});