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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .changeset/client-unwrap-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@kubb/plugin-axios': minor
'@kubb/plugin-fetch': minor
---

Every generated call now resolves to a promise with an extra `unwrap()` method: it resolves to the
bare success body, or rejects with `error` for a call that didn't throw. `await getPetById(...)`
still returns the full result as before, so this is additive.
8 changes: 7 additions & 1 deletion internals/client/src/builders/generics.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ast } from 'kubb/kit'
import { resolverTs } from '@kubb/plugin-ts'
import { describe, expect, test } from 'vitest'
import { buildRequestResultGenerics } from './generics.ts'
import { buildRequestResultGenerics, buildResultType } from './generics.ts'

const node = ast.factory.createOperation({
operationId: 'getPetById',
Expand All @@ -17,3 +17,9 @@ describe('buildRequestResultGenerics', () => {
expect(buildRequestResultGenerics({ node, types: resolverTs })).toBe('GetPetByIdResponses, ThrowOnError')
})
})

describe('buildResultType', () => {
test('wraps RequestResult in Unwrappable', () => {
expect(buildResultType({ node, types: resolverTs })).toBe('Unwrappable<RequestResult<GetPetByIdResponses, ThrowOnError>>')
})
})
12 changes: 12 additions & 0 deletions internals/client/src/builders/generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ import type { OperationTypeNames } from '../resolveOperationTypes.ts'
export function buildRequestResultGenerics({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): string {
return `${types.response.responses(node)}, ThrowOnError`
}

/**
* Builds the result type name an operation's function signature and return statement use: the
* runtime's `Unwrappable`, wrapping `RequestResult` so the resolved call also carries an
* RTK-style `unwrap()` method.
*
* @example
* `buildResultType({ node, types }) // 'Unwrappable<RequestResult<AddPetResponses, ThrowOnError>>'`
*/
export function buildResultType({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): string {
return `Unwrappable<RequestResult<${buildRequestResultGenerics({ node, types })}>>`
}
4 changes: 2 additions & 2 deletions internals/client/src/builders/returnStatement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const node = ast.factory.createOperation({
})

describe('buildReturnStatement', () => {
test('forwards the call config and casts to the operation RequestResult', () => {
test('casts to RequestResult first, then wraps the call in withUnwrap', () => {
const callConfig = "{ method: 'POST', url: '/pet', ...config }"
expect(buildReturnStatement({ node, types: resolverTs, callConfig })).toBe(
"return request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>",
"return withUnwrap(request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>)",
)
})
})
14 changes: 10 additions & 4 deletions internals/client/src/builders/returnStatement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import { buildRequestResultGenerics } from './generics.ts'

/**
* Builds the return statement of a generated operation function. The runtime call already resolves
* to `{ data, error, request, response }`; the generated code forwards that result and casts it to
* the operation's `RequestResult`, which carries the `throwOnError` discrimination.
* to `{ data, error, request, response }`; the generated code casts that result to the operation's
* `RequestResult` first (as it always did), then wraps it with `withUnwrap` so the caller can
* `await` it directly or call `.unwrap()` for the bare success body.
*
* Casting the raw call and only then wrapping keeps `withUnwrap`'s generic inferred as
* `RequestResult`, not the runtime's own internal result type: an `Unwrappable<A>` cast straight to
* `Unwrappable<B>` carries a `.then` overload pinned to `A`, and that two-generic swap is too narrow
* for `as` to allow, even where `A` and `B` alone would satisfy it.
*
* @example
* `return request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>`
* `return withUnwrap(request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>)`
*/
export function buildReturnStatement({ node, types, callConfig }: { node: ast.OperationNode; types: OperationTypeNames; callConfig: string }): string {
return `return request(${callConfig}) as Promise<RequestResult<${buildRequestResultGenerics({ node, types })}>>`
return `return withUnwrap(request(${callConfig}) as Promise<RequestResult<${buildRequestResultGenerics({ node, types })}>>)`
}
2 changes: 1 addition & 1 deletion internals/client/src/builders/sdkMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function buildCallConfig({
/**
* Builds a single instance method for a generated SDK class. The body forwards the single grouped
* `options` object to the instance's own client (`this.client`, built once in the constructor) and
* returns the `RequestResult`. A per-call `options.client` still overrides the instance client, so
* returns the `Unwrappable<RequestResult>`. A per-call `options.client` still overrides the instance client, so
* one operation can be routed to a different environment without a new instance.
*/
export function buildSdkMethod({
Expand Down
4 changes: 2 additions & 2 deletions internals/client/src/builders/signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ describe('buildGroupedOptionsSignature', () => {
expect(signature.paramsSignature).toBe('options: Options<ListPetsOptions, ThrowOnError> = {}')
})

test('keys the return type on the plugin-ts per-status responses record', () => {
test('keys the return type on the plugin-ts per-status responses record, wrapped in Unwrappable', () => {
const signature = buildGroupedOptionsSignature({ node: addPet, types: resolverTs })
expect(signature.returnType).toBe('Promise<RequestResult<AddPetResponses, ThrowOnError>>')
expect(signature.returnType).toBe('Unwrappable<RequestResult<AddPetResponses, ThrowOnError>>')
})
})
7 changes: 3 additions & 4 deletions internals/client/src/builders/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ast } from 'kubb/kit'
import { getRequestGroupOptionality } from '@internals/shared'
import { createFunctionParameter, createFunctionParameters, functionPrinter } from '@kubb/plugin-ts'
import type { OperationTypeNames } from '../resolveOperationTypes.ts'
import { buildRequestResultGenerics } from './generics.ts'
import { buildResultType } from './generics.ts'

const declarationPrinter = functionPrinter({ mode: 'declaration' })

Expand All @@ -15,7 +15,7 @@ export type GroupedOptionsSignature = {
*/
paramsSignature: string
/**
* The function return type: `Promise<RequestResult<<Name>Responses, ThrowOnError>>`.
* The function return type: `Unwrappable<RequestResult<<Name>Responses, ThrowOnError>>`.
*/
returnType: string
/**
Expand All @@ -35,7 +35,6 @@ export type GroupedOptionsSignature = {
*/
export function buildGroupedOptionsSignature({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): GroupedOptionsSignature {
const optionsName = types.response.options(node)
const resultGenerics = buildRequestResultGenerics({ node, types })
const { isOptional } = getRequestGroupOptionality(node)

const paramsSignature =
Expand All @@ -47,7 +46,7 @@ export function buildGroupedOptionsSignature({ node, types }: { node: ast.Operat

return {
paramsSignature,
returnType: `Promise<RequestResult<${resultGenerics}>>`,
returnType: buildResultType({ node, types }),
generics: ['ThrowOnError extends boolean = true'],
}
}
2 changes: 1 addition & 1 deletion internals/client/src/components/Operation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type Props = {

/**
* Renders one client operation: the grouped `<Name>Request` type and the function that forwards a
* single `options` object to the resolved client and returns the `RequestResult`. The type, signature,
* single `options` object to the resolved client and returns the `Unwrappable<RequestResult>`. The type, signature,
* and call config are built with the AST factory, and only the jsx-renderer emits the source.
*/
export function Operation({ name, node, types, zodResolver, validator, security, isExportable = true, isIndexable = true }: Props): KubbReactNode {
Expand Down
6 changes: 3 additions & 3 deletions internals/client/src/generators/clientGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { ContractClientFactory } from '../types.ts'
* Builds the built-in per-operation generator shared by the client plugins (`@kubb/plugin-fetch`,
* `@kubb/plugin-axios`). Emits one async function per OpenAPI operation using the shared
* `Operation` component: a grouped `<Name>Request` type and a function that forwards a single
* `options` object to the bundled `client` and returns the `RequestResult`. Only the generator
* `options` object to the bundled `client` and returns the `Unwrappable<RequestResult>`. Only the generator
* `name` differs between plugins; every other resolution, import, and rendering step is identical.
*/
export function createClientGenerator<TFactory extends ContractClientFactory>(name: string): Generator<TFactory> {
Expand Down Expand Up @@ -87,9 +87,9 @@ export function createClientGenerator<TFactory extends ContractClientFactory>(na
banner={resolver.default.banner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
footer={resolver.default.footer(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
>
<File.Import name={eventStream ? ['client', 'toEventStream'] : ['client']} root={meta.file.path} path={clientPath} />
<File.Import name={eventStream ? ['client', 'toEventStream'] : ['client', 'withUnwrap']} root={meta.file.path} path={clientPath} />
<File.Import
name={eventStream ? ['Options', 'EventStreamResult', 'SuccessOf'] : ['Options', 'RequestResult']}
name={eventStream ? ['Options', 'EventStreamResult', 'SuccessOf'] : ['Options', 'Unwrappable', 'RequestResult']}
root={meta.file.path}
path={clientPath}
isTypeOnly
Expand Down
4 changes: 2 additions & 2 deletions internals/client/src/generators/sdkGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ export function createSdkGenerator<TFactory extends ContractClientFactory>(): Ge

return (
<File key={file.path} baseName={file.baseName} path={file.path} meta={file.meta} banner={banner(file)} footer={footer(file)}>
<File.Import name={['createClient']} root={file.path} path={clientPath} />
<File.Import name={['ClientConfig', 'ClientInstance', 'Options', 'RequestResult']} root={file.path} path={clientPath} isTypeOnly />
<File.Import name={['createClient', 'withUnwrap']} root={file.path} path={clientPath} />
<File.Import name={['ClientConfig', 'ClientInstance', 'Options', 'Unwrappable', 'RequestResult']} root={file.path} path={clientPath} isTypeOnly />

{validator === 'zod' && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && <File.Import name={['z']} path="zod" isTypeOnly />}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { AddPetOptions, AddPetResponses } from './AddPet'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /pet}
*/
export function addPet<ThrowOnError extends boolean = true>(
options: Options<AddPetOptions, ThrowOnError>,
): Promise<RequestResult<AddPetResponses, ThrowOnError>> {
): Unwrappable<RequestResult<AddPetResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>
return withUnwrap(request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { AddPetOptions, AddPetResponses } from './AddPet'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'
import { AddPetResponse } from './AddPet'

/**
* {@link /pet}
*/
export function addPet<ThrowOnError extends boolean = true>(
options: Options<AddPetOptions, ThrowOnError>,
): Promise<RequestResult<AddPetResponses, ThrowOnError>> {
): Unwrappable<RequestResult<AddPetResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({ method: 'POST', url: '/pet', validator: { response: AddPetResponse }, ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>
return withUnwrap(
request({ method: 'POST', url: '/pet', validator: { response: AddPetResponse }, ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>,
)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { DeletePetOptions, DeletePetResponses } from './DeletePet'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /pet/:petId}
*/
export function deletePet<ThrowOnError extends boolean = true>(
options: Options<DeletePetOptions, ThrowOnError>,
): Promise<RequestResult<DeletePetResponses, ThrowOnError>> {
): Unwrappable<RequestResult<DeletePetResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({ method: 'DELETE', url: '/pet/{petId}', ...config }) as Promise<RequestResult<DeletePetResponses, ThrowOnError>>
return withUnwrap(request({ method: 'DELETE', url: '/pet/{petId}', ...config }) as Promise<RequestResult<DeletePetResponses, ThrowOnError>>)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { FindPetsByTagsOptions, FindPetsByTagsResponses } from './FindPetsByTags'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /pet/findByTags}
*/
export function findPetsByTags<ThrowOnError extends boolean = true>(
options: Options<FindPetsByTagsOptions, ThrowOnError>,
): Promise<RequestResult<FindPetsByTagsResponses, ThrowOnError>> {
): Unwrappable<RequestResult<FindPetsByTagsResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({ method: 'GET', url: '/pet/findByTags', ...config }) as Promise<RequestResult<FindPetsByTagsResponses, ThrowOnError>>
return withUnwrap(request({ method: 'GET', url: '/pet/findByTags', ...config }) as Promise<RequestResult<FindPetsByTagsResponses, ThrowOnError>>)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { GetPetByIdOptions, GetPetByIdResponses } from './GetPetById'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /pet/:petId}
*/
export function getPetById<ThrowOnError extends boolean = true>(
options: Options<GetPetByIdOptions, ThrowOnError>,
): Promise<RequestResult<GetPetByIdResponses, ThrowOnError>> {
): Unwrappable<RequestResult<GetPetByIdResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({ method: 'GET', url: '/pet/{petId}', ...config }) as Promise<RequestResult<GetPetByIdResponses, ThrowOnError>>
return withUnwrap(request({ method: 'GET', url: '/pet/{petId}', ...config }) as Promise<RequestResult<GetPetByIdResponses, ThrowOnError>>)
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { GetPetByIdOptions, GetPetByIdResponses } from './GetPetById'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /pet/:petId}
*/
export function getPetById<ThrowOnError extends boolean = true>(
options: Options<GetPetByIdOptions, ThrowOnError>,
): Promise<RequestResult<GetPetByIdResponses, ThrowOnError>> {
): Unwrappable<RequestResult<GetPetByIdResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({
method: 'GET',
url: '/pet/{petId}',
security: [{ type: 'oauth2' }, { type: 'apiKey', name: 'api_key', in: 'header' }],
...config,
}) as Promise<RequestResult<GetPetByIdResponses, ThrowOnError>>
return withUnwrap(
request({ method: 'GET', url: '/pet/{petId}', security: [{ type: 'oauth2' }, { type: 'apiKey', name: 'api_key', in: 'header' }], ...config }) as Promise<
RequestResult<GetPetByIdResponses, ThrowOnError>
>,
)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { GetProjectOptions, GetProjectResponses } from './GetProject'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /projects/:project_id}
*/
export function getProject<ThrowOnError extends boolean = true>(
options: Options<GetProjectOptions, ThrowOnError>,
): Promise<RequestResult<GetProjectResponses, ThrowOnError>> {
): Unwrappable<RequestResult<GetProjectResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({ method: 'GET', url: '/projects/{project_id}', ...config }) as Promise<RequestResult<GetProjectResponses, ThrowOnError>>
return withUnwrap(request({ method: 'GET', url: '/projects/{project_id}', ...config }) as Promise<RequestResult<GetProjectResponses, ThrowOnError>>)
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
/* eslint-disable no-alert, no-console */

import type { Options, RequestResult } from './.kubb/client'
import type { Options, Unwrappable, RequestResult } from './.kubb/client'
import type { ListPetsStyledOptions, ListPetsStyledResponses } from './ListPetsStyled'
import { client } from './.kubb/client'
import { client, withUnwrap } from './.kubb/client'

/**
* {@link /pets/:petId}
*/
export function listPetsStyled<ThrowOnError extends boolean = true>(
options: Options<ListPetsStyledOptions, ThrowOnError>,
): Promise<RequestResult<ListPetsStyledResponses, ThrowOnError>> {
): Unwrappable<RequestResult<ListPetsStyledResponses, ThrowOnError>> {
const { client: request = client, ...config } = options

return request({
method: 'GET',
url: '/pets/{petId}',
styles: { path: { petId: { style: 'matrix', explode: true } }, query: { tags: { style: 'pipeDelimited', explode: false } } },
...config,
}) as Promise<RequestResult<ListPetsStyledResponses, ThrowOnError>>
return withUnwrap(
request({
method: 'GET',
url: '/pets/{petId}',
styles: { path: { petId: { style: 'matrix', explode: true } }, query: { tags: { style: 'pipeDelimited', explode: false } } },
...config,
}) as Promise<RequestResult<ListPetsStyledResponses, ThrowOnError>>,
)
}
Loading
Loading