Skip to content

Instantly share code, notes, and snippets.

@crrobinson14
Created January 23, 2019 18:52
Show Gist options
  • Save crrobinson14/c08356126654dc1c6c179b08dc0029f3 to your computer and use it in GitHub Desktop.
Save crrobinson14/c08356126654dc1c6c179b08dc0029f3 to your computer and use it in GitHub Desktop.

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.

#!/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;
}
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