/* * Copyright (C) 2018-2019 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool'; import {Logger} from '@openstapps/logger'; import {OpenAPIV3} from 'openapi-types'; import {basename, dirname, join} from 'path'; import {ProjectReflection} from 'typedoc'; import {Type} from 'typedoc/dist/lib/models'; import {capitalize, NodeWithMetaInformation, RouteWithMetaInformation} from './common'; /** * Gather relevant information of routes * * This gathers the information for all routes that implement the abstract class SCAbstractRoute. * Furthermore it instantiates every route and adds it to the information. * * @param reflection Contents of the JSON representation which Typedoc generates */ export async function gatherRouteInformation(reflection: ProjectReflection): Promise { const routes: RouteWithMetaInformation[] = []; if (!Array.isArray(reflection.children)) { throw new Error('Project reflection doesn\'t contain any modules.'); } // tslint:disable-next-line:no-magic-numbers await asyncPool(2, reflection.children, async (module) => { if (Array.isArray(module.children) && module.children.length > 0) { // tslint:disable-next-line:no-magic-numbers await asyncPool(2, module.children, (async (node) => { if (Array.isArray(node.extendedTypes) && node.extendedTypes.length > 0) { if (node.extendedTypes.some((extendedType) => { // tslint:disable-next-line:completed-docs return (extendedType as (Type & { name: string; })).name === 'SCAbstractRoute'; })) { Logger.info(`Found ${node.name} in ${module.originalName}.`); if (Array.isArray(module.originalName.match(/\.d\.ts$/))) { module.originalName = join(dirname(module.originalName), basename(module.originalName, '.d.ts')); Logger.info(`Using compiled version of module in ${module.originalName}.`); } const importedModule = await import(module.originalName); const route = new importedModule[node.name](); // tslint:disable-next-line: no-any const errors = route.errorNames.map((error: any) => { const scError = new importedModule[error.name](); scError.name = error.name; return scError; }); route.responseBodyDescription = module.children!.find(element => element.name === route.responseBodyName)?.comment?.shortText; route.requestBodyDescription = module.children!.find(element => element.name === route.requestBodyName)?.comment?.shortText; route.errors = errors; routes.push({description: node.comment!, name: node.name, route}); } } })); } }); if (routes.length === 0) { throw new Error('No route information found.'); } return routes; } /** * Get link for a node * * @param name Name of the node * @param node Node itself */ export function getLinkForNode(name: string, node: NodeWithMetaInformation): string { let link = 'https://openstapps.gitlab.io/core/'; const module = node.module .toLowerCase() .split('/') .join('_'); if (node.type === 'Type alias') { link += 'modules/'; link += `_${module}_`; link += `.html#${name.toLowerCase()}`; return link; } let type = 'classes'; if (node.type !== 'Class') { type = `${node.type.toLowerCase()}s`; } link += `${type}/`; link += `_${module}_`; link += `.${name.toLowerCase()}.html`; return link; } /** * Generate documentation snippet for one route * * @param routeWithInfo A route instance with its meta information * @param outDirSchemasPath Path to directory that will contain relevant schemas for the route * @param schemasToCopy Schemas identified as relevant for this route * @param tagsToKeep Tags / keywords that can be used for grouping routes */ export function generateOpenAPIForRoute(routeWithInfo: RouteWithMetaInformation, outDirSchemasPath: string, schemasToCopy: string[], tagsToKeep: string[]): OpenAPIV3.PathItemObject { const route = routeWithInfo.route; const path: OpenAPIV3.PathItemObject = {}; schemasToCopy.push(route.requestBodyName, route.responseBodyName); path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)] = { summary: capitalize(routeWithInfo.description.shortText?.replace(/(Route to |Route for )/gmi, '')), description: routeWithInfo.description.text, requestBody: { description: route.responseBodyDescription ?? undefined, content: { 'application/json': { schema: { $ref: join(outDirSchemasPath, `${route.requestBodyName}.json`), }, }, }, }, parameters: [{ name: 'X-StApps-Version', in: 'header', schema: { type: 'string', example: '2.0.0', }, required: true, }], responses: {}, tags: routeWithInfo.tags?.filter(value => tagsToKeep.includes(value)), }; path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)]!.responses![route.statusCodeSuccess] = { description: route.responseBodyDescription, content: { 'application/json': { schema: { $ref: join(outDirSchemasPath, `${route.responseBodyName}.json`), }, }, }, }; route.errors.forEach(error => { schemasToCopy.push(error.name); path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)]!.responses![error.statusCode] = { description: error.message ?? capitalize(error.name.replace(/([A-Z][a-z])/g,' $1') .replace('SC ', '')), content: { 'application/json': { schema: { $ref: join(outDirSchemasPath, `${error.name}.json`), }, }, }, }; }); if (typeof route.obligatoryParameters === 'object') { for (const [parameter, schemaDefinition] of Object.entries(route.obligatoryParameters)) { const openapiParam: OpenAPIV3.ParameterObject = { in: 'path', name: parameter, required: true, schema: { // TODO make this less of a hack and search copied schemas for the first occurring definition $ref: `schemas/SCSearchResponse.json#/definitions/${schemaDefinition}`, }, }; path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)]?.parameters?.push(openapiParam); } } return path; }