diff --git a/packages/core/src/backend/registry_generator.ts b/packages/core/src/backend/registry_generator.ts index b68f59f..2bf1d94 100644 --- a/packages/core/src/backend/registry_generator.ts +++ b/packages/core/src/backend/registry_generator.ts @@ -109,6 +109,19 @@ export class RegistryGenerator { return `ExtractErrorResponse<${responseType}>` } + /** + * Wrap a response type with `ExtractRawResponse` to expose the full + * `{ status; response }` discriminated union across all status codes + * (success + error). Consumed by OpenAPI generators. + */ + #wrapRawResponseType(responseType: string): string { + if (responseType === 'unknown' || responseType === '{}') return 'unknown' + if (responseType.startsWith('ReturnType<')) + return `ExtractRawResponse>` + + return `ExtractRawResponse<${responseType}>` + } + /** * Resolve body and query type strings based on the HTTP method. * GET/HEAD routes use `ExtractQueryForGet`, others use @@ -227,6 +240,7 @@ export class RegistryGenerator { const hasValidator = requestType !== '{}' let errorResponseType = this.#wrapErrorResponseType(rawResponseType) + let rawResponseUnion = this.#wrapRawResponseType(rawResponseType) if (hasValidator && this.#validationErrorType !== false) { const validationError = `{ status: 422; response: ${this.#validationErrorType} }` @@ -234,6 +248,10 @@ export class RegistryGenerator { errorResponseType === 'unknown' ? validationError : `${errorResponseType} | ${validationError}` + rawResponseUnion = + rawResponseUnion === 'unknown' + ? validationError + : `${rawResponseUnion} | ${validationError}` } const { paramsType, paramsTuple } = this.#generateRouteParams(route) @@ -254,6 +272,7 @@ export class RegistryGenerator { query: ${queryType} response: ${responseType} errorResponse: ${errorResponseType} + rawResponse: ${rawResponseUnion} } }` } @@ -307,6 +326,7 @@ declare module '@tuyau/core/types' { 'ExtractErrorResponse', 'ExtractQuery', 'ExtractQueryForGet', + 'ExtractRawResponse', 'ExtractResponse', ] const vineImports = ['InferInput'] diff --git a/packages/core/src/client/types/types.ts b/packages/core/src/client/types/types.ts index 71fb721..83a978b 100644 --- a/packages/core/src/client/types/types.ts +++ b/packages/core/src/client/types/types.ts @@ -124,6 +124,19 @@ export type ExtractErrorResponse = T extends { __response: infer R; __status: : { status: S; response: R } : never +/** + * Normalizes a controller return type into a discriminated union of + * `{ status; response }` across ALL status codes (success and error alike), + * preserving the status that `ExtractResponse` drops (every 2xx collapses to its + * body) and `ExtractErrorResponse` splits off. Members carrying no status brand + * (a plain returned body) default to status 200. Consumers such as OpenAPI + * generators use this to emit every documented response with its correct code + * (200/201/204/4xx/…). + */ +export type ExtractRawResponse = T extends { __response: infer R; __status: infer S } + ? { status: S; response: R } + : { status: 200; response: T } + /** * Registry mapping endpoint names to their definitions */ diff --git a/packages/core/tests/generate_registry.spec.ts b/packages/core/tests/generate_registry.spec.ts index c5790ac..8472fde 100644 --- a/packages/core/tests/generate_registry.spec.ts +++ b/packages/core/tests/generate_registry.spec.ts @@ -447,3 +447,62 @@ test.group('filterRoute', () => { ) }) }) + +test.group('generateTypesContent | rawResponse union', () => { + test('emits rawResponse wrapping the awaited return type', ({ assert }) => { + const routes = [ + makeRoute({ + name: 'users.index', + methods: ['GET'], + pattern: '/users', + response: { type: "ReturnType", imports: [] }, + }), + ] + + const generator = new RegistryGenerator() + const content = generator.generateTypesContent(routes) + + assert.include( + content, + "rawResponse: ExtractRawResponse>>", + ) + }) + + test('unions the 422 validation error into rawResponse for validated routes', ({ assert }) => { + const routes = [ + makeRoute({ + name: 'users.store', + methods: ['POST'], + pattern: '/users', + request: { type: 'InferInput', imports: [] }, + response: { type: "ReturnType", imports: [] }, + }), + ] + + const generator = new RegistryGenerator() + const content = generator.generateTypesContent(routes) + + assert.include( + content, + "rawResponse: ExtractRawResponse>> | { status: 422; response: { errors: SimpleError[] } }", + ) + }) + + test('rawResponse is unknown for routes without a known return type', ({ assert }) => { + const routes = [makeRoute({ name: 'home', methods: ['GET'], pattern: '/' })] + + const generator = new RegistryGenerator() + const content = generator.generateTypesContent(routes) + + assert.include(content, 'rawResponse: unknown') + }) + + test('imports ExtractRawResponse from @tuyau/core/types', ({ assert }) => { + const routes = [makeRoute({ name: 'home', methods: ['GET'], pattern: '/' })] + + const generator = new RegistryGenerator() + const content = generator.generateTypesContent(routes) + + assert.match(content, /import type \{[^}]*ExtractRawResponse[^}]*\} from '@tuyau\/core\/types'/) + }) +})