Skip to content
This repository has been archived by the owner on Dec 27, 2024. It is now read-only.

Commit

Permalink
feat: path parameters enhancement #43
Browse files Browse the repository at this point in the history
Closes #43
  • Loading branch information
joolfe committed Aug 18, 2020
1 parent be35516 commit a90c4a1
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 16 deletions.
26 changes: 25 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Support Json and Text body formats.
- Global Authorization parse or by configuration (Basic and Bearer).
- Contact and License from variables or by configuration.
- Provide meta-information as a markdown table.

See [Features](#features) section for more details about how to use each of this features.

Expand Down Expand Up @@ -206,7 +207,7 @@ This library automatically transform query and headers parameters from Postman o

The default schema used for parameters is `string` but the library try to infer the type of the parameters based on the value using regular expressions, the detected types are `integer`, `number`, `boolean` and `string`, if you find any problem in the inference process please open an issue.

Path parameters are also automatically detected, this library look for [Postman variables](https://learning.postman.com/docs/sending-requests/variables/) in the url as `{{variable}}` and transform to a single curly brace expression as `{variable}` as supported by OpenAPI, also create the parameter definition using the variable name.
Path parameters are also automatically detected, this library look for [Postman variables](https://learning.postman.com/docs/sending-requests/variables/) in the url as `{{variable}}` and transform to a single curly brace expression as `{variable}` as supported by OpenAPI, also create the parameter definition using the variable name. To provide additional information about a path parameter you can [Pass Meta-information as markdown](#pass-meta-information-as-markdown).

For headers and query fields you can indicate that this parameter is mandatory/required adding into the description the literal `[required]`. The library use a case insensitive regexp so all variations are supported (`[REQUIRED]`, `[Required]`...) and never mind the location inside the description (at the beginning, at the end...).

Expand Down Expand Up @@ -246,6 +247,29 @@ The variables names will be in dot notation, for example for `contact` fields wi

You can also customize this information using the [Info option](#info-(object)), note that info provided by options will overwrite the variables inside the Postman collection (has more priority) but values will be merged from both sources (postman variables and options).

## Pass Meta-information as markdown

As Postman don't provide a free way to pass meta information in all the sections, for example you cannot describe a Path parameter in Postman, the easy way we have found is to provide this information in the `options` parameter when calling the library, although this solution is not a bad solution, and give lot of freedom about where you store the info, we think that have all the info about the API in the Postman Collection is the best solution as you only have a source of information for document your APIs.

That's the reason why API `version` can be defined as a postman collection variable, as described in [Basic API Info](#basic-api-info) section, but for some other information as for example describing a Path parameter where you should indicate multiples values as the description, if it is required, an example, schema type.... the solution of use collection variables don't fit too well, for this reason we have add support for provide Meta-Information as a markdown table.

Taking advantage that Postman support markdown in description fields we have defined a especial section delimited with a md header `# postman-to-openapi`, where you can define a markdown table for provide Meta-Information. As an example:

```markdown
# postman-to-openapi

| object | name | description | required | type | example |
|--------|----------|--------------------------------|----------|--------|-----------|
| path | user_id | This is just a user identifier | true | number | 476587598 |
| path | group_id | Group of the user | true | string | RETAIL |
```

This table is providing additional information about a Path parameter, the supported field in this moment are the column thats appear in the example. This way of provide Meta-information is supported in the Postman request description in this moment.

Take into account that `postman-to-openapi` remove from the description all the content after the key header `# postman-to-openapi`, so the meta-information table should be the last content of the description field.

Have a look to the collections [PathParams](https://github.com/joolfe/postman-to-openapi/blob/master/test/resources/input/PathParams.json) for examples of how to use this feature.

</div></div>
<div class="tilted-section"><div markdown="1">

Expand Down
42 changes: 31 additions & 11 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { promises: { writeFile, readFile } } = require('fs')
const { safeDump } = require('js-yaml')
const { parseMdTable } = require('./md-utils')

async function postmanToOpenApi (input, output, { info = {}, defaultTag = 'default', auth, servers } = {}) {
// TODO validate?
Expand All @@ -21,18 +22,20 @@ async function postmanToOpenApi (input, output, { info = {}, defaultTag = 'defau
element = tagged.shift()
}
const {
request: { url: { path, query, protocol, host }, method, body, description, header },
request: { url: { path, query, protocol, host }, method, body, description: rawDesc, header },
name: summary, tag = defaultTag
} = element
domains.add(calculateDomains(protocol, host))
const joinedPath = calculatePath(path)
if (!paths[joinedPath]) paths[joinedPath] = {}
const { description, paramsMeta } = descriptionParse(rawDesc)

paths[joinedPath][method.toLowerCase()] = {
tags: [tag],
summary,
...(description ? { description } : {}),
...parseBody(body, method),
...parseParameters(query, header, joinedPath),
...parseParameters(query, header, joinedPath, paramsMeta),
responses: {
200: {
description: 'Successful response',
Expand Down Expand Up @@ -139,13 +142,13 @@ function parseBody (body = {}, method) {
}

/* Parse the Postman query and header and transform into OpenApi parameters */
function parseParameters (query = [], header, paths) {
function parseParameters (query = [], header, paths, paramsMeta = {}) {
// parse Headers
let parameters = header.reduce(mapParameters('header'), [])
// parse Query
parameters = query.reduce(mapParameters('query'), parameters)
// Path params
parameters.push(...extractPathParameters(paths))
parameters.push(...extractPathParameters(paths, paramsMeta))
return (parameters.length) ? { parameters } : {}
}

Expand All @@ -165,14 +168,21 @@ function mapParameters (type) {
}
}

function extractPathParameters (path) {
function extractPathParameters (path, paramsMeta) {
const matched = path.match(/{\s*[\w]+\s*}/g) || []
return matched.map(match => ({
name: match.slice(1, -1),
in: 'path',
schema: { type: 'string' },
required: true
}))
return matched.map(match => {
const name = match.slice(1, -1)
const { type = 'string', description, example } = paramsMeta[name] || {}
return {
name,
in: 'path',
schema: { type },
required: true,
...(description ? { description } : {}),
...(example ? { example } : {})
}
}
)
}

function getVarValue (variables, name, def = undefined) {
Expand Down Expand Up @@ -264,4 +274,14 @@ function parseTags (tagsObj) {
return (tags.length > 0) ? { tags } : {}
}

function descriptionParse (description) {
if (description == null) return { description }
const splitDesc = description.split(/# postman-to-openapi/gi)
if (splitDesc.length === 1) return { description }
return {
description: splitDesc[0].trim(),
paramsMeta: parseMdTable(splitDesc[1])
}
}

module.exports = postmanToOpenApi
26 changes: 26 additions & 0 deletions lib/md-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict'

const marked = require('marked')
const supHeaders = ['object', 'name', 'description', 'example', 'type', 'required']

function parseMdTable (md) {
const parsed = marked.lexer(md)
const table = parsed.find(el => el.type === 'table')
if (table == null) return {}
const { header, cells } = table
if (!header.includes('object') || !header.includes('name')) return {}
const headers = header.map(h => supHeaders.includes(h) ? h : false)
const tableObj = cells.reduce((accTable, cell, i) => {
const cellObj = cell.reduce((accCell, field, index) => {
if (headers[index]) {
accCell[headers[index]] = field
}
return accCell
}, {})
accTable[cellObj.name] = cellObj
return accTable
}, {})
return tableObj
}

module.exports = { parseMdTable }
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"check-coverage": true
},
"dependencies": {
"js-yaml": "^3.14.0"
"js-yaml": "^3.14.0",
"marked": "^1.1.1"
}
}
5 changes: 5 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ describe('Library specs', function () {

it('should parse path params', async function () {
const result = await postmanToOpenApi(COLLECTION_PATH_PARAMS, OUTPUT_PATH)
console.log(result)
equal(result, EXPECTED_PATH_PARAMS)
})

Expand Down Expand Up @@ -209,4 +210,8 @@ describe('Library specs', function () {
})
equal(result, EXPECTED_LICENSE_CONTACT_PARTIAL_2)
})

it('should work with multiple params in path')

it('should work with multiple paths')
})
94 changes: 94 additions & 0 deletions test/md-utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict'

const { describe, it } = require('mocha')
const { parseMdTable } = require('../lib/md-utils')
const { deepEqual } = require('assert').strict

describe('MD table to Json specs', function () {
const MD_WITH_ADDITIONS = '| object | name | description | required | type | example |\n' +
'|--------------|------|----------|-----|-------|-----|\n' +
'| path | user_id | This is just a user identifier | true | string | 476587598 |\n' +
'| path | group_id | Group of the user | true | string | RETAIL |\n' +
'# hola'
const MD_ADDITIONAL_HEADER = '| object | name | description | required | type | additional | example |\n' +
'|--------------|------|----------|-----|-------|-----|-----|\n' +
'| path | user_id | This is just a user identifier | true | string | add field 1 | 476587598 |\n' +
'| path | group_id | Group of the user | true | string | add field 2 | RETAIL |\n' +
'# hola'
const MD_NO_OBJECT = '| name | description | required | type | additional | example |\n' +
'|------|----------|-----|-------|-----|-----|\n' +
'| user_id | This is just a user identifier | true | string | add field 1 | 476587598 |\n'
const MD_NO_NAME = '| object | description | required | type | additional | example |\n' +
'|------|----------|-----|-------|-----|-----|\n' +
'| user_id | This is just a user identifier | true | string | add field 1 | 476587598 |\n'
const MD_MISS_COLUMN = '| object | name | description | type | additional | example |\n' +
'|--------------|------|----------|-----|-------|-----|\n' +
'| path | user_id | This is just a user identifier | string | add field 1 | 476587598 |\n' +
'# hola'
const MD_INCORRECT = '| object | name | description | required | type | example |\n' +
'|--------------|------|----------|-----|-----|\n' +
'| path | user_id | This is just a user identifier | true | string | 476587598 |\n' +
'| path | group_id | Group of the user | true | string | RETAIL | RETAIL |\n' +
'# hola'
const PARSED_TABLE = {
user_id: {
object: 'path',
name: 'user_id',
description: 'This is just a user identifier',
required: 'true',
type: 'string',
example: '476587598'
},
group_id: {
object: 'path',
name: 'group_id',
description: 'Group of the user',
required: 'true',
type: 'string',
example: 'RETAIL'
}
}
const PARSED_NO_REQUIRED_TABLE = {
user_id: {
object: 'path',
name: 'user_id',
description: 'This is just a user identifier',
type: 'string',
example: '476587598'
}
}

it('should return a json object with parsed md table', function () {
const parsed = parseMdTable(MD_WITH_ADDITIONS)
deepEqual(parsed, PARSED_TABLE)
})

it('should return empty if not a markdown string', function () {
deepEqual(parseMdTable(''), {})
})

it('should return empty if not a table in the markdown', function () {
deepEqual(parseMdTable('# headers'), {})
})

it('should not parse not allowed headers', function () {
deepEqual(parseMdTable(MD_ADDITIONAL_HEADER), PARSED_TABLE)
})

it('should return empty if no "name" column', function () {
deepEqual(parseMdTable(MD_NO_NAME), {})
})

it('should return empty if no "object" column', function () {
deepEqual(parseMdTable(MD_NO_OBJECT), {})
})

it('should not fail if column as "required" not exist in md table', function () {
const parsed = parseMdTable(MD_MISS_COLUMN)
deepEqual(parsed, PARSED_NO_REQUIRED_TABLE)
})

it('should return empty if incorrect md table', function () {
deepEqual(parseMdTable(MD_INCORRECT), {})
})
})
27 changes: 24 additions & 3 deletions test/resources/input/PathParams.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "74fffb64-a73a-445f-aafc-3021f381a886",
"_postman_id": "c2a620d9-979e-4f39-b6bc-22282ce4b21a",
"name": "Path Params",
"description": "Collection to test path parameters",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
Expand All @@ -26,13 +26,34 @@
"description": "Obtain a list of users that fullfill the conditions of the filters"
},
"response": []
},
{
"name": "Get one users with description",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.io/desc/{{user_id}}",
"protocol": "https",
"host": [
"api",
"io"
],
"path": [
"desc",
"{{user_id}}"
]
},
"description": "Obtain a list of users descriptions\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 | 476587598 |\n| path | group_id | Group of the user | true | string | RETAIL |"
},
"response": []
}
],
"event": [
{
"listen": "prerequest",
"script": {
"id": "ce24dfe2-b234-4cb3-9930-ab0992f07671",
"id": "3d4c9432-87d0-46d1-8e0a-7bf78b95838f",
"type": "text/javascript",
"exec": [
""
Expand All @@ -42,7 +63,7 @@
{
"listen": "test",
"script": {
"id": "749d3a75-8078-44bf-bd5e-4b0c7dc6125c",
"id": "bfa95668-f3ba-423f-93d3-152fbba765af",
"type": "text/javascript",
"exec": [
""
Expand Down
19 changes: 19 additions & 0 deletions test/resources/output/PathParams.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,22 @@ paths:
description: Successful response
content:
application/json: {}
'/desc/{user_id}':
get:
tags:
- default
summary: Get one users with description
description: Obtain a list of users descriptions
parameters:
- name: user_id
in: path
schema:
type: number
required: true
description: This is just a user identifier
example: '476587598'
responses:
'200':
description: Successful response
content:
application/json: {}

0 comments on commit a90c4a1

Please sign in to comment.