Files
openstapps/src/routes.ts
2021-08-25 09:47:36 +00:00

162 lines
6.0 KiB
TypeScript

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<RouteWithMetaInformation[]> {
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;
}