UNPKG

@wesleytodd/openapi

Version:

Middleware for generating OpenAPI/Swagger documentation for your Express app

195 lines (166 loc) 6.95 kB
'use strict' const httpErrors = require('http-errors') const Router = require('router') const SwaggerParser = require('swagger-parser') const ui = require('./lib/ui') const makeValidator = require('./lib/validate') const { get: getSchema, set: setSchema } = require('./lib/layer-schema') const minimumViableDocument = require('./lib/minimum-doc') const generateDocument = require('./lib/generate-doc') const defaultRoutePrefix = '/openapi' const YAML = require('yaml') module.exports = function ExpressOpenApi (_routePrefix, _doc, _opts) { // Acceptable arguments: // oapi() // oapi('/path') // oapi('/path', doc) // oapi('/path', doc, opts) // oapi(doc) // oapi(doc, opts) // // The below logic is correct, but very hard to reason about let routePrefix = _routePrefix || defaultRoutePrefix let doc = _doc || minimumViableDocument let opts = _opts || {} if (typeof _routePrefix !== 'string') { routePrefix = defaultRoutePrefix doc = _routePrefix || minimumViableDocument opts = _doc || {} } // We need to route a bit, seems a safe addition // to use the express router in an express middleware const router = new Router() // Fully generate the doc on the first request let isFirstRequest = true // Where the magic happens const middleware = function OpenApiMiddleware (req, res, next) { if (isFirstRequest) { middleware.document = generateDocument(middleware.document, req.app._router || req.app.router, opts.basePath) isFirstRequest = false } router.handle(req, res, next) } // Expose the current document and prefix middleware.routePrefix = routePrefix middleware.document = generateDocument(doc, undefined, opts.basePath) middleware.generateDocument = generateDocument middleware.options = opts // Add a path schema to the document middleware.path = function (schema = {}) { function schemaMiddleware (req, res, next) { next() } setSchema(schemaMiddleware, schema) return schemaMiddleware } // Validate path middleware middleware.validPath = function (schema = {}, pathOpts = {}) { let validate function validSchemaMiddleware (req, res, next) { if (!validate) { validate = makeValidator(middleware, getSchema(validSchemaMiddleware), { ...pathOpts, ...opts }) } return validate(req, res, next) } setSchema(validSchemaMiddleware, schema) return validSchemaMiddleware } // Component definitions middleware.component = function (type, name, description) { if (!type) { throw new TypeError('Component type is required') } // Return whole component type if (!name && !description) { return middleware.document.components && middleware.document.components[type] } // Return ref to type if (name && !description) { if (!middleware.document.components || !middleware.document.components[type] || !middleware.document.components[type][name]) { throw new Error(`Unknown ${type} ref: ${name}`) } return { $ref: `#/components/${type}/${name}` } } // @TODO create id // Is this necessary? The point of this was to provide canonical component ref urls // But now I think that might not be necessary. // if (!description || !description['$id']) { // const server = middleware.document.servers && middleware.document.servers[0] && middleware.document.servers[0].url // console.log(`${server || '/'}{routePrefix}/components/${type}/${name}.json`) // description['$id'] = `${middleware.document.servers[0].url}/${routePrefix}/components/${type}/${name}.json` // } // Set name on parameter if not passed if (type === 'parameters') { description.name = description.name || name } // Define a new component middleware.document.components = middleware.document.components || {} middleware.document.components[type] = middleware.document.components[type] || {} middleware.document.components[type][name] = description return middleware } middleware.schema = middleware.component.bind(null, 'schemas') middleware.response = middleware.component.bind(null, 'responses') middleware.parameters = middleware.component.bind(null, 'parameters') middleware.examples = middleware.component.bind(null, 'examples') middleware.requestBodies = middleware.component.bind(null, 'requestBodies') middleware.headers = middleware.component.bind(null, 'headers') middleware.securitySchemes = middleware.component.bind(null, 'securitySchemes') middleware.links = middleware.component.bind(null, 'links') middleware.callbacks = middleware.component.bind(null, 'callbacks') // Expose ui middleware middleware.swaggerui = (options) => ui.serveSwaggerUI(`${routePrefix}.json`, options) // OpenAPI document as json router.get(`${routePrefix}.json`, (req, res) => { middleware.document = generateDocument(middleware.document, req.app._router || req.app.router, opts.basePath) res.json(middleware.document) }) // OpenAPI document as yaml router.get([`${routePrefix}.yaml`, `${routePrefix}.yml`], (req, res) => { const jsonSpec = generateDocument(middleware.document, req.app._router || req.app.router, opts.basePath) const yamlSpec = YAML.stringify(jsonSpec) res.type('yaml') res.send(yamlSpec) }) router.get(`${routePrefix}/components/:type/:name.json`, (req, res, next) => { const { type, name } = req.params middleware.document = generateDocument(middleware.document, req.app._router || req.app.router, opts.basePath) // No component by that identifer if (!middleware.document.components[type] || !middleware.document.components[type][name]) { return next(httpErrors(404, `Component does not exist: ${type}/${name}`)) } // Return component res.json(middleware.document.components[type][name]) }) // Validate full open api document router.get(`${routePrefix}/validate`, (req, res) => { middleware.document = generateDocument(middleware.document, req.app._router || req.app.router, opts.basePath) SwaggerParser.validate(middleware.document, (err, api) => { if (err) { return res.json({ valid: false, details: err.details, document: middleware.document }) } res.json({ valid: true, document: middleware.document }) }) }) // Serve up the for exploring the document if (opts.htmlui) { let ui = opts.htmlui if (!Array.isArray(opts.htmlui)) { ui = [opts.htmlui] } if (ui.includes('swagger-ui')) { router.get(`${routePrefix}`, (req, res) => { res.redirect(`${routePrefix}/swagger-ui`) }) router.use(`${routePrefix}/swagger-ui`, middleware.swaggerui) } } return middleware } module.exports.minimumViableDocument = minimumViableDocument module.exports.defaultRoutePrefix = defaultRoutePrefix