/* * Copyright (C) 2018-2021 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 {assign, filter, map} from 'lodash'; import {OpenAPIV3} from 'openapi-types'; import {isLightweightClass} from './easy-ast/ast-util'; import {LightweightProjectWithIndex} from './easy-ast/types/lightweight-project'; import {RouteInstanceWithMeta, RouteWithMetaInformation} from './types/routes'; import {rejectNil} from './util/collections'; import {capitalize} from './util/string'; import path from 'path'; import {lightweightProjectFromPath} from './easy-ast/easy-ast'; /** * 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. */ export async function gatherRouteInformation(path: string): Promise { const project = new LightweightProjectWithIndex(lightweightProjectFromPath(path)); // find all classes that implement the SCAbstractRoute return rejectNil( await Promise.all( map(filter(project.definitions, isLightweightClass), async node => { if (!node.extendedDefinitions?.some(it => it.referenceName === 'SCAbstractRoute')) { return undefined; } const instantiatedRoute = (await project.instantiateDefinitionByName( node.name, )) as RouteInstanceWithMeta; // instantiate all errors instantiatedRoute.errors = await Promise.all( // eslint-disable-next-line @typescript-eslint/no-explicit-any instantiatedRoute.errorNames.map(async (error: any) => // eslint-disable-next-line @typescript-eslint/ban-types assign((await project.instantiateDefinitionByName(error.name)) as object, {name: error.name}), ), ); instantiatedRoute.responseBodyDescription = // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain project.definitions[instantiatedRoute.responseBodyName]?.comment?.shortSummary!; instantiatedRoute.requestBodyDescription = // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain project.definitions[instantiatedRoute.requestBodyName]?.comment?.shortSummary!; return { description: { shortText: node.comment?.shortSummary, text: node.comment?.description, }, name: node.name!, route: instantiatedRoute, }; }), ), ); } /** * Generate documentation snippet for one route * * @param routeWithInfo A route instance with its meta information * @param outDirectorySchemasPath 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, outDirectorySchemasPath: string, schemasToCopy: string[], tagsToKeep: string[], ): OpenAPIV3.PathItemObject { const route = routeWithInfo.route; const openapiPath: OpenAPIV3.PathItemObject = {}; schemasToCopy.push(route.requestBodyName, route.responseBodyName); openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods] = { summary: capitalize(routeWithInfo.description.shortText?.replace(/(Route to |Route for )/gim, '')), description: routeWithInfo.description.text, requestBody: { description: route.responseBodyDescription ?? undefined, content: { 'application/json': { schema: { $ref: path.join(outDirectorySchemasPath, `${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)), }; openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![route.statusCodeSuccess] = { description: route.responseBodyDescription, content: { 'application/json': { schema: { $ref: path.join(outDirectorySchemasPath, `${route.responseBodyName}.json`), }, }, }, }; for (const error of route.errors) { schemasToCopy.push(error.name); openapiPath[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: path.join(outDirectorySchemasPath, `${error.name}.json`), }, }, }, }; } if (typeof route.obligatoryParameters === 'object') { for (const [parameter, schemaDefinition] of Object.entries(route.obligatoryParameters)) { const openapiParameter: 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}`, }, }; openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]?.parameters?.push(openapiParameter); } } return openapiPath; }