Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/core/src/backend/registry_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Awaited<${responseType}>>`

return `ExtractRawResponse<${responseType}>`
}

/**
* Resolve body and query type strings based on the HTTP method.
* GET/HEAD routes use `ExtractQueryForGet`, others use
Expand Down Expand Up @@ -227,13 +240,18 @@ 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} }`
errorResponseType =
errorResponseType === 'unknown'
? validationError
: `${errorResponseType} | ${validationError}`
rawResponseUnion =
rawResponseUnion === 'unknown'
? validationError
: `${rawResponseUnion} | ${validationError}`
}

const { paramsType, paramsTuple } = this.#generateRouteParams(route)
Expand All @@ -254,6 +272,7 @@ export class RegistryGenerator {
query: ${queryType}
response: ${responseType}
errorResponse: ${errorResponseType}
rawResponse: ${rawResponseUnion}
}
}`
}
Expand Down Expand Up @@ -307,6 +326,7 @@ declare module '@tuyau/core/types' {
'ExtractErrorResponse',
'ExtractQuery',
'ExtractQueryForGet',
'ExtractRawResponse',
'ExtractResponse',
]
const vineImports = ['InferInput']
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/client/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ export type ExtractErrorResponse<T> = 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> = T extends { __response: infer R; __status: infer S }
? { status: S; response: R }
: { status: 200; response: T }

/**
* Registry mapping endpoint names to their definitions
*/
Expand Down
59 changes: 59 additions & 0 deletions packages/core/tests/generate_registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UsersController['index']>", imports: [] },
}),
]

const generator = new RegistryGenerator()
const content = generator.generateTypesContent(routes)

assert.include(
content,
"rawResponse: ExtractRawResponse<Awaited<ReturnType<UsersController['index']>>>",
)
})

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<typeof createUserValidator>', imports: [] },
response: { type: "ReturnType<UsersController['store']>", imports: [] },
}),
]

const generator = new RegistryGenerator()
const content = generator.generateTypesContent(routes)

assert.include(
content,
"rawResponse: ExtractRawResponse<Awaited<ReturnType<UsersController['store']>>> | { 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'/)
})
})
Loading