Requires json2yaml
and, of course, ActionHero itself installed in the project. This script is fairly crude
and has some hard-coded behaviors, such as expecting to find data models defined on api.models
. It is provided
here only for reference.
Created
January 23, 2019 18:52
-
-
Save crrobinson14/c08356126654dc1c6c179b08dc0029f3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
const fs = require('fs'); | |
const path = require('path'); | |
const json2yaml = require('json2yaml'); | |
const { Process, api } = require('actionhero'); | |
const actionhero = new Process(); | |
// eslint-disable-next-line import/no-dynamic-require | |
const packageJson = require(path.join(process.cwd(), 'package.json')); | |
class Swagger { | |
constructor() { | |
this.doc = {}; | |
this.definitions = {}; | |
} | |
static standardResponses() { | |
return { | |
401: { | |
description: 'Session Error. Required authentication was missing or invalid for the requested resource.' | |
}, | |
403: { | |
description: 'Forbidden. Authentication was provided, but not sufficient for the request.' | |
}, | |
404: { | |
description: 'Not Found. The primary content related to the request does not exist.' | |
}, | |
405: { | |
description: 'Request Error. An required input parameter was missing or invalid.' | |
}, | |
429: { | |
description: 'Quota Exceeded. Reduce request rate.' | |
}, | |
500: { | |
description: 'Internal server error. Try again later.' | |
} | |
}; | |
} | |
static typeLookup(attribute) { | |
const types = { | |
STRING: 'string', | |
CHAR: 'string', | |
TEXT: 'string', | |
NUMBER: 'integer', | |
INTEGER: 'integer', | |
BIGINT: 'integer', | |
FLOAT: 'number', | |
TIME: 'string', | |
DATE: 'string', | |
DATEONLY: 'string', | |
BOOLEAN: 'boolean', | |
NOW: 'string', | |
BLOB: 'string', | |
DECIMAL: 'number', | |
NUMERIC: 'number', | |
UUID: 'string', | |
UUIDV1: 'string', | |
UUIDV4: 'string', | |
ENUM: 'string', | |
INT32: 'integer', | |
INT64: 'integer', | |
DOUBLE: 'number', | |
BYTE: 'string', | |
'DATE-TIME': 'string', | |
VARCHAR: 'string', | |
TIMESTAMP: 'string', | |
REAL: 'number', | |
}; | |
const formats = { | |
INTEGER: 'int32', | |
INT32: 'integer', | |
INT64: 'int64', | |
BIGINT: 'int64', | |
FLOAT: 'float', | |
DOUBLE: 'double', | |
DATE: 'date-time', | |
'DATE-TIME': 'date-time', | |
DATEONLY: 'date', | |
BLOB: 'binary', | |
}; | |
const typeKey = attribute.type.key; | |
const type = { | |
type: types[typeKey] || 'string', | |
description: attribute.comment, | |
}; | |
if (formats[typeKey]) { | |
type.format = formats[typeKey]; | |
} | |
if (typeKey === 'ENUM') { | |
type.enum = attribute.type.values; | |
} | |
if (typeKey === 'UUID') { | |
type.maxLength = 36; | |
} | |
if (attribute.allowNull) { | |
type.nullable = true; | |
} | |
return type; | |
} | |
generate() { | |
return this | |
.header() | |
.paths() | |
.objectDefinitions(); | |
} | |
// toJSON() { | |
// return this.doc; | |
// } | |
toYAML() { | |
return json2yaml.stringify(this.doc); | |
} | |
header() { | |
const swaggerMeta = packageJson.swagger || {}; | |
Object.assign(this.doc, { | |
swagger: '2.0', | |
info: { | |
description: swaggerMeta.description || packageJson.description, | |
version: swaggerMeta.version || packageJson.version, | |
title: swaggerMeta.title || packageJson.name, | |
termsOfService: swaggerMeta.termsOfService || 'http://www.snap-interactive.com/terms-of-service/', | |
contact: { | |
email: swaggerMeta.contact || packageJson.author | |
} | |
}, | |
host: swaggerMeta.uri || `${packageJson.name}.apis.theirweb.net`, | |
basePath: swaggerMeta.basePath || '/v1', | |
schemes: swaggerMeta.schemes || ['https'], | |
}); | |
return this; | |
} | |
getActionTemplate(route) { | |
const versions = Object.keys(api.actions.actions[route.action]).sort(); | |
const version = versions[versions.length - 1]; | |
return api.actions.actions[route.action][version]; | |
} | |
paths() { | |
this.doc.paths = {}; | |
Object.keys(api.config.routes).sort().forEach(method => { | |
api.config.routes[method].forEach(route => { | |
const template = this.getActionTemplate(route); | |
this.doc.paths[route.path] = this.doc.paths[route.path] || {}; | |
this.doc.paths[route.path][method] = this.doc.paths[route.path][method] || {}; | |
if (template.swaggerTags) { | |
this.doc.paths[route.path][method].tags = template.swaggerTags; | |
} | |
this.doc.paths[route.path][method].summary = template.name; | |
this.doc.paths[route.path][method].description = template.description; | |
this.doc.paths[route.path][method].operationId = template.name; | |
this.doc.paths[route.path][method].consumes = ['application/json']; | |
this.doc.paths[route.path][method].produces = ['application/json']; | |
const successResponseSchema = {}; | |
Object.keys(template.successResult.result).sort().forEach(key => { | |
const { type } = template.successResult.result[key]; | |
if (['string', 'object', 'number', 'integer', 'boolean'].indexOf(type) !== -1) { | |
successResponseSchema.type = type; | |
} else if (type === 'array') { | |
successResponseSchema.type = type; | |
const items = template.successResult.result[key].items.substring(1); | |
this.definitions[items] = true; | |
successResponseSchema.items = { | |
$ref: `#/definitions/${items}` | |
}; | |
} else if (type.charAt(0) === '#') { | |
const model = type.substring(1); | |
this.definitions[model] = true; | |
successResponseSchema.$ref = `#/definitions/${model}`; | |
} | |
}); | |
const successResponse = { | |
200: { | |
description: template.successResult.description, | |
headers: template.successResult.headers, | |
schema: successResponseSchema, | |
} | |
}; | |
this.doc.paths[route.path][method].responses = Object.assign( | |
successResponse, | |
Swagger.standardResponses() | |
); | |
if (template.inputs) { | |
this.doc.paths[route.path][method].parameters = []; | |
Object.keys(template.inputs).sort().forEach(inputKey => { | |
const input = template.inputs[inputKey]; | |
const paramLocation = input.type && input.type.charAt(0) === '#' ? 'body' : 'query'; | |
const param = { | |
in: paramLocation, // #User for example would be a post or put | |
name: inputKey, | |
description: input.description, | |
required: input.required, | |
}; | |
if (paramLocation === 'body') { | |
const definition = input.type.substring(1); | |
this.definitions[definition] = true; | |
param.schema = { | |
$ref: `#/definitions/${definition}` | |
}; | |
} else { | |
param.type = input.type || 'string'; | |
} | |
this.doc.paths[route.path][method].parameters.push(param); | |
}); | |
} | |
}); | |
}); | |
return this; | |
} | |
objectDefinitions() { | |
Object.keys(api.models || {}).sort().forEach(definition => { | |
const rawAttributes = api.models[definition].rawAttributes || {}; | |
const properties = {}; | |
Object.keys(rawAttributes).sort().forEach(key => { | |
properties[key] = Swagger.typeLookup(rawAttributes[key]); | |
}); | |
this.doc.definitions = this.doc.definitions || {}; | |
this.doc.definitions[definition] = { | |
type: 'object', | |
description: api.models[definition].options.comment, | |
properties | |
}; | |
}); | |
return this; | |
} | |
} | |
async function generate() { | |
await actionhero.initialize(); | |
api.log(' >> Generating Swagger spec'); | |
const yamlDoc = new Swagger().generate().toYAML(); | |
fs.writeFileSync(path.join('docs', 'swagger.yml'), yamlDoc); | |
} | |
if (require.main === module) { | |
generate() | |
.then(() => process.exit(0)) | |
.catch(e => { | |
console.error(e); | |
process.exit(-1); | |
}); | |
} else { | |
module.exports = Swagger; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
node ./ah-swaggergen && swagger-markdown -i docs/swagger.yml -o docs/api.md |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment