From a6f578efca3115df594e61ecc660d3348ce52439 Mon Sep 17 00:00:00 2001 From: Vilppu Saarinen Date: Tue, 23 May 2023 10:28:29 +0300 Subject: [PATCH] Support documenting route params Implemented routeParam descriptions and validation --- README.md | 1 + src/generate.ts | 51 ++++++++++++++++++++--- tests/__snapshots__/generate.spec.ts.snap | 41 ++++++++++++++++++ tests/other-routes.ts | 11 ++++- 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1f7e20f0..d2e8cbe7 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ const bodyCodec = t.type({ * @response 200 Success response description. * @response 400 Another description for a response. This one * spans multile lines. + * @routeParam myRouteParam pathparameter description */ const myRoute: Route | Response.BadRequest> = route.post(...).use(Parser.body(bodyCodec)).handler(...) diff --git a/src/generate.ts b/src/generate.ts index 5c7aee63..1a5a787d 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -180,6 +180,19 @@ const getRouterCallArgSymbols = ( return argSymbols } +const getRouteParameters = ( + symbol: ts.Symbol +): [string, string | undefined][] => + symbol + .getJsDocTags() + .filter((tag) => tag.name === 'routeParam') + .flatMap((tag) => tag.text) + .filter(isDefined) + .map((symbolDisplayPart) => symbolDisplayPart.text) + .map((tag) => tag.trim()) + .map((tagString) => tagString.split(' ')) + .map((tag) => [tag[0], tag.slice(1).join(' ') || undefined]) + const getRouteDeclaration = ( ctx: Context, components: Components, @@ -189,6 +202,10 @@ const getRouteDeclaration = ( const summary = getRouteSummary(symbol) const tags = getSymbolTags(symbol) const routeInput = getRouteInput(ctx, symbol) + const routeParameterDescriptions = new Map( + getRouteParameters(symbol) + ) + const operationId = getRouteOperationId(symbol) ?? symbol.escapedName.toString() @@ -222,7 +239,12 @@ const getRouteDeclaration = ( : undefined const parameters = [ - ...typeToRequestParameters(ctx, 'path', routeParams), + ...typeToRequestParameters( + ctx, + 'path', + routeParams, + routeParameterDescriptions + ), ...typeToRequestParameters(ctx, 'query', query), ...typeToRequestParameters(ctx, 'header', headers), ...typeToRequestParameters(ctx, 'cookie', cookies), @@ -745,19 +767,36 @@ const getContentTypeHeader = ( const typeToRequestParameters = ( ctx: Context, in_: 'path' | 'query' | 'header' | 'cookie', - type: ts.Type | undefined + type: ts.Type | undefined, + routeParameters?: Map ): OpenAPIV3.ParameterObject[] => { if (!type) return [] const props = ctx.checker.getPropertiesOfType(type) + const missingRouteParam = routeParameters + ? [...routeParameters.keys()].find( + (param) => !props.map((prop) => prop.name).includes(param) + ) + : undefined + if (missingRouteParam) { + throw new Error(`RouteParameter ${missingRouteParam} does not exist.`) + } return props.map((prop): OpenAPIV3.ParameterObject => { const description = getDescriptionFromComment(ctx, prop) return { name: prop.name, in: in_, required: in_ === 'path' ? true : !isOptional(prop), - schema: { type: 'string' }, - ...(description ? { description } : undefined), + schema: { + type: 'string', + }, + ...(routeParameters + ? routeParameters.get(prop.name) + ? { description: routeParameters.get(prop.name) } + : undefined + : description + ? { description } + : undefined), } }) } @@ -788,7 +827,9 @@ const getBaseSchema = ( symbol: ts.Symbol | undefined ): BaseSchema => { const description = symbol ? getDescriptionFromComment(ctx, symbol) : '' - return { ...(description ? { description } : undefined) } + return { + ...(description ? { description } : undefined), + } } const typeToSchema = ( diff --git a/tests/__snapshots__/generate.spec.ts.snap b/tests/__snapshots__/generate.spec.ts.snap index def47610..c821200e 100644 --- a/tests/__snapshots__/generate.spec.ts.snap +++ b/tests/__snapshots__/generate.spec.ts.snap @@ -551,6 +551,47 @@ Object { ], }, }, + "/other-stuff/route-with-route-parameter-tag/{myRouteParameter}": Object { + "get": Object { + "operationId": "routeWithRouteParameterTag", + "parameters": Array [ + Object { + "description": "12345", + "in": "path", + "name": "myRouteParameter", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "type": "string", + }, + }, + }, + "description": "OK", + }, + "400": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "type": "string", + }, + }, + }, + "description": "Bad Request", + }, + }, + "tags": Array [ + "TagFromRouter", + ], + }, + }, "/other-stuff/route-with-tag": Object { "get": Object { "operationId": "routeWithTag", diff --git a/tests/other-routes.ts b/tests/other-routes.ts index d0463e69..d71f775c 100644 --- a/tests/other-routes.ts +++ b/tests/other-routes.ts @@ -1,5 +1,14 @@ import { Response, route, Route, router } from 'typera-express' +/** + * @routeParam myRouteParameter 12345 + */ +const routeWithRouteParameterTag: Route< + Response.Ok | Response.BadRequest +> = route + .get('/route-with-route-parameter-tag/:myRouteParameter') + .handler(() => Response.ok('hello')) + const otherRoute: Route> = route .get('/other-route') .handler(() => Response.ok('hello')) @@ -16,4 +25,4 @@ const routeWithTag: Route> = route * @prefix /other-stuff * @tags TagFromRouter */ -export default router(otherRoute, routeWithTag) +export default router(otherRoute, routeWithTag, routeWithRouteParameterTag)