hapi-swagger
Version:
A swagger documentation UI generator plugin for hapi
338 lines (295 loc) • 8.69 kB
JavaScript
const Hoek = require('@hapi/hoek');
const Joi = require('joi');
const JSONDeRef = require('@apidevtools/json-schema-ref-parser');
const Filter = require('../lib/filter');
const Group = require('../lib/group');
const Sort = require('../lib/sort');
const Info = require('../lib/info');
const Paths = require('../lib/paths');
const Tags = require('../lib/tags');
const Validate = require('../lib/validate');
const Utilities = require('../lib/utilities');
const builder = (module.exports = {});
const internals = {};
/**
* @typedef {import('@hapi/hapi').Request}
*/
/**
* default data for swagger root object
*/
builder.default = {
basePath: '/',
routeTag: 'api'
};
internals.openapiBaseSchema = Joi.object({
info: Joi.any(),
schemes: Joi.array()
.items(Joi.string().valid('http', 'https', 'ws', 'wss'))
.optional(),
consumes: Joi.array().items(Joi.string()),
produces: Joi.array().items(Joi.string()),
paths: Joi.any(),
security: Joi.any(),
grouping: Joi.string().valid('path', 'tags'),
tagsGroupingFilter: Joi.func(),
tags: Joi.any(),
cors: Joi.boolean(),
externalDocs: Joi.object({
description: Joi.string(),
url: Joi.string().uri()
}),
cache: Joi.object({
expiresIn: Joi.number(),
expiresAt: Joi.string(),
generateTimeout: Joi.number()
})
})
.or('swagger', 'openapi')
.pattern(/^x-/, Joi.any());
/**
* schema for swagger root object
*/
builder.schema = {
swagger: internals.openapiBaseSchema.keys({
swagger: Joi.string().valid('2.0').required(),
host: Joi.string(), // JOI hostname validator too strict
basePath: Joi.string().regex(/^\//),
definitions: Joi.any(),
parameters: Joi.any(),
responses: Joi.any(),
securityDefinitions: Joi.any()
}),
openapi3: internals.openapiBaseSchema.keys({
openapi: Joi.string().valid('3.0.0').required(),
servers: Joi.array().items(
Joi.object({
url: Joi.string().uri(),
description: Joi.string()
})
),
components: Joi.object({
schemas: Joi.any(),
parameters: Joi.any(),
responses: Joi.any(),
securitySchemes: Joi.any()
})
})
};
/**
* gets the Swagger JSON
*
* @param {Object} settings
* @param {Request} request
*/
builder.getSwaggerJSON = async (settings, request) => {
// remove items that cannot be changed by user
delete settings.swagger;
delete settings.templates;
// collect root information
settings = Hoek.applyToDefaults(builder.default, settings);
if (settings.basePath !== '/') {
settings.basePath = Utilities.removeTrailingSlash(settings.basePath);
}
if (settings.OAS === 'v2') {
settings.swagger = '2.0';
settings.host = settings.host || internals.getHost(request);
settings.schemes = settings.schemes || [internals.getSchema(request)];
} else {
settings.openapi = '3.0.0';
settings.servers = settings.servers || [{ url: internals.getServerUrl(request, settings) }];
settings.components = {};
if (settings.securityDefinitions) {
settings.components.securitySchemes = settings.securityDefinitions;
}
}
let out = internals.removeNoneSchemaOptions(settings, settings);
Joi.assert(out, settings.OAS === 'v2' ? builder.schema.swagger : builder.schema.openapi3);
if (settings.customSwaggerFile) {
Object.assign(settings.customSwaggerFile, out);
return settings.customSwaggerFile;
}
out.info = Info.build(settings);
out.tags = Tags.build(settings);
let routes = request.server.table();
// filter routes displayed based on tags passed in query string
if (request.query.tags) {
const queryTags = request.query.tags.split(',');
routes = Filter.byTags(queryTags, routes);
}
if (typeof settings.routeTag === 'function') {
routes = Filter.byFunction(settings.routeTag, routes);
} else {
routes = Filter.byTags([settings.routeTag], routes);
}
Sort.paths(settings.sortPaths, routes);
// append group property - by path
Group.appendGroupByPath(settings.pathPrefixSize, settings.basePath, routes, settings.pathReplacements);
const paths = new Paths(settings);
const pathData = paths.build(routes);
out.paths = pathData.paths;
if (settings.OAS === 'v2') {
out.definitions = pathData.definitions;
} else {
out.components.schemas = pathData.definitions;
}
if (Utilities.hasProperties(pathData['x-alt-definitions'])) {
out['x-alt-definitions'] = pathData['x-alt-definitions'];
}
out = internals.removeNoneSchemaOptions(out, settings);
if (settings.OAS === 'v3.0') {
delete out.produces;
delete out.consumes;
}
if (settings.debug) {
await Validate.log(out, settings.log);
}
if (settings.deReference === true) {
return builder.dereference(out);
}
return out;
};
/**
* dereference a schema
*
* @param {Object} schema
*/
builder.dereference = async (schema) => {
try {
const json = await JSONDeRef.dereference(schema);
if (schema.openapi === '3.0.0') {
for (const key of Object.keys(json.components)) {
if (key !== 'securitySchemes') {
delete json.components[key];
}
}
} else {
delete json.definitions;
}
delete json['x-alt-definitions'];
return json;
} catch (err) {
throw new Error('failed to dereference schema');
}
};
/**
* return originating value for an `x-forwarded` header
* @param {Object} request
* @param {string} name header name (without x-forwarded prefix)
* @return {string | undefined}
*/
internals.getProxyHeader = function (request, name) {
const header = request.headers['x-forwarded-' + name];
return header ? header.split(',')[0] : undefined;
};
/**
* finds the current host
*
* @param {Request} request
* @return {string}
*/
internals.getHost = function (request) {
const proxyHost = internals.getProxyHeader(request, 'host') || request.headers['disguised-host'] || '';
if (proxyHost) {
return proxyHost;
}
try {
const url = new URL(request.info.referrer);
return url.host;
} catch (error) {
// backup in case referrer isn't set for some reason. (i.e. tests)
return request.info.host;
}
};
internals.getServerUrl = function (request, settings) {
const forwardedProtocol = internals.getProxyHeader(request, 'proto');
const proxyHost = internals.getProxyHeader(request, 'host') || request.headers['disguised-host'] || '';
if (proxyHost) {
return `${forwardedProtocol ?? internals.getSchema(request)}://${proxyHost}${settings.basePath}`;
}
try {
const url = new URL(request.info.referrer);
url.search = '';
url.hash = '';
url.pathname = settings.basePath;
return url.toString();
} catch (error) {
// backup in case referrer isn't set for some reason. (i.e. tests)
return `${request.url.protocol}//${request.info.host}${settings.basePath === '/' ? '' : settings.basePath}`;
}
};
/**
* finds the current schema
*
* @param {Request} request
* @return {string}
*/
internals.getSchema = function (request) {
const forwardedProtocol = internals.getProxyHeader(request, 'proto');
if (forwardedProtocol) {
return forwardedProtocol;
}
// Azure Web Sites adds this header when requests was received via HTTPS.
if (request.headers['x-arr-ssl']) {
return 'https';
}
const protocol = request.server.info.protocol;
// When iisnode is used, connection protocol is `socket`. While IIS
// receives request over HTTP and passes it to node via a named pipe.
if (protocol === 'socket') {
return 'http';
}
return protocol;
};
/**
* removes none schema properties from options
*
* @param {Object} options
* @return {Object}
*/
internals.removeNoneSchemaOptions = function (options, settings) {
const out = Hoek.clone(options);
[
'debug',
'documentationPath',
'documentationRoutePlugins',
'documentationRouteTags',
'documentationPage',
'jsonPath',
'jsonRoutePath',
'auth',
'swaggerUIPath',
'routesBasePath',
'swaggerUI',
'pathPrefixSize',
'payloadType',
'expanded',
'sortTags',
'sortEndpoints',
'sortPaths',
'authAccessFormatter',
'grouping',
'tagsGroupingFilter',
'xProperties',
'reuseDefinitions',
'uiCompleteScript',
'uiOptions',
'deReference',
'definitionPrefix',
'validatorUrl',
'acceptToProduce',
'cache',
'pathReplacements',
'log',
'cors',
'routeTag',
'validate',
'tryItOutEnabled',
'customSwaggerFile',
'wildcardMethods',
'OAS',
...(settings.OAS === 'v2' ? [] : ['basePath', 'securityDefinitions'])
].forEach((element) => {
delete out[element];
});
return out;
};