mirror of https://github.com/Budibase/budibase.git
6 changed files with 540 additions and 31 deletions
@ -0,0 +1,31 @@ |
|||
### Documentation |
|||
|
|||
This directory contains the scripts required to generate the APIDoc based documentation. |
|||
You can find the docs about comment structure at the [APIDocs page](https://apidocjs.com/). |
|||
|
|||
In general most API endpoints will look like: |
|||
```js |
|||
/** |
|||
* @api {post} /api/:param/url Give it a name |
|||
* @apiName Give it a name |
|||
* @apiGroup group |
|||
* @apiPermission permission |
|||
* @apiDescription Describe what the endpoint does, any special cases the user |
|||
* should be aware of. |
|||
* |
|||
* @apiParam {string} param describe a URL parameter. |
|||
* |
|||
* @apiParam (Body) input describe a field on the body. |
|||
* |
|||
* @apiSuccess {object} output describe the output. |
|||
*/ |
|||
``` |
|||
|
|||
There are a few key points to note when writing API docs: |
|||
1. Don't use `@apiBody` - this isn't currently supported by our swagger converter. |
|||
2. Make sure to always have an `@api` definition at the start, which must always have the |
|||
HTTP verb, the endpoint URL and the name. |
|||
3. There are three ways you can specify parameters used as inputs for your endpoint, |
|||
`@apiParam` for a URL param, `@apiParam (Body)` for a field on the request body and `@apiParam (Query)` |
|||
for query string parameters. |
|||
4. The `@apiGroup` should be the same for all API Doc comments in a route file. |
|||
@ -0,0 +1,320 @@ |
|||
let _ = require("lodash") |
|||
let { pathToRegexp } = require("path-to-regexp") |
|||
|
|||
/******************************************************** |
|||
* Based on: https://github.com/fsbahman/apidoc-swagger *
|
|||
********************************************************/ |
|||
|
|||
let swagger = { |
|||
swagger: "2.0", |
|||
info: {}, |
|||
paths: {}, |
|||
definitions: {}, |
|||
} |
|||
|
|||
function toSwagger(apidocJson, projectJson) { |
|||
swagger.info = addInfo(projectJson) |
|||
swagger.paths = extractPaths(apidocJson) |
|||
return swagger |
|||
} |
|||
|
|||
let tagsRegex = /(<([^>]+)>)/gi |
|||
// Removes <p> </p> tags from text
|
|||
function removeTags(text) { |
|||
return text ? text.replace(tagsRegex, "") : text |
|||
} |
|||
|
|||
function addInfo(projectJson) { |
|||
let info = {} |
|||
info["title"] = projectJson.title || projectJson.name |
|||
info["version"] = projectJson.version |
|||
info["description"] = projectJson.description |
|||
return info |
|||
} |
|||
|
|||
/** |
|||
* Extracts paths provided in json format |
|||
* post, patch, put request parameters are extracted in body |
|||
* get and delete are extracted to path parameters |
|||
* @param apidocJson |
|||
* @returns {{}} |
|||
*/ |
|||
function extractPaths(apidocJson) { |
|||
let apiPaths = groupByUrl(apidocJson) |
|||
let paths = {} |
|||
for (let i = 0; i < apiPaths.length; i++) { |
|||
let verbs = apiPaths[i].verbs |
|||
let url = verbs[0].url |
|||
let pattern = pathToRegexp(url, null) |
|||
let matches = pattern.exec(url) |
|||
|
|||
// Surrounds URL parameters with curly brackets -> :email with {email}
|
|||
let pathKeys = [] |
|||
for (let j = 1; j < matches.length; j++) { |
|||
let key = matches[j].substr(1) |
|||
url = url.replace(matches[j], "{" + key + "}") |
|||
pathKeys.push(key) |
|||
} |
|||
|
|||
for (let j = 0; j < verbs.length; j++) { |
|||
let verb = verbs[j] |
|||
let type = verb.type |
|||
|
|||
let obj = (paths[url] = paths[url] || {}) |
|||
|
|||
if (type === "post" || type === "patch" || type === "put") { |
|||
_.extend( |
|||
obj, |
|||
createPostPushPutOutput(verb, swagger.definitions, pathKeys) |
|||
) |
|||
} else { |
|||
_.extend(obj, createGetDeleteOutput(verb, swagger.definitions)) |
|||
} |
|||
} |
|||
} |
|||
return paths |
|||
} |
|||
|
|||
function createPostPushPutOutput(verbs, definitions, pathKeys) { |
|||
let pathItemObject = {} |
|||
let verbDefinitionResult = createVerbDefinitions(verbs, definitions) |
|||
|
|||
let params = [] |
|||
let pathParams = createPathParameters(verbs, pathKeys) |
|||
pathParams = _.filter(pathParams, function (param) { |
|||
let hasKey = pathKeys.indexOf(param.name) !== -1 |
|||
return !(param.in === "path" && !hasKey) |
|||
}) |
|||
|
|||
params = params.concat(pathParams) |
|||
let required = |
|||
verbs.parameter && |
|||
verbs.parameter.fields && |
|||
verbs.parameter.fields.Parameter && |
|||
verbs.parameter.fields.Parameter.length > 0 |
|||
|
|||
params.push({ |
|||
in: "body", |
|||
name: "body", |
|||
description: removeTags(verbs.description), |
|||
required: required, |
|||
schema: { |
|||
$ref: "#/definitions/" + verbDefinitionResult.topLevelParametersRef, |
|||
}, |
|||
}) |
|||
|
|||
pathItemObject[verbs.type] = { |
|||
tags: [verbs.group], |
|||
summary: removeTags(verbs.description), |
|||
consumes: ["application/json"], |
|||
produces: ["application/json"], |
|||
parameters: params, |
|||
} |
|||
|
|||
if (verbDefinitionResult.topLevelSuccessRef) { |
|||
pathItemObject[verbs.type].responses = { |
|||
200: { |
|||
description: "successful operation", |
|||
schema: { |
|||
type: verbDefinitionResult.topLevelSuccessRefType, |
|||
items: { |
|||
$ref: "#/definitions/" + verbDefinitionResult.topLevelSuccessRef, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
return pathItemObject |
|||
} |
|||
|
|||
function createVerbDefinitions(verbs, definitions) { |
|||
let result = { |
|||
topLevelParametersRef: null, |
|||
topLevelSuccessRef: null, |
|||
topLevelSuccessRefType: null, |
|||
} |
|||
let defaultObjectName = verbs.name |
|||
|
|||
let fieldArrayResult = {} |
|||
if (verbs && verbs.parameter && verbs.parameter.fields) { |
|||
fieldArrayResult = createFieldArrayDefinitions( |
|||
verbs.parameter.fields.Parameter, |
|||
definitions, |
|||
verbs.name, |
|||
defaultObjectName |
|||
) |
|||
result.topLevelParametersRef = fieldArrayResult.topLevelRef |
|||
} |
|||
|
|||
if (verbs && verbs.success && verbs.success.fields) { |
|||
fieldArrayResult = createFieldArrayDefinitions( |
|||
verbs.success.fields["Success 200"], |
|||
definitions, |
|||
verbs.name, |
|||
defaultObjectName |
|||
) |
|||
result.topLevelSuccessRef = fieldArrayResult.topLevelRef |
|||
result.topLevelSuccessRefType = fieldArrayResult.topLevelRefType |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
function createFieldArrayDefinitions( |
|||
fieldArray, |
|||
definitions, |
|||
topLevelRef, |
|||
defaultObjectName |
|||
) { |
|||
let result = { |
|||
topLevelRef: topLevelRef, |
|||
topLevelRefType: null, |
|||
} |
|||
|
|||
if (!fieldArray) { |
|||
return result |
|||
} |
|||
|
|||
for (let i = 0; i < fieldArray.length; i++) { |
|||
let parameter = fieldArray[i] |
|||
|
|||
let nestedName = createNestedName(parameter.field) |
|||
let objectName = nestedName.objectName |
|||
if (!objectName) { |
|||
objectName = defaultObjectName |
|||
} |
|||
let type = parameter.type |
|||
if (i === 0) { |
|||
result.topLevelRefType = type |
|||
if (parameter.type === "Object") { |
|||
objectName = nestedName.propertyName |
|||
nestedName.propertyName = null |
|||
} else if (parameter.type === "Array") { |
|||
objectName = nestedName.propertyName |
|||
nestedName.propertyName = null |
|||
result.topLevelRefType = "array" |
|||
} |
|||
result.topLevelRef = objectName |
|||
} |
|||
|
|||
definitions[objectName] = definitions[objectName] || { |
|||
properties: {}, |
|||
required: [], |
|||
} |
|||
|
|||
if (nestedName.propertyName) { |
|||
let prop = { |
|||
type: (parameter.type || "").toLowerCase(), |
|||
description: removeTags(parameter.description), |
|||
} |
|||
if (parameter.type === "Object") { |
|||
prop.$ref = "#/definitions/" + parameter.field |
|||
} |
|||
|
|||
let typeIndex = type.indexOf("[]") |
|||
if (typeIndex !== -1 && typeIndex === type.length - 2) { |
|||
prop.type = "array" |
|||
prop.items = { |
|||
type: type.slice(0, type.length - 2), |
|||
} |
|||
} |
|||
|
|||
definitions[objectName]["properties"][nestedName.propertyName] = prop |
|||
if (!parameter.optional) { |
|||
let arr = definitions[objectName]["required"] |
|||
if (arr.indexOf(nestedName.propertyName) === -1) { |
|||
arr.push(nestedName.propertyName) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
function createNestedName(field) { |
|||
let propertyName = field |
|||
let objectName |
|||
let propertyNames = field.split(".") |
|||
if (propertyNames && propertyNames.length > 1) { |
|||
propertyName = propertyNames[propertyNames.length - 1] |
|||
propertyNames.pop() |
|||
objectName = propertyNames.join(".") |
|||
} |
|||
|
|||
return { |
|||
propertyName: propertyName, |
|||
objectName: objectName, |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Generate get, delete method output |
|||
* @param verbs |
|||
* @param definitions |
|||
* @returns {{}} |
|||
*/ |
|||
function createGetDeleteOutput(verbs, definitions) { |
|||
let pathItemObject = {} |
|||
verbs.type = verbs.type === "del" ? "delete" : verbs.type |
|||
|
|||
let verbDefinitionResult = createVerbDefinitions(verbs, definitions) |
|||
pathItemObject[verbs.type] = { |
|||
tags: [verbs.group], |
|||
summary: removeTags(verbs.description), |
|||
consumes: ["application/json"], |
|||
produces: ["application/json"], |
|||
parameters: createPathParameters(verbs), |
|||
} |
|||
if (verbDefinitionResult.topLevelSuccessRef) { |
|||
pathItemObject[verbs.type].responses = { |
|||
200: { |
|||
description: "successful operation", |
|||
schema: { |
|||
type: verbDefinitionResult.topLevelSuccessRefType, |
|||
items: { |
|||
$ref: "#/definitions/" + verbDefinitionResult.topLevelSuccessRef, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
return pathItemObject |
|||
} |
|||
|
|||
/** |
|||
* Iterate through all method parameters and create array of parameter objects which are stored as path parameters |
|||
* @param verbs |
|||
* @returns {Array} |
|||
*/ |
|||
function createPathParameters(verbs) { |
|||
let pathItemObject = [] |
|||
if (verbs.parameter && verbs.parameter.fields.Parameter) { |
|||
for (let i = 0; i < verbs.parameter.fields.Parameter.length; i++) { |
|||
let param = verbs.parameter.fields.Parameter[i] |
|||
let field = param.field |
|||
let type = param.type |
|||
pathItemObject.push({ |
|||
name: field, |
|||
in: type === "file" ? "formData" : "path", |
|||
required: !param.optional, |
|||
type: param.type.toLowerCase(), |
|||
description: removeTags(param.description), |
|||
}) |
|||
} |
|||
} |
|||
return pathItemObject |
|||
} |
|||
|
|||
function groupByUrl(apidocJson) { |
|||
return _.chain(apidocJson) |
|||
.groupBy("url") |
|||
.toPairs() |
|||
.map(function (element) { |
|||
return _.zipObject(["url", "verbs"], element) |
|||
}) |
|||
.value() |
|||
} |
|||
|
|||
module.exports = toSwagger |
|||
Loading…
Reference in new issue