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', }, }); 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[] = []; 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: { includeRegexpArray: RegExp[]; excludeRegexpArray: RegExp[]; }, componentArg: plugins.openapiTypes.OpenAPIV3.ComponentsObject ) { for (const pathCandidateRoute of Object.keys(this.apiDoc.paths)) { let included: boolean = false; let excluded: boolean = false; for (const regExp of routeDescriptor.includeRegexpArray) { if (regExp.test(pathCandidateRoute)) { included = true; break; } } if (included) { for (const regExp of routeDescriptor.excludeRegexpArray) { if (regExp.test(pathCandidateRoute)) { 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) { } }; } } } 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(` ${this.apiDoc.info?.title || 'no name set'} - SwaggerUI
`); res.end(); }; } public getSlashRedocMiddleware() { return (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => { res.setHeader('content-type', 'text/html'); res.write(` ${this.apiDoc.info?.title || 'no name set'} - Redoc `); 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(); }; } }