Skip to content

Commit d70edde

Browse files
committed
feat: push it further
1 parent 440ad59 commit d70edde

3 files changed

Lines changed: 100 additions & 25 deletions

File tree

packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,18 @@ export class ServerOperationBuilder {
114114
return {type, path, query, header, body}
115115
}
116116

117-
responseValidator(): string {
118-
const {specific, defaultResponse} = this.responseSchemas()
119-
120-
const pairs = specific.map((it) => `["${it.statusString}", ${it.schema}]`)
121-
122-
return `responseValidationFactory([${pairs}], ${defaultResponse?.schema})`
123-
}
124-
125117
responder(): {implementation: string} {
126118
const {specific, defaultResponse} = this.responseSchemas()
127119

128-
const implementation = object([
120+
const implementation = `b(r => (${object([
129121
...specific.map(
130-
(it) => `with${it.statusType}: r.with${it.statusType}<${it.type}>`,
122+
(it) =>
123+
`with${it.statusType}: r.with${it.statusType}<${it.type}>(${it.schema})`,
131124
),
132-
defaultResponse && `withDefault: r.withDefault<${defaultResponse.type}>`,
125+
defaultResponse &&
126+
`withDefault: r.withDefault<${defaultResponse.type}>(${defaultResponse.schema})`,
133127
"withStatus: r.withStatus",
134-
])
128+
])}))`
135129

136130
return {implementation}
137131
}

packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class KoaRouterBuilder extends AbstractRouterBuilder {
4848
"StatusCode4xx",
4949
"StatusCode5xx",
5050
"startServer",
51-
"r",
51+
"b",
5252
)
5353

5454
this.imports
@@ -98,9 +98,8 @@ export class KoaRouterBuilder extends AbstractRouterBuilder {
9898
this.operationTypes.push({
9999
operationId: builder.operationId,
100100
statements: [
101-
`const ${symbols.responderName} = ${responder.implementation}`,
102-
`type ${titleCase(symbols.responderName)} = typeof ${symbols.responderName} & KoaRuntimeResponder`,
103-
`const ${symbols.responseBodyValidator} = ${builder.responseValidator()}`,
101+
`const ${symbols.implPropName} = ${responder.implementation}`,
102+
`type ${titleCase(symbols.responderName)} = typeof ${symbols.implPropName}['responder'] & KoaRuntimeResponder`,
104103
buildExport({
105104
name: symbols.implTypeName,
106105
value: `(
@@ -130,12 +129,12 @@ router.${builder.method.toLowerCase()}('${symbols.implPropName}','${route(builde
130129
headers: ${params.header.schema ? `parseRequestInput(${symbols.requestHeaderSchema}, Reflect.get(ctx.request, "headers"), RequestInputType.RequestHeader)` : "undefined"}
131130
}
132131
133-
const response = await implementation.${symbols.implPropName}(input, ${symbols.responderName}, ctx)
132+
const response = await implementation.${symbols.implPropName}(input, ${symbols.implPropName}.responder, ctx)
134133
.catch(err => { throw KoaRuntimeError.HandlerError(err) })
135134
136135
const { status, body } = response instanceof KoaRuntimeResponse ? response.unpack() : response
137136
138-
ctx.body = ${symbols.responseBodyValidator}(status, body)
137+
ctx.body = ${symbols.implPropName}.validator(status, body)
139138
ctx.status = status
140139
return next();
141140
})`)

packages/typescript-koa-runtime/src/server.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type Router from "@koa/router"
66
import Koa, {type Middleware} from "koa"
77
import KoaBody from "koa-body"
88
import type {KoaBodyMiddlewareOptions} from "koa-body/lib/types"
9+
import {responseValidationFactory} from "./zod"
910

1011
// from https://stackoverflow.com/questions/39494689/is-it-possible-to-restrict-number-to-a-certain-range
1112
type Enumerate<
@@ -54,15 +55,29 @@ export class KoaRuntimeResponse<Type> {
5455
}
5556

5657
export type ResponderBuilder = {
57-
[key in `with${StatusCode}`]: <T>() => KoaRuntimeResponse<T>
58+
[key in `with${StatusCode}`]: <T, S = unknown>(
59+
s: S,
60+
) => () => KoaRuntimeResponse<T>
5861
} & {
5962
withStatus(status: StatusCode): KoaRuntimeResponse<unknown>
60-
withStatusCode1xx<T>(status: StatusCode1xx): KoaRuntimeResponse<T>
61-
withStatusCode2xx<T>(status: StatusCode2xx): KoaRuntimeResponse<T>
62-
withStatusCode3xx<T>(status: StatusCode3xx): KoaRuntimeResponse<T>
63-
withStatusCode4xx<T>(status: StatusCode4xx): KoaRuntimeResponse<T>
64-
withStatusCode5xx<T>(status: StatusCode5xx): KoaRuntimeResponse<T>
65-
withDefault<T>(status: StatusCode): KoaRuntimeResponse<T>
63+
withStatusCode1xx<T, S = unknown>(
64+
s: S,
65+
): (status: StatusCode1xx) => KoaRuntimeResponse<T>
66+
withStatusCode2xx<T, S = unknown>(
67+
s: S,
68+
): (status: StatusCode2xx) => KoaRuntimeResponse<T>
69+
withStatusCode3xx<T, S = unknown>(
70+
s: S,
71+
): (status: StatusCode3xx) => KoaRuntimeResponse<T>
72+
withStatusCode4xx<T, S = unknown>(
73+
s: S,
74+
): (status: StatusCode4xx) => KoaRuntimeResponse<T>
75+
withStatusCode5xx<T, S = unknown>(
76+
s: S,
77+
): (status: StatusCode5xx) => KoaRuntimeResponse<T>
78+
withDefault<T, S = unknown>(
79+
s: S,
80+
): (status: StatusCode) => KoaRuntimeResponse<T>
6681
}
6782

6883
function isValidStatusCode(status: unknown): status is StatusCode {
@@ -72,6 +87,73 @@ function isValidStatusCode(status: unknown): status is StatusCode {
7287
return !(status < 100 || status > 599)
7388
}
7489

90+
export const b = <T>(fn: (r: ResponderBuilder) => T) => {
91+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
92+
const responses: any[] = []
93+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
94+
let defaultResponse: any = undefined
95+
96+
const r: ResponderBuilder = new Proxy({} as ResponderBuilder, {
97+
get(_, prop: string) {
98+
const exactMatch = /^with(\d{3})$/.exec(prop)
99+
100+
if (exactMatch?.[1]) {
101+
const status = Number(exactMatch[1])
102+
103+
if (!isValidStatusCode(status)) {
104+
throw new Error(`Status ${status} is not a valid status code`)
105+
}
106+
107+
return <S>(s: S) => {
108+
responses.push([status.toString(), s])
109+
110+
return <T>() => new KoaRuntimeResponse<T>(status)
111+
}
112+
}
113+
114+
const groupMatch = /^withStatusCode([1-5]xx)$/.exec(prop)
115+
if (groupMatch?.[1]) {
116+
const range = groupMatch[1]
117+
118+
return <S>(s: S) => {
119+
responses.push([range, s])
120+
121+
return <T>(status: StatusCode) => {
122+
const expectedHundreds = Number(range[0])
123+
if (Math.floor(status / 100) !== expectedHundreds) {
124+
throw new Error(
125+
`Status ${status} is not a valid ${range} status code`,
126+
)
127+
}
128+
return new KoaRuntimeResponse<T>(status)
129+
}
130+
}
131+
}
132+
133+
if (prop === "withDefault") {
134+
return <S>(s: S) => {
135+
defaultResponse = s
136+
137+
return <T>(status: StatusCode) => new KoaRuntimeResponse<T>(status)
138+
}
139+
}
140+
141+
if (prop === "withStatus") {
142+
return (status: StatusCode) => new KoaRuntimeResponse<unknown>(status)
143+
}
144+
145+
throw new Error(`Unknown responder method: ${prop}`)
146+
},
147+
})
148+
149+
const responder = fn(r)
150+
151+
return {
152+
responder,
153+
validator: responseValidationFactory(responses, defaultResponse),
154+
}
155+
}
156+
75157
export const r: ResponderBuilder = new Proxy({} as ResponderBuilder, {
76158
get(_, prop: string) {
77159
const exactMatch = /^with(\d{3})$/.exec(prop)

0 commit comments

Comments
 (0)