restana-swagger-validator
Version:
High performance Swagger/OpenAPI validator middleware for Restana
218 lines (185 loc) • 6.36 kB
JavaScript
const apiSchemaBuilder = require('api-schema-builder')
class SwaggerValidationError extends Error {
constructor (message, statusCode, errors) {
super(message)
this.errors = errors
this.statusCode = statusCode
}
}
class RequestValidationError extends SwaggerValidationError {
constructor (errors) {
super('Swagger request validation failed!', 400, errors)
}
}
class ResponseValidationError extends SwaggerValidationError {
constructor (errors) {
super('Swagger response validation failed!', 500, errors)
}
}
class SwaggerSchemaNotFound extends SwaggerValidationError {
constructor (path, method, type = 'request') {
super(`Swagger schema not found for ${type}!`, 500)
this.path = path
this.method = method
this.type = type
}
}
function validateParameters ({ query, headers, params, files }, schema) {
schema.parameters.validate({
query,
headers,
path: params,
files
})
return schema.parameters.errors || []
}
function getContentType ({ 'content-type': contentType }) {
return contentType && contentType.split(';')[0].trim()
}
function validateBody ({ headers, body }, schema) {
const contentType = getContentType(headers)
if (schema.body) {
const validator = schema.body[contentType] || schema.body
if (!validator.validate(body)) {
return validator.errors || []
}
}
return []
}
function validate (req, schema) {
return [].concat(
validateParameters(req, schema),
validateBody(req, schema)
)
}
function SwaggerValidator (app, spec, options = {}) {
app.swaggerValidatorOptions = options = Object.assign({
buildResponses: true,
requireSchemaSpec: true,
apiSpecEndpoint: '/swagger.json',
uiEndpoint: '/docs',
publicApiEndpoint: 'http://localhost:3000'
}, options)
let schema
try {
spec = typeof spec === 'string' ? require(spec) : spec
schema = apiSchemaBuilder.buildSchemaSync(spec, options)
} catch (err) {
throw new Error(`Invalid OpenAPI specification was provided "${err.message}"`)
}
const uiHtml = getSwaggerUi(options.publicApiEndpoint + options.apiSpecEndpoint)
app.get(options.uiEndpoint, (_, res) => {
res.setHeader('content-type', 'text/html')
res.send(uiHtml)
})
app.get(options.apiSpecEndpoint, (_, res) => {
res.send(spec)
})
const eventName = app.events.BEFORE_ROUTE_REGISTER
app.events.on(eventName, (method, args) => {
const path = args[0]
if (!(schema[path] && schema[path][method])) {
if (options.requireSchemaSpec) {
throw new SwaggerSchemaNotFound(path, method)
}
} else {
const endpointSchema = schema[path][method]
args.splice(1, 0, (req, res, next) => {
const errors = validate(req, endpointSchema)
if (errors.length) {
return next(new RequestValidationError(errors))
} else {
if (options.buildResponses) {
const _end = res.end
res.end = (data, cb) => {
res.end = _end
const endpointSchema = schema[path][method].responses[res.statusCode]
if (options.requireSchemaSpec && !endpointSchema && data) {
throw new SwaggerSchemaNotFound(path, method, 'response')
}
if (endpointSchema) {
const responseType = res.getHeader('content-type') || ''
const isValid = endpointSchema.validate({
body: responseType.startsWith('application/json') ? JSON.parse(data) : data,
headers: res.getHeaders()
})
if (!isValid) {
throw new ResponseValidationError(endpointSchema.errors)
}
}
res.end(data, cb)
}
}
return next()
}
})
}
})
}
function getSwaggerUi (swaggerSpec) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.31.1/swagger-ui.css" integrity="sha512-1gs56cGdIn+SDweW3bEtMcVVxX+oWJdpKHztczu8WpE3GmZquxIpjg4IoOPRJ8hmTPLiZriIW38uE+059dcYcA==" crossorigin="anonymous" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.31.1/swagger-ui-bundle.min.js" integrity="sha512-oGb0sN1rO+CBXLRsaiWSWRrTj+Ja9Tq/iDqhetDHrtUlkuDEP4vQk1T5bIM1gRtTnnWJNf2CMc++oDZNiTIR8g==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.31.1/swagger-ui-standalone-preset.min.js" integrity="sha512-eaXFoJZC91+fHqbvojyUgDvwiwn2uZQ8mNe3RxZnJKl+bEWrxa/HLzammegshoUR2BLVlnuUXMMVRP6axf9suQ==" crossorigin="anonymous"></script>
<script>
const catalog = [{
url: '${swaggerSpec}',
name: 'API Documentation'
}]
const ui = SwaggerUIBundle({
validatorUrl: null,
urls: catalog,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
requestInterceptor: (req) => {
return req
},
displayRequestDuration: true,
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "StandaloneLayout"
})
window.ui = ui
</script>
</body>
</html>
`
}
module.exports = {
SwaggerSchemaNotFound,
SwaggerValidationError,
SwaggerValidator,
RequestValidationError,
ResponseValidationError,
getSwaggerUi
}