Skip to content

Commit

Permalink
Support documenting route params
Browse files Browse the repository at this point in the history
Implemented routeParam descriptions and validation
  • Loading branch information
wilbrt authored and akheron committed May 27, 2023
1 parent 12fa716 commit a6f578e
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.Ok<MyResult> | Response.BadRequest<string>> =
route.post(...).use(Parser.body(bodyCodec)).handler(...)
Expand Down
51 changes: 46 additions & 5 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -189,6 +202,10 @@ const getRouteDeclaration = (
const summary = getRouteSummary(symbol)
const tags = getSymbolTags(symbol)
const routeInput = getRouteInput(ctx, symbol)
const routeParameterDescriptions = new Map<string, string | undefined>(
getRouteParameters(symbol)
)

const operationId =
getRouteOperationId(symbol) ?? symbol.escapedName.toString()

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<string, string | undefined>
): 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),
}
})
}
Expand Down Expand Up @@ -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 = (
Expand Down
41 changes: 41 additions & 0 deletions tests/__snapshots__/generate.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion tests/other-routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Response, route, Route, router } from 'typera-express'

/**
* @routeParam myRouteParameter 12345
*/
const routeWithRouteParameterTag: Route<
Response.Ok<string> | Response.BadRequest<string, undefined>
> = route
.get('/route-with-route-parameter-tag/:myRouteParameter')
.handler(() => Response.ok('hello'))

const otherRoute: Route<Response.Ok<string>> = route
.get('/other-route')
.handler(() => Response.ok('hello'))
Expand All @@ -16,4 +25,4 @@ const routeWithTag: Route<Response.Ok<string>> = route
* @prefix /other-stuff
* @tags TagFromRouter
*/
export default router(otherRoute, routeWithTag)
export default router(otherRoute, routeWithTag, routeWithRouteParameterTag)

0 comments on commit a6f578e

Please sign in to comment.