@coorpacademy/baucis-openapi3
Version:
Generate customizable OpenAPI version 3.0.0 definitions for your Baucis REST API.
311 lines (270 loc) • 8.16 kB
JavaScript
const _ = require('lodash/fp');
const utils = require('./utils');
const params = require('./parameters');
// Implements OpenAPI 3.0.0-RC (implementers draft) as described in:
// https://www.openapis.org/blog/2017/03/01/openapi-spec-3-implementers-draft-released#
// https://github.com/OAI/OpenAPI-Specification/blob/3.0.0-rc0/versions/3.0.md
const clone = obj => (obj ? _.cloneDeep(obj) : {});
function mergeIn(container, items) {
if (!items) {
return;
}
for (const key in items) {
if (items.hasOwnProperty(key)) {
container[key] = items[key];
}
}
}
// Figure out the basePath for OpenAPI definition
function getBase(request, extra) {
const parts = request.originalUrl.split('/');
// Remove extra path parts.
parts.splice(-extra, extra);
const base = parts.join('/');
return base;
}
function generateValidationErrorSchema() {
const def = {
required: ['message', 'name', 'kind', 'path'],
properties: {
properties: {
$ref: '#/components/schemas/ValidationErrorProperties'
},
message: {
type: 'string'
},
name: {
type: 'string'
},
kind: {
type: 'string'
},
path: {
type: 'string'
}
}
};
return def;
}
function generateValidationErrorPropertiesSchema() {
const def = {
required: ['type', 'message', 'path'],
properties: {
type: {
type: 'string'
},
message: {
type: 'string'
},
path: {
type: 'string'
}
}
};
return def;
}
function buildTags(opts, controllers) {
const tags = [];
if (controllers) {
controllers.forEach(function(controller) {
tags.push({
name: controller.model().singular(),
description: `${utils.capitalize(controller.model().singular())} resource.`,
'x-resource': true // custom extension to state this tag represent a resource
});
});
}
return tags;
}
function buildPaths(opts, controllers) {
const paths = {};
if (controllers) {
controllers.forEach(function(controller) {
controller.generateOpenApi3();
const collection = controller.openApi3.paths;
for (const path in collection) {
if (collection.hasOwnProperty(path)) {
paths[path] = collection[path];
}
}
});
}
return paths;
}
function buildDefaultServers() {
return [
{
url: '/api'
}
];
}
function defaultIfMissing(obj, prop, defaultValue) {
if (!obj) {
return;
}
if (!obj.hasOwnProperty(prop) || obj[prop] === null) {
if (defaultValue) {
obj[prop] = defaultValue;
} else {
delete obj[prop];
}
}
}
function buildInfo(options) {
const info = options.info || {};
defaultIfMissing(info, 'title', 'api');
defaultIfMissing(info, 'description', 'Baucis generated OpenAPI v.3 documentation.');
defaultIfMissing(info, 'version', null);
// defaultIfMissing(info, 'termsOfService', null);
// defaultIfMissing(info, 'contact', {
// name: "name",
// url: "http://acme.com",
// email: "name@acme.com"
// });
// defaultIfMissing(info, 'license', {
// name: "Apache 2.0",
// url: "http://www.apache.org/licenses/LICENSE-2.0.html"
// });
return info;
}
function buildSchemas(controllers) {
const schemas = {};
controllers.forEach(function(controller) {
controller.generateOpenApi3();
const collection = controller.openApi3.components.schemas;
for (const def in collection) {
if (collection.hasOwnProperty(def)) {
schemas[def] = collection[def];
}
}
});
schemas.ValidationError = generateValidationErrorSchema();
schemas.ValidationErrorProperties = generateValidationErrorPropertiesSchema();
return schemas;
}
function buildParameters(controllers) {
controllers.forEach(function() {});
return params.generateCommonParams();
}
function buildResponses(controllers) {
controllers.forEach(function() {});
return {};
}
function buildSecuritySchemes(options, controllers) {
controllers.forEach(function() {});
return {};
}
function buildExamples(controllers) {
controllers.forEach(function() {});
return {};
}
function buildRequestBodies(controllers) {
controllers.forEach(function() {});
return {};
}
function buildHeaders(controllers) {
controllers.forEach(function() {});
return {};
}
function buildLinks(controllers) {
controllers.forEach(function() {});
return {};
}
function buildCallbacks(controllers) {
controllers.forEach(function() {});
return {};
}
function buildComponents(options, controllers) {
const components = clone(options.components);
defaultIfMissing(components, 'schemas', {});
defaultIfMissing(components, 'responses', {});
defaultIfMissing(components, 'parameters', {});
defaultIfMissing(components, 'examples', {});
defaultIfMissing(components, 'requestBodies', {});
defaultIfMissing(components, 'headers', {});
defaultIfMissing(components, 'securitySchemes', {});
defaultIfMissing(components, 'links', {});
defaultIfMissing(components, 'callbacks', {});
mergeIn(components.schemas, buildSchemas(controllers));
mergeIn(components.responses, buildResponses(controllers));
mergeIn(components.parameters, buildParameters(controllers));
mergeIn(components.examples, buildExamples(controllers));
mergeIn(components.requestBodies, buildRequestBodies(controllers));
mergeIn(components.headers, buildHeaders(controllers));
mergeIn(components.securitySchemes, buildSecuritySchemes(options, controllers));
mergeIn(components.links, buildLinks(controllers));
mergeIn(components.callbacks, buildCallbacks(controllers));
return components;
}
// A method for generating OpenAPI resource listing
function generateResourceListing(options) {
const controllers = options.controllers;
const opts = options.options || {};
const listing = {
openapi: '3.0.0',
info: buildInfo(opts),
servers: opts.servers || buildDefaultServers(),
tags: buildTags(opts, controllers),
paths: buildPaths(opts, controllers),
components: buildComponents(opts, controllers)
};
mergeIn(listing.paths, opts.paths);
if (opts.security) {
listing.security = opts.security;
}
if (opts.externalDocs) {
listing.externalDocs = opts.externalDocs;
}
return listing;
}
// build an specific spec based on options and filtered controllers
function generateResourceListingForVersion(options) {
const clonedDoc = clone(options.rootDocument);
if (!clonedDoc.info.version) {
// Set baucis version if not provided previously by options
clonedDoc.info.version = options.version;
}
clonedDoc.paths = clonedDoc.paths || {};
mergeIn(clonedDoc.paths, buildPaths(options.controllers));
clonedDoc.components.schemas = clonedDoc.components.schemas || {};
const compo2 = buildComponents(options, options.controllers);
mergeIn(clonedDoc.components.schemas, compo2.schemas);
return clonedDoc;
}
module.exports = (pluginOptions = {}) =>
function extendApi(api) {
let customOpts = {};
api.generateOpenApi3 = function(opts) {
if (opts) {
customOpts = opts;
}
// user can extend this openApi3Document
api.openApi3Document = generateResourceListing({
version: null,
controllers: api.controllers('0.0.1'),
basePath: null,
options: customOpts
});
return api;
};
// Middleware for the documentation index.
api.get('/openapi.json', function(request, response) {
try {
if (!api.openApi3Document) {
api.generateOpenApi3(customOpts);
}
// Customize a openApi3Document copy by requested version
const versionedApi = generateResourceListingForVersion({
rootDocument: api.openApi3Document,
version: request.baucis.release,
controllers: api.controllers(request.baucis.release),
basePath: getBase(request, 1),
options: customOpts
});
response.json(versionedApi);
} catch (e) {
console.error(JSON.stringify(e));
response.status(500).json(e);
}
});
};