Skip to content

Commit

Permalink
Merge pull request #122 from joolfe/develop
Browse files Browse the repository at this point in the history
Parse response examples
  • Loading branch information
joolfe authored Jul 16, 2021
2 parents 5a52d23 + 7854324 commit 8289a98
Show file tree
Hide file tree
Showing 16 changed files with 4,395 additions and 1,984 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## [1.13.0](https://github.com/joolfe/postman-to-openapi/compare/1.12.1...1.13.0) (2021-07-16)


### Features

* parse API response from postman examples ([b731f4e](https://github.com/joolfe/postman-to-openapi/commit/b731f4e2748622e85be1a93dc097d6723dfb0578))


### Documentation

* update docs about responses ([aa61018](https://github.com/joolfe/postman-to-openapi/commit/aa6101856a2d40dcfb794af50af96a2387a67608))


### Build System

* update deps and version ([65991ee](https://github.com/joolfe/postman-to-openapi/commit/65991ee1510db3429e636503e22d11e26cb123bd))

### [1.12.1](https://github.com/joolfe/postman-to-openapi/compare/1.12.0...1.12.1) (2021-06-11)


Expand Down
33 changes: 31 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* Contact and License from variables or by configuration.
* Provide meta-information as a markdown table.
* Path depth configuration.
* Response status code parse from test.
* API Response parse from postman examples and from test code (status code).
* [x-logo](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-logo) extension support

See [Features](#features) section for more details about how to use each of this features.
Expand Down Expand Up @@ -119,6 +119,17 @@ See demo in next gif:

The third parameter used in the library method is an `options` object containing the optional parameters for the transformation, the allowed parameters are:

| Param | Description |
|------------------|------------------------------------------------------------------------------------|
| [info](#info-object) | Basic API information |
| [defaultTag](#defaulttag-string) | Values of the default tag object. |
| [pathDepth](#pathdepth-number) | Number of subpaths that should be part of the operation path. |
| [auth](#auth-object) | Global authorization definition object. |
| [servers](#servers-array) | Server list for the OpenApi specs. |
| [externalDocs](#externaldocs-object) | Info about the API external documentation. |
| [folders](#folders-object) | Config object for folders and nested folders in postman collection. |
| [responseHeaders](#responseheaders-boolean) | Indicate if should parse the response headers from the collection examples. |

### info (Object)

The basic information of the API is obtained from Postman collection as described in section [default info](#basic-api-info), but you can customize this parameters using the `info` options that can contain the next parameters:
Expand Down Expand Up @@ -299,6 +310,12 @@ Avoid concatenation
</div></div>
<div class="tilted-section"><div markdown="1">

### responseHeaders (Boolean)

This flag indicates if the headers that are saved as part of the postman collection examples (see feature [Responses parsed from Postman collection examples](#responses-parsed-from-postman-collection-examples)) should be used in the OpenApi specification. This headers normally contain lot of unused headers but are automatically saved by postman when create an example, a better approach is to define response headers in a common way.

The default value is `true`, so headers are by default added to the response definition.

# Features

## Basic conversion
Expand Down Expand Up @@ -408,7 +425,19 @@ pm.response.to.have.status(201)
pm.expect(pm.response.code).to.eql(202)
```

The status code will be automatically parsed and used in the OpenAPI specification.
The status code will be automatically parsed and used in the OpenAPI specification, take into account that feature [Responses parsed from Postman collection examples](#responses-parsed-from-postman-collection-examples) has priority over this feature.

## Responses parsed from Postman collection examples

As described in [Postman docs](https://learning.postman.com/docs/sending-requests/examples/) is possible to save real responses from a server or create manually responses to save as examples in a postman request, this examples contain all the information about the request (method, url, headers, parameters...) and the corresponding response (body, headers, status code...) and will be automatically parsed by `postman-to-openapi` and added as an operation [Response Object Example/Examples](https://swagger.io/specification/) in the result OpenAPI specification.

Note that this examples will be added in OpenAPI specification as a 'Operation Object > Responses Objects > content > Media Type Object > example or examples' and not as a schema.

Actually multiple examples in the same request are supported with the same or different status code response as OpenAPI support the description of more than one example. The Supported Media Types in this moment are `application/json` and `text/plain`.

Take into account that this feature has priority over the [Response status code parse from Test](#response-status-code-parse-from-test) one so if `postman-to-openapi` detect that some example exist in the postman collection will no parse the test script.

If there are more than one example at request level the used headers will be the ones that appear in the last example in the postman collection.

</div></div>
<div class="tilted-section"><div markdown="1">
Expand Down
101 changes: 97 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const { version } = require('../package.json')

async function postmanToOpenApi (input, output, {
info = {}, defaultTag = 'default', pathDepth = 0,
auth: optsAuth, servers, externalDocs = {}, folders = {}
auth: optsAuth, servers, externalDocs = {}, folders = {},
responseHeaders = true
} = {}) {
// TODO validate?
const collectionFile = await readFile(input)
Expand All @@ -30,7 +31,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) {
Expand All @@ -45,7 +46,7 @@ async function postmanToOpenApi (input, output, {
...parseBody(body, method),
...parseOperationAuth(auth, securitySchemes, optsAuth),
...parseParameters(query, header, joinedPath, paramsMeta),
responses: parseResponse(events)
...parseResponse(response, events, responseHeaders)
}
}
}
Expand Down Expand Up @@ -353,7 +354,15 @@ function descriptionParse (description) {
}
}

function parseResponse (events = []) {
function parseResponse (responses, events, responseHeaders) {
if (responses != null && Array.isArray(responses) && responses.length > 0) {
return parseResponseFromExamples(responses, responseHeaders)
} else {
return { responses: parseResponseFromEvents(events) }
}
}

function parseResponseFromEvents (events = []) {
let status = 200
const test = events.filter(event => event.listen === 'test')
if (test.length > 0) {
Expand All @@ -371,6 +380,90 @@ function parseResponse (events = []) {
}
}

function parseResponseFromExamples (responses, responseHeaders) {
// 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, responseHeaders),
...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 {
content['text/plain'] = {
schema: { type: 'string' },
...parseExamples(bodies, 'text')
}
}
return content
}, {})
return { content }
}

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, responseHeaders) {
if (!responseHeaders) {
return {}
}
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
Loading

0 comments on commit 8289a98

Please sign in to comment.