diff --git a/lib/index.js b/lib/index.js index 53e7428..d3cec19 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,7 +30,7 @@ async function postmanToOpenApi (input, output, { } const { request: { url, method, body, description: rawDesc, header, auth }, - name: summary, tag = defaultTag, event: events + name: summary, tag = defaultTag, event: events, response } = element const { path, query, protocol, host, port, valid } = scrapeURL(url) if (valid) { @@ -45,7 +45,7 @@ async function postmanToOpenApi (input, output, { ...parseBody(body, method), ...parseOperationAuth(auth, securitySchemes, optsAuth), ...parseParameters(query, header, joinedPath, paramsMeta), - responses: parseResponse(events) + ...parseResponse(response, events) } } } @@ -353,7 +353,15 @@ function descriptionParse (description) { } } -function parseResponse (events = []) { +function parseResponse (responses, events) { + if (responses != null) { + return parseResponseFromExamples(responses) + } else { + return { responses: parseResponseFromEvents(events) } + } +} + +function parseResponseFromEvents (events = []) { let status = 200 const test = events.filter(event => event.listen === 'test') if (test.length > 0) { @@ -371,6 +379,89 @@ function parseResponse (events = []) { } } +function parseResponseFromExamples (responses) { + // Group responses by status code + const statusCodeMap = responses + .reduce((statusMap, { name, code, status: description, header, body, _postman_previewlanguage: language }) => { + if (code in statusMap) { + if (!(language in statusMap[code].bodies)) { + statusMap[code].bodies[language] = [] + } + statusMap[code].bodies[language].push({ name, body }) + } else { + statusMap[code] = { + description, + header, + bodies: { [language]: [{ name, body }] } + } + } + return statusMap + }, {}) + // Parse for OpenAPI + const parsedResponses = Object.entries(statusCodeMap) + .reduce((parsed, [status, { description, header, bodies }]) => { + parsed[status] = { + description, + ...parseResponseHeaders(header), + ...parseContent(bodies) + } + return parsed + }, {}) + return { responses: parsedResponses } +} + +function parseContent (bodiesByLanguage) { + const content = Object.entries(bodiesByLanguage) + .reduce((content, [language, bodies]) => { + if (language === 'json') { + content['application/json'] = { + schema: { type: 'object' }, + ...parseExamples(bodies, 'json') + } + } else if (language === 'text') { + content['text/plain'] = { + schema: { type: 'string' }, + ...parseExamples(bodies, 'text') + } + } + return content + }, {}) + return { content } +} + +// TODO should not fail if body, json is not correct (empty) +// TODO should support string only +function parseExamples (bodies, language) { + if (Array.isArray(bodies) && bodies.length > 1) { + return { + examples: bodies.reduce((ex, { name: summary, body }, i) => { + ex[`example-${i}`] = { + summary, + value: (language === 'json') ? JSON.parse(body) : body + } + return ex + }, {}) + } + } else { + return { + example: (language === 'json') ? JSON.parse(bodies[0].body) : bodies[0].body + } + } +} + +function parseResponseHeaders (headerArray) { + const headers = headerArray.reduce((acc, { key, value }) => { + acc[key] = { + schema: { + type: inferType(value), + example: value + } + } + return acc + }, {}) + return (Object.keys(headers).length > 0) ? { headers } : {} +} + postmanToOpenApi.version = version module.exports = postmanToOpenApi diff --git a/test/index.spec.js b/test/index.spec.js index bf1154f..16dc306 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -371,6 +371,14 @@ describe('Library specs', function () { }) }) + // TODO add the user id description of the field in markdown or were is need it + it.only('should add responses from postman examples', async function () { + const result = await postmanToOpenApi('./test/resources/input/v21/Responses.json', OUTPUT_PATH, { pathDepth: 2 }) + console.log(result) + // equal(result, EXPECTED_BASIC) + }) + it('should add responses from multiple format for the same status code (text and json)') + it('should work if no options in request body', async function () { const result = await postmanToOpenApi(COLLECTION_NO_OPTIONS, OUTPUT_PATH, {}) equal(result, EXPECTED_BASIC) diff --git a/test/resources/input/v21/Responses.json b/test/resources/input/v21/Responses.json new file mode 100644 index 0000000..32d5dae --- /dev/null +++ b/test/resources/input/v21/Responses.json @@ -0,0 +1,444 @@ +{ + "info": { + "_postman_id": "7f7ad829-6db0-4229-9c38-5e9f341ec7bb", + "name": "Responses", + "description": "Postman collection with saved responses", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Create new User", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"100\",\n \"createdAt\": \"2021-06-04T15:50:38.568Z\",\n \"name\": \"Carol\",\n \"avatar\": \"https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg\"\n }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://60bb37ab42e1d000176206c3.mockapi.io/api/v1/users", + "protocol": "https", + "host": [ + "60bb37ab42e1d000176206c3", + "mockapi", + "io" + ], + "path": [ + "api", + "v1", + "users" + ] + }, + "description": "Create a new user into your amazing API" + }, + "response": [ + { + "name": "Create new User example", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"100\",\n \"createdAt\": \"2021-06-04T15:50:38.568Z\",\n \"name\": \"Carol\",\n \"avatar\": \"https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg\"\n }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://60bb37ab42e1d000176206c3.mockapi.io/api/v1/users", + "protocol": "https", + "host": [ + "60bb37ab42e1d000176206c3", + "mockapi", + "io" + ], + "path": [ + "api", + "v1", + "users" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Server", + "value": "Cowboy" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET,PUT,POST,DELETE,OPTIONS" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "X-Requested-With,Content-Type,Cache-Control,access_token" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Content-Length", + "value": "131" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Date", + "value": "Sat, 05 Jun 2021 08:42:32 GMT" + }, + { + "key": "Via", + "value": "1.1 vegur" + } + ], + "cookie": [], + "body": "{\n \"id\": \"51\",\n \"createdAt\": \"2021-06-04T15:50:38.568Z\",\n \"name\": \"Carol\",\n \"avatar\": \"https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg\"\n}" + }, + { + "name": "Create new User automatic id", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"createdAt\": \"2021-06-04T15:50:38.568Z\",\n \"name\": \"Carol\",\n \"avatar\": \"https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://60bb37ab42e1d000176206c3.mockapi.io/api/v1/users", + "protocol": "https", + "host": [ + "60bb37ab42e1d000176206c3", + "mockapi", + "io" + ], + "path": [ + "api", + "v1", + "users" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Server", + "value": "Cowboy" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET,PUT,POST,DELETE,OPTIONS" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "X-Requested-With,Content-Type,Cache-Control,access_token" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Content-Length", + "value": "131" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Date", + "value": "Sat, 05 Jun 2021 08:49:09 GMT" + }, + { + "key": "Via", + "value": "1.1 vegur" + } + ], + "cookie": [], + "body": "{\n \"id\": \"54\",\n \"createdAt\": \"2021-06-04T15:50:38.568Z\",\n \"name\": \"Carol\",\n \"avatar\": \"https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg\"\n}" + } + ] + }, + { + "name": "Get User data", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "file", + "file": {}, + "options": { + "raw": { + "language": "text" + } + } + }, + "url": { + "raw": "https://60bb37ab42e1d000176206c3.mockapi.io/api/v1/users/{{user_id}}", + "protocol": "https", + "host": [ + "60bb37ab42e1d000176206c3", + "mockapi", + "io" + ], + "path": [ + "api", + "v1", + "users", + "{{user_id}}" + ] + }, + "description": "Retrieve the user data\n\n# postman-to-openapi\n\n| object | name | description | required | type | example |\n|--------|----------|--------------------------------|----------|--------|-----------|\n| path | user_id | This is just a user identifier | true | number | 54 |" + }, + "response": [ + { + "name": "Get User data NOT FOUND", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "file", + "file": {}, + "options": { + "raw": { + "language": "text" + } + } + }, + "url": { + "raw": "https://60bb37ab42e1d000176206c3.mockapi.io/api/v1/users/100", + "protocol": "https", + "host": [ + "60bb37ab42e1d000176206c3", + "mockapi", + "io" + ], + "path": [ + "api", + "v1", + "users", + "100" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Server", + "value": "Cowboy" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET,PUT,POST,DELETE,OPTIONS" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "X-Requested-With,Content-Type,Cache-Control,access_token" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Content-Length", + "value": "11" + }, + { + "key": "Etag", + "value": "\"1592724850\"" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Date", + "value": "Sat, 05 Jun 2021 08:48:30 GMT" + }, + { + "key": "Via", + "value": "1.1 vegur" + } + ], + "cookie": [], + "body": "\"Not found\"" + }, + { + "name": "Get User data OK", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "file", + "file": {}, + "options": { + "raw": { + "language": "text" + } + } + }, + "url": { + "raw": "https://60bb37ab42e1d000176206c3.mockapi.io/api/v1/users/{{user_id}}", + "protocol": "https", + "host": [ + "60bb37ab42e1d000176206c3", + "mockapi", + "io" + ], + "path": [ + "api", + "v1", + "users", + "{{user_id}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Server", + "value": "Cowboy" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET,PUT,POST,DELETE,OPTIONS" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "X-Requested-With,Content-Type,Cache-Control,access_token" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Content-Length", + "value": "127" + }, + { + "key": "Etag", + "value": "\"1711725458\"" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Date", + "value": "Sat, 05 Jun 2021 08:48:00 GMT" + }, + { + "key": "Via", + "value": "1.1 vegur" + } + ], + "cookie": [], + "body": "{\n \"id\": \"50\",\n \"createdAt\": \"2021-06-04T23:41:02.287Z\",\n \"name\": \"Leanne\",\n \"avatar\": \"https://cdn.fakercloud.com/avatars/bartjo_128.jpg\"\n}" + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "version", + "value": "1.2.0" + }, + { + "key": "user_id", + "value": "50" + } + ] +} \ No newline at end of file diff --git a/test/resources/output/Responses.yml b/test/resources/output/Responses.yml new file mode 100644 index 0000000..2cad1c0 --- /dev/null +++ b/test/resources/output/Responses.yml @@ -0,0 +1,222 @@ +openapi: 3.0.0 +info: + title: Responses + description: Postman collection with saved responses + version: 1.2.0 +servers: + - url: https://60bb37ab42e1d000176206c3.mockapi.io +paths: + /users: + post: + tags: + - default + summary: Create new User + description: Create a new user into your amazing API + requestBody: + content: + application/json: + schema: + type: object + example: + id: '100' + createdAt: '2021-06-04T15:50:38.568Z' + name: Carol + avatar: https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg + responses: + '201': + description: Created + headers: + Server: + schema: + type: string + example: Cowboy + Connection: + schema: + type: string + example: keep-alive + X-Powered-By: + schema: + type: string + example: Express + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Access-Control-Allow-Methods: + schema: + type: string + example: GET,PUT,POST,DELETE,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: X-Requested-With,Content-Type,Cache-Control,access_token + Content-Type: + schema: + type: string + example: application/json + Content-Length: + schema: + type: integer + example: '131' + Vary: + schema: + type: string + example: Accept-Encoding + Date: + schema: + type: string + example: Sat, 05 Jun 2021 08:42:32 GMT + Via: + schema: + type: number + example: 1.1 vegur + content: + application/json: + schema: + type: object + examples: + example-0: + summary: Create new User example + value: + id: '51' + createdAt: '2021-06-04T15:50:38.568Z' + name: Carol + avatar: https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg + example-1: + summary: Create new User automatic id + value: + id: '54' + createdAt: '2021-06-04T15:50:38.568Z' + name: Carol + avatar: https://cdn.fakercloud.com/avatars/nelsonjoyce_128.jpg + /users/{user_id}: + get: + tags: + - default + summary: Get User data + description: Retrieve the user data + parameters: + - name: user_id + in: path + schema: + type: number + required: true + description: This is just a user identifier + example: '54' + responses: + '200': + description: OK + headers: + Server: + schema: + type: string + example: Cowboy + Connection: + schema: + type: string + example: keep-alive + X-Powered-By: + schema: + type: string + example: Express + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Access-Control-Allow-Methods: + schema: + type: string + example: GET,PUT,POST,DELETE,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: X-Requested-With,Content-Type,Cache-Control,access_token + Content-Type: + schema: + type: string + example: application/json + Content-Length: + schema: + type: integer + example: '127' + Etag: + schema: + type: string + example: '"1711725458"' + Vary: + schema: + type: string + example: Accept-Encoding + Date: + schema: + type: string + example: Sat, 05 Jun 2021 08:48:00 GMT + Via: + schema: + type: number + example: 1.1 vegur + content: + application/json: + schema: + type: object + example: + id: '50' + createdAt: '2021-06-04T23:41:02.287Z' + name: Leanne + avatar: https://cdn.fakercloud.com/avatars/bartjo_128.jpg + '404': + description: Not Found + headers: + Server: + schema: + type: string + example: Cowboy + Connection: + schema: + type: string + example: keep-alive + X-Powered-By: + schema: + type: string + example: Express + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Access-Control-Allow-Methods: + schema: + type: string + example: GET,PUT,POST,DELETE,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: X-Requested-With,Content-Type,Cache-Control,access_token + Content-Type: + schema: + type: string + example: application/json + Content-Length: + schema: + type: integer + example: '11' + Etag: + schema: + type: string + example: '"1592724850"' + Vary: + schema: + type: string + example: Accept-Encoding + Date: + schema: + type: string + example: Sat, 05 Jun 2021 08:48:30 GMT + Via: + schema: + type: number + example: 1.1 vegur + content: + text/plain: + schema: + type: text + example: '"Not found"'