smartswagger/ts/smartswagger.classes.smartswagger.ts

291 lines
9.5 KiB
TypeScript

import path from 'path';
import * as plugins from './smartswagger.plugins';
interface RedocProps {
'x-tagGroups'?: {name: string, tags: string[]}[];
}
type IExtendedApiDoc = plugins.openapiTypes.OpenAPIV3.Document & RedocProps;
export class Smartswagger {
// STATIC
/**
* creates a new Smartswagger instance from an external document
* @param urlArg a url arg that contains an original swagger.json in the response
* @returns an instance of
*/
public static async createFromUrl(urlArg: string) {
const jsonResponse = await plugins.nodeFetch(urlArg, {
headers: {
accept: 'application/json',
},
});
const apiDoc = await jsonResponse.json();
const newSMartswaggerInstance = new Smartswagger(apiDoc);
return newSMartswaggerInstance;
}
public static async createNew(titleArg: string = 'an OpenApiDoc') {
const newSMartswaggerInstance = new Smartswagger({
openapi: '3.0.0',
paths: {},
info: {
title: titleArg,
version: '1.0.0'
}
});
return newSMartswaggerInstance;
}
// INSTANCE
/**
* the basic info of the api doc
*/
public baseInfo: plugins.openapiTypes.OpenAPIV3.InfoObject;
public apiDoc: IExtendedApiDoc;
constructor(apiDocArg: IExtendedApiDoc) {
this.apiDoc = apiDocArg;
}
/**
* dereferences the document at hand
*/
public async deref() {
this.apiDoc = (await plugins.swaggerParser.dereference(this.apiDoc)) as IExtendedApiDoc;
}
public async addServer(serverArg: plugins.openapiTypes.OpenAPIV3.ServerObject) {
await this.deref();
this.apiDoc.servers = this.apiDoc.servers || [];
this.apiDoc.servers.push(serverArg);
}
/**
* merge a document from url
* @param documentToMergeArg
* @param basePathArg
*/
public async mergeDocument(documentToMergeArg: IExtendedApiDoc, basePathArg: string = '', tagArg?: string) {
console.log(`merging document with name ${documentToMergeArg.info?.title}`);
await this.deref();
// lets get a dereferenced version of the document we want to merge
const documentToMerge = (await plugins.swaggerParser.dereference(
documentToMergeArg
)) as IExtendedApiDoc;
for (const path of Object.keys(documentToMerge.paths)) {
const pathToMerge = plugins.path.join(basePathArg, path);
this.apiDoc.paths = this.apiDoc.paths || {};
this.apiDoc.paths[pathToMerge] = documentToMerge.paths[path];
if (tagArg) {
if (this.apiDoc.paths[pathToMerge].post) {
this.apiDoc.paths[pathToMerge].post.tags = this.apiDoc.paths[pathToMerge].post.tags || [];
this.apiDoc.paths[pathToMerge].post.tags.push(tagArg);
}
if (this.apiDoc.paths[pathToMerge].get) {
this.apiDoc.paths[pathToMerge].get.tags = this.apiDoc.paths[pathToMerge].get.tags || [];
this.apiDoc.paths[pathToMerge].get.tags.push(tagArg);
}
if (this.apiDoc.paths[pathToMerge].put) {
this.apiDoc.paths[pathToMerge].put.tags = this.apiDoc.paths[pathToMerge].put.tags || [];
this.apiDoc.paths[pathToMerge].put.tags.push(tagArg);
}
if (this.apiDoc.paths[pathToMerge].delete) {
this.apiDoc.paths[pathToMerge].delete.tags = this.apiDoc.paths[pathToMerge].delete.tags || [];
this.apiDoc.paths[pathToMerge].delete.tags.push(tagArg);
}
}
}
// merge tag groups
this.apiDoc['x-tagGroups'] = this.apiDoc['x-tagGroups'] || [];
if (documentToMerge['x-tagGroups']) {
for (const xTagGroup of documentToMerge['x-tagGroups']) {
this.apiDoc['x-tagGroups'].push(xTagGroup);
}
}
console.log('merged!');
// set custom tag arg.
if (tagArg) {
this.apiDoc['x-tagGroups'].push({name: tagArg, tags: [tagArg]});
}
}
/**
* merges a document from url
*/
public async mergeDocumentFromUrl(documentUrlArg: string, basePathArg: string = '', tagArg?: string) {
console.log(`getting document at ${documentUrlArg} for merging...`);
const documentResponse = await plugins.nodeFetch(documentUrlArg, {
headers: {
'accept-encoding': 'application/json',
},
}).catch(err => {
console.log(err);
});
if (!documentResponse || documentResponse.status > 299) {
return;
}
const documentString = await documentResponse.text();
const apiDoc: IExtendedApiDoc = JSON.parse(documentString);
console.log(`document successfully fetched!`);
await this.mergeDocument(apiDoc, basePathArg, tagArg);
}
/**
* merge multiple documents in parallel
* @param urlArrayArg
*/
public async mergeManyDocumentsFromUrl(urlArrayArg: { url: string; basePath?: string, tagArg?: string }[]) {
const promiseArray: Promise<void>[] = [];
for (const urlArg of urlArrayArg) {
promiseArray.push(this.mergeDocumentFromUrl(urlArg.url, urlArg.basePath, urlArg.tagArg));
}
await Promise.all(promiseArray);
}
/**
* merges a component to routes based on regex
*/
public mergeComponentToRoutes(
routeDescriptor: {
includeGlobArray: string[];
excludeGlobArray: string[];
},
componentArg: plugins.openapiTypes.OpenAPIV3.ComponentsObject
) {
for (const pathCandidateRoute of Object.keys(this.apiDoc.paths)) {
let included: boolean = false;
let excluded: boolean = false;
// We are using glob espressions here due to easier path expressions
for (const globExpression of routeDescriptor.includeGlobArray) {
if (plugins.matcher.isMatch(pathCandidateRoute, globExpression)) {
included = true;
break;
}
}
if (included) {
for (const globExpression of routeDescriptor.excludeGlobArray) {
if (plugins.matcher.isMatch(pathCandidateRoute, globExpression)) {
excluded = true;
break;
}
}
}
if (included && !excluded) {
// lets do the actual component inclusion
const pathCandidate = this.apiDoc.paths[pathCandidateRoute];
const instrumentMethod = (methodArg: typeof pathCandidate.get) => {
if (!methodArg) {
return;
}
if (componentArg.securitySchemes) {
methodArg.security = methodArg.security || [];
for (const securityScheme of Object.keys(componentArg.securitySchemes)) {
methodArg.security.push({ [securityScheme]: [] })
}
}
};
instrumentMethod(pathCandidate.get);
instrumentMethod(pathCandidate.post);
instrumentMethod(pathCandidate.put);
instrumentMethod(pathCandidate.delete);
}
}
}
private cacheResult: string = '';
private cacheCreationUnixTimestamp: number;
/**
* returns an express middleware using 'swagger-ui-express'
*/
public getSlashApiUiMiddleware() {
return (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => {
res.setHeader('content-type', 'text/html');
res.write(`
<html>
<head>
<title>${this.apiDoc.info?.title || 'no name set'} - SwaggerUI</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.12.1/swagger-ui.css">
<style>
body {
margin: 0px;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
</body>
<script src="https://unpkg.com/swagger-ui-dist@3.12.1/swagger-ui-standalone-preset.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@3.12.1/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "/apischema",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
})
window.ui = ui
}
</script>
</html>
`);
res.end();
};
}
public getSlashRedocMiddleware() {
return (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => {
res.setHeader('content-type', 'text/html');
res.write(`
<html>
<head>
<title>${this.apiDoc.info?.title || 'no name set'} - Redoc </title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='/apischema'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script>
</body>
</html>
`);
res.end();
};
}
public getSlashApiSchemaMiddleware() {
return async (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => {
res.header('content-type', 'application/json');
res.write(JSON.stringify(this.apiDoc));
res.end();
};
}
}