@kubb/plugin-client 5.12.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/client-Dq0z61hu.d.ts +554 -0
- package/dist/index.cjs +1378 -784
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -74
- package/dist/index.d.ts +114 -74
- package/dist/index.js +1376 -786
- package/dist/index.js.map +1 -1
- package/dist/serializers-BRgfPGr1.d.ts +119 -0
- package/dist/{standardSchema-BShJWz2i.d.ts → standardSchema-CGuiSK7F.d.ts} +15 -37
- package/dist/templates/client.d.ts +3 -2
- package/dist/templates/core.d.ts +3 -3
- package/dist/templates/mediaTypes/formData.d.ts +5 -0
- package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
- package/dist/templates/mediaTypes.d.ts +2 -0
- package/dist/templates/serializers/cookies.d.ts +5 -0
- package/dist/templates/serializers/headers.d.ts +5 -0
- package/dist/templates/serializers/path.d.ts +5 -0
- package/dist/templates/serializers/pathStyles.d.ts +5 -0
- package/dist/templates/serializers/query.d.ts +5 -0
- package/dist/templates/serializers/queryStyles.d.ts +14 -0
- package/dist/templates/serializers.d.ts +2 -2
- package/dist/templates/sse.d.ts +20 -113
- package/dist/templates/standardSchema.d.ts +1 -1
- package/docs/guides/authentication.md +9 -1
- package/docs/guides/migrate-to-v6.md +139 -0
- package/docs/guides/validate-requests-and-responses.md +8 -3
- package/docs/guides/write-your-client.md +117 -11
- package/docs/options.md +2 -2
- package/docs/overview.md +1 -1
- package/kubb-extension.json +1 -1
- package/package.json +6 -5
- package/src/builders/callConfig.ts +80 -0
- package/src/builders/contentType.ts +35 -0
- package/src/builders/generics.ts +29 -0
- package/src/builders/operationBody.ts +43 -0
- package/src/builders/operationData.ts +64 -0
- package/src/builders/returnStatement.ts +103 -0
- package/src/builders/security.ts +125 -0
- package/src/builders/signature.ts +71 -0
- package/src/builders/styles.ts +94 -0
- package/src/builders/validator.ts +84 -0
- package/src/builders/validatorOptions.ts +53 -0
- package/src/clientTypes.ts +195 -0
- package/src/components/ClientRuntime.tsx +97 -0
- package/src/components/Operation.tsx +72 -0
- package/src/components/SdkClient.tsx +100 -0
- package/src/components/SdkFacade.tsx +35 -0
- package/src/generators/clientGenerator.tsx +91 -6
- package/src/generators/sdkGenerator.tsx +209 -0
- package/src/index.ts +3 -1
- package/src/macros.ts +9 -0
- package/src/plugin.ts +7 -8
- package/src/resolver.ts +28 -0
- package/src/setup.ts +90 -0
- package/src/types.ts +4 -2
- package/templates/client.ts +2 -1
- package/templates/core.ts +735 -286
- package/templates/mediaTypes/formData.ts +59 -0
- package/templates/mediaTypes/urlencoded.ts +33 -0
- package/templates/mediaTypes.ts +147 -0
- package/templates/serializers/cookies.ts +58 -0
- package/templates/serializers/headers.ts +35 -0
- package/templates/serializers/path.ts +19 -0
- package/templates/serializers/pathStyles.ts +27 -0
- package/templates/serializers/query.ts +21 -0
- package/templates/serializers/queryStyles.ts +107 -0
- package/templates/serializers.ts +48 -364
- package/templates/sse.ts +87 -178
- package/templates/standardSchema.ts +16 -37
- package/dist/client-BxO6Dygu.d.ts +0 -355
- package/dist/serializers-Ds8zZwOO.d.ts +0 -190
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getResponseHeadersTypeNames, type OperationTypeNames, type OperationTypeSource } from '@internals/client'
|
|
2
|
+
import { operationKey, resolveOperationFile } from '@internals/shared'
|
|
3
|
+
import { ast, type Generator } from '@kubb/kit'
|
|
4
|
+
import { pluginZodName } from '@kubb/plugin-zod'
|
|
5
|
+
import type { ContractClientFactory } from '../types.ts'
|
|
6
|
+
import { getOperationSecurity, type SecurityRequirement } from './security.ts'
|
|
7
|
+
import { buildValidatorHooks, type ValidatorHooks } from './validator.ts'
|
|
8
|
+
import { isValidatorEnabled } from './validatorOptions.ts'
|
|
9
|
+
|
|
10
|
+
export type GeneratorContext = Parameters<NonNullable<Generator<ContractClientFactory>['operations']>>[1]
|
|
11
|
+
|
|
12
|
+
export type OperationData = {
|
|
13
|
+
node: ast.HttpOperationNode
|
|
14
|
+
name: string
|
|
15
|
+
types: OperationTypeNames
|
|
16
|
+
typeFile: ast.FileNode
|
|
17
|
+
/**
|
|
18
|
+
* `null` unless `validator` turns a direction on.
|
|
19
|
+
*/
|
|
20
|
+
zodFile: ast.FileNode | null
|
|
21
|
+
validatorHooks: ValidatorHooks
|
|
22
|
+
/**
|
|
23
|
+
* The names of the headers types the result references.
|
|
24
|
+
*/
|
|
25
|
+
headerTypeNames: Array<string>
|
|
26
|
+
security?: Array<SecurityRequirement>
|
|
27
|
+
baseURL?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves what the function and SDK generators both print for one operation: its name, the files its types and zod
|
|
32
|
+
* schemas come from, and its validator hooks, security, base URL and response headers types.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveOperationData({ node, ctx, types }: { node: ast.HttpOperationNode; ctx: GeneratorContext; types: OperationTypeSource }): OperationData {
|
|
35
|
+
const { driver, resolver, root } = ctx
|
|
36
|
+
const { output, validator, importPath } = ctx.options
|
|
37
|
+
const pluginZod = isValidatorEnabled(validator) ? driver.getPlugin(pluginZodName) : null
|
|
38
|
+
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
node,
|
|
42
|
+
name: resolver.resolveName({ key: operationKey(node), name: resolver.name(node.operationId) }),
|
|
43
|
+
types,
|
|
44
|
+
typeFile: resolveOperationFile({ ctx, node, resolver: types.resolver, root, output: types.output ?? output, group: types.group }),
|
|
45
|
+
zodFile:
|
|
46
|
+
zodResolver && pluginZod?.options
|
|
47
|
+
? resolveOperationFile({
|
|
48
|
+
ctx,
|
|
49
|
+
node,
|
|
50
|
+
resolver: zodResolver,
|
|
51
|
+
root,
|
|
52
|
+
output: pluginZod.options.output ?? output,
|
|
53
|
+
group: pluginZod.options.group ?? undefined,
|
|
54
|
+
})
|
|
55
|
+
: null,
|
|
56
|
+
validatorHooks: buildValidatorHooks({ node, validator, zodResolver }),
|
|
57
|
+
headerTypeNames: getResponseHeadersTypeNames({ node, types }),
|
|
58
|
+
// The fetch and axios clients write `securitySchemes` into `.kubb/client.ts`. A custom client module has none.
|
|
59
|
+
security: getOperationSecurity({ auth: node.auth, schemes: ctx.meta.auth, hoist: importPath === undefined }),
|
|
60
|
+
baseURL: ast.http.getBaseURL(
|
|
61
|
+
ast.server.get({ servers: node.servers, index: (ctx.adapter.options as { server?: { index?: number } } | undefined)?.server?.index }),
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getEventStream } from '@internals/shared'
|
|
2
|
+
import type { ast } from '@kubb/kit'
|
|
3
|
+
import { buildResponseHeadersType, buildResultTypeRef, resultTypeName, type OperationTypeNames } from '@internals/client'
|
|
4
|
+
import type { ReturnTypeOption } from '../types.ts'
|
|
5
|
+
import { buildRequestResultGenerics } from './generics.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the return statement of a generated operation function. The client call takes the responses record and
|
|
9
|
+
* `ThrowOnError` as type arguments, so its result is already the operation's `RequestResult` and needs no cast. With the
|
|
10
|
+
* default `returnType: 'full'` it is wrapped in `withUnwrap`, so the caller can `await` it or call `.unwrap()`. With
|
|
11
|
+
* `returnType: 'data'` it goes through `unwrapResult`, which keeps the success body when the call throws on errors.
|
|
12
|
+
* With `returnType: 'plain'` it is returned as is.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* `return withUnwrap(apiClient<AddPetResponses, ThrowOnError>({ method: 'POST', url: '/pet', ...config }))`
|
|
16
|
+
* @example
|
|
17
|
+
* `return unwrapResult(apiClient<AddPetResponses, ThrowOnError>({ ... }), resolveThrowOnError(config.throwOnError, true))`
|
|
18
|
+
* @example
|
|
19
|
+
* `return apiClient<AddPetResponses, ThrowOnError>({ method: 'POST', url: '/pet', ...config })`
|
|
20
|
+
*/
|
|
21
|
+
export function buildReturnStatement({
|
|
22
|
+
node,
|
|
23
|
+
types,
|
|
24
|
+
callConfig,
|
|
25
|
+
returnType,
|
|
26
|
+
throwOnErrorDefault,
|
|
27
|
+
}: {
|
|
28
|
+
node: ast.OperationNode
|
|
29
|
+
types: OperationTypeNames
|
|
30
|
+
callConfig: string
|
|
31
|
+
returnType: ReturnTypeOption
|
|
32
|
+
throwOnErrorDefault: boolean
|
|
33
|
+
}): string {
|
|
34
|
+
const call = `apiClient<${buildRequestResultGenerics({ node, types })}>(${callConfig})`
|
|
35
|
+
if (returnType === 'data') return `return unwrapResult(${call}, resolveThrowOnError(config.throwOnError, ${throwOnErrorDefault}))`
|
|
36
|
+
if (returnType === 'plain') return `return ${call}`
|
|
37
|
+
return `return withUnwrap(${call})`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds the return type and statement of a streamed operation, which calls `apiClient.stream`. The item type is
|
|
42
|
+
* `TypedEvent<...>` when each event declares its own `event` and `data`, the success type itself for JSON Lines, and
|
|
43
|
+
* otherwise the default, `ServerSentEvent` around the payload, which needs no type argument.
|
|
44
|
+
*/
|
|
45
|
+
export function buildEventStreamReturn({
|
|
46
|
+
node,
|
|
47
|
+
types,
|
|
48
|
+
callConfig,
|
|
49
|
+
returnType,
|
|
50
|
+
throwOnErrorDefault,
|
|
51
|
+
}: {
|
|
52
|
+
node: ast.HttpOperationNode
|
|
53
|
+
types: OperationTypeNames
|
|
54
|
+
callConfig: string
|
|
55
|
+
returnType: ReturnTypeOption
|
|
56
|
+
throwOnErrorDefault: boolean
|
|
57
|
+
}): {
|
|
58
|
+
returnType: string
|
|
59
|
+
statement: string
|
|
60
|
+
} {
|
|
61
|
+
const shape = getEventStream(node) ?? 'data'
|
|
62
|
+
const responses = types.response.responses(node)
|
|
63
|
+
const headers = buildResponseHeadersType({ node, types })
|
|
64
|
+
const success = `SuccessOf<${responses}>`
|
|
65
|
+
const item = { event: `TypedEvent<${success}>`, data: null, line: success }[shape]
|
|
66
|
+
// The headers come after the item in `client.stream`, so a typed-headers call spells out the default item.
|
|
67
|
+
const callItem = item ?? (headers ? `ServerSentEvent<${success}>` : null)
|
|
68
|
+
const call = `apiClient.stream<${[responses, 'ThrowOnError', callItem, headers].filter(Boolean).join(', ')}>(${callConfig})`
|
|
69
|
+
const result = (name: string) => buildResultTypeRef(name, responses, headers, ['ThrowOnError', item])
|
|
70
|
+
if (returnType === 'plain') return { returnType: `Promise<${result('StreamResult')}>`, statement: `return ${call}` }
|
|
71
|
+
if (returnType === 'data') {
|
|
72
|
+
return {
|
|
73
|
+
returnType: `Promise<${result('UnwrappedStreamResult')}>`,
|
|
74
|
+
statement: `return unwrapResult(${call}, resolveThrowOnError(config.throwOnError, ${throwOnErrorDefault}))`,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { returnType: `Unwrappable<${result('StreamResult')}>`, statement: `return withUnwrap(${call})` }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The stream types an operation file imports from the client for one streamed operation: the result and options types
|
|
82
|
+
* for `returnType`, their `WithHeaders` form when the response declares headers, and `SuccessOf` with `TypedEvent` or
|
|
83
|
+
* `ServerSentEvent` for an item type the call spells out.
|
|
84
|
+
*/
|
|
85
|
+
export function buildEventStreamImports({
|
|
86
|
+
node,
|
|
87
|
+
types,
|
|
88
|
+
returnType,
|
|
89
|
+
}: {
|
|
90
|
+
node: ast.OperationNode
|
|
91
|
+
types: OperationTypeNames
|
|
92
|
+
returnType: ReturnTypeOption
|
|
93
|
+
}): Array<string> {
|
|
94
|
+
const shape = getEventStream(node) ?? 'data'
|
|
95
|
+
const headers = buildResponseHeadersType({ node, types }) !== null
|
|
96
|
+
const result = {
|
|
97
|
+
plain: ['Options', 'StreamResult'],
|
|
98
|
+
full: ['StreamOptions', 'Unwrappable', 'StreamResult'],
|
|
99
|
+
data: ['StreamOptions', 'UnwrappedStreamResult'],
|
|
100
|
+
}[returnType].map((name) => (name.endsWith('Result') ? resultTypeName(name, headers) : name))
|
|
101
|
+
const item = { event: ['SuccessOf', 'TypedEvent'], data: headers ? ['SuccessOf', 'ServerSentEvent'] : [], line: ['SuccessOf'] }[shape]
|
|
102
|
+
return [...result, ...item]
|
|
103
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isValidVarName, objectKey, toLiteral } from '@internals/utils'
|
|
2
|
+
import { ast, text } from '@kubb/kit'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A resolved security scheme. The runtime calls the configured `auth` resolver with this object and places the
|
|
6
|
+
* returned token: `http` bearer/basic on `Authorization`, `apiKey` under `name` in the header/query/cookie, and
|
|
7
|
+
* `oauth2`/`openIdConnect` as a bearer token.
|
|
8
|
+
*/
|
|
9
|
+
export type Auth = {
|
|
10
|
+
type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
|
|
11
|
+
scheme?: 'bearer' | 'basic'
|
|
12
|
+
name?: string
|
|
13
|
+
in?: 'header' | 'query' | 'cookie'
|
|
14
|
+
scopes?: Array<string>
|
|
15
|
+
flows?: Record<string, Omit<ast.OAuthFlow, 'scopes'>>
|
|
16
|
+
metadataUrl?: string
|
|
17
|
+
url?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One entry of a generated call's `security` array: an OpenAPI security requirement, as one scheme or as the schemes it
|
|
22
|
+
* needs together. The runtime sends the first requirement whose schemes all resolve a token.
|
|
23
|
+
*/
|
|
24
|
+
export type SecurityRequirement = OperationAuth | Array<OperationAuth>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A scheme of one operation. `ref` names its entry in `securitySchemes`, which the call then points at.
|
|
28
|
+
*/
|
|
29
|
+
type OperationAuth = Auth & { ref?: string }
|
|
30
|
+
|
|
31
|
+
// The order the keys of an inline scheme are printed in.
|
|
32
|
+
const inlineAuthKeys = ['type', 'scheme', 'name', 'in', 'scopes', 'flows', 'metadataUrl', 'url'] as const
|
|
33
|
+
|
|
34
|
+
function serializeScheme({ ref, ...auth }: OperationAuth): string {
|
|
35
|
+
const scopes = auth.scopes?.length ? auth.scopes : undefined
|
|
36
|
+
if (ref) {
|
|
37
|
+
const schemeRef = isValidVarName(ref) ? `securitySchemes.${ref}` : `securitySchemes[${text.quote(ref)}]`
|
|
38
|
+
return scopes ? `{ ...${schemeRef}, scopes: ${toLiteral(scopes)} }` : schemeRef
|
|
39
|
+
}
|
|
40
|
+
const entries = inlineAuthKeys.flatMap((key) => {
|
|
41
|
+
const entryValue = key === 'scopes' ? scopes : auth[key]
|
|
42
|
+
return entryValue ? [`${key}: ${toLiteral(entryValue)}`] : []
|
|
43
|
+
})
|
|
44
|
+
return `{ ${entries.join(', ')} }`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function serializeRequirement(requirement: SecurityRequirement): string {
|
|
48
|
+
return Array.isArray(requirement) ? `[${requirement.map(serializeScheme).join(', ')}]` : serializeScheme(requirement)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Maps an AST credential to the inline `Auth` object, or `null` for `mutualTLS`, which the runtime cannot send.
|
|
53
|
+
* `http` schemes other than `basic` are treated as bearer. OAuth2 and OpenID Connect keep their URLs for the `auth` resolver.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveCredential(credential: ast.AuthCredential): Auth | null {
|
|
56
|
+
if (credential.type === 'apiKey') return { type: 'apiKey', name: credential.name, in: credential.in }
|
|
57
|
+
if (credential.type === 'http') return { type: 'http', scheme: credential.scheme === 'basic' ? 'basic' : 'bearer' }
|
|
58
|
+
if (credential.type === 'oauth2') {
|
|
59
|
+
const flows = Object.fromEntries(Object.entries(credential.flows).map(([name, { scopes: _scopes, ...urls }]) => [name, urls]))
|
|
60
|
+
return {
|
|
61
|
+
type: 'oauth2',
|
|
62
|
+
...(Object.keys(flows).length ? { flows } : {}),
|
|
63
|
+
...(credential.metadataUrl ? { metadataUrl: credential.metadataUrl } : {}),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (credential.type === 'openIdConnect') return { type: 'openIdConnect', ...(credential.url ? { url: credential.url } : {}) }
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Derives the per-operation security metadata from `OperationNode.auth` and the document's `meta.auth` schemes. Each
|
|
72
|
+
* requirement becomes one entry: its scheme, or an array of its schemes when it needs several together. A requirement
|
|
73
|
+
* with a scheme the runtime cannot place or that is not declared is left out, since it can never be met, and so is a
|
|
74
|
+
* repeated one. With `hoist`, each scheme carries its name as `ref`.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* `getOperationSecurity({ auth: [[{ scheme: 'bearerAuth' }]], schemes: meta.auth })`
|
|
78
|
+
* `// [{ type: 'http', scheme: 'bearer' }]`
|
|
79
|
+
* @example
|
|
80
|
+
* `// auth: [[{ scheme: 'api_key' }, { scheme: 'session' }]] gives [[{ type: 'apiKey', ... }, { type: 'apiKey', ... }]]`
|
|
81
|
+
*/
|
|
82
|
+
export function getOperationSecurity({
|
|
83
|
+
auth,
|
|
84
|
+
schemes,
|
|
85
|
+
hoist,
|
|
86
|
+
}: {
|
|
87
|
+
auth: Array<ast.AuthRequirement> | undefined
|
|
88
|
+
schemes: Array<ast.AuthScheme> | undefined
|
|
89
|
+
hoist?: boolean
|
|
90
|
+
}): Array<SecurityRequirement> | undefined {
|
|
91
|
+
const security = ast.auth.getRequirements({ auth, schemes }).flatMap((requirement): Array<SecurityRequirement> => {
|
|
92
|
+
const resolved = requirement.map(({ name, credential, scopes }) => {
|
|
93
|
+
const entry = resolveCredential(credential)
|
|
94
|
+
return entry && { ...entry, ...(scopes.length ? { scopes } : {}), ...(hoist ? { ref: name } : {}) }
|
|
95
|
+
})
|
|
96
|
+
if (!resolved.length || !resolved.every((entry): entry is OperationAuth => entry !== null)) return []
|
|
97
|
+
return [resolved.length === 1 ? resolved[0]! : resolved]
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return security.length ? security : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Serializes the per-operation security into the literal emitted on each generated call's `security`
|
|
105
|
+
* field. The runtime `resolveAuth` helper walks it, calling the configured `auth` resolver per scheme.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* `buildSecurityMetadata({ security: [{ type: 'http', scheme: 'bearer' }] }) // "[{ type: 'http', scheme: 'bearer' }]"`
|
|
109
|
+
*/
|
|
110
|
+
export function buildSecurityMetadata({ security }: { security?: Array<SecurityRequirement> }): string | null {
|
|
111
|
+
if (!security?.length) return null
|
|
112
|
+
return `[${security.map(serializeRequirement).join(', ')}]`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The `securitySchemes` const of `.kubb/client.ts`: every scheme the runtime can place, by name, or `null` without one.
|
|
117
|
+
*/
|
|
118
|
+
export function buildSecuritySchemes(schemes: Array<ast.AuthScheme> | undefined): string | null {
|
|
119
|
+
const entries = (schemes ?? []).flatMap(({ name, credential }) => {
|
|
120
|
+
const auth = resolveCredential(credential)
|
|
121
|
+
return auth ? [`${objectKey(name)}: ${serializeScheme(auth)}`] : []
|
|
122
|
+
})
|
|
123
|
+
if (!entries.length) return null
|
|
124
|
+
return `/**\n * The security schemes from the spec by name. A generated call sends one of them to the \`auth\` resolver.\n */\nexport const securitySchemes = { ${entries.join(', ')} } as const`
|
|
125
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ast } from '@kubb/kit'
|
|
2
|
+
import { getRequestGroupOptionality } from '@internals/shared'
|
|
3
|
+
import { createFunctionParameter, createFunctionParameters, functionPrinter } from '@kubb/plugin-ts'
|
|
4
|
+
import type { OperationTypeNames } from '@internals/client'
|
|
5
|
+
import type { ReturnTypeOption } from '../types.ts'
|
|
6
|
+
import { buildResultType } from './generics.ts'
|
|
7
|
+
|
|
8
|
+
const declarationPrinter = functionPrinter({ mode: 'declaration' })
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The pieces of a generated operation function's grouped-options signature.
|
|
12
|
+
*/
|
|
13
|
+
export type GroupedOptionsSignature = {
|
|
14
|
+
/**
|
|
15
|
+
* The single function parameter: `options: Options<<Name>Options, ThrowOnError>`, or `StreamOptions<...>`
|
|
16
|
+
* for a `text/event-stream` operation.
|
|
17
|
+
*/
|
|
18
|
+
paramsSignature: string
|
|
19
|
+
/**
|
|
20
|
+
* The function return type: `Unwrappable<RequestResult<<Name>Responses, ThrowOnError>>`.
|
|
21
|
+
*/
|
|
22
|
+
returnType: string
|
|
23
|
+
/**
|
|
24
|
+
* The function generics. One per-call `ThrowOnError` flag, defaulting to the plugin setting.
|
|
25
|
+
*/
|
|
26
|
+
generics: Array<string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds the grouped-options signature for one operation: a single `options` object whose `TData`
|
|
31
|
+
* is the `<Name>Options` type, and a `RequestResult` return type keyed to the per-status responses
|
|
32
|
+
* record. There are no positional arguments.
|
|
33
|
+
*
|
|
34
|
+
* The generated file imports `<Name>Options` and `<Name>Responses` and uses them directly, so no
|
|
35
|
+
* per-operation input type has to be emitted. Both names come from `types`, which is `plugin-ts` or
|
|
36
|
+
* `plugin-zod`'s inferred types (see `resolveOperationTypes`).
|
|
37
|
+
*/
|
|
38
|
+
export function buildGroupedOptionsSignature({
|
|
39
|
+
node,
|
|
40
|
+
types,
|
|
41
|
+
returnType,
|
|
42
|
+
throwOnErrorDefault,
|
|
43
|
+
eventStream = false,
|
|
44
|
+
}: {
|
|
45
|
+
node: ast.OperationNode
|
|
46
|
+
types: OperationTypeNames
|
|
47
|
+
returnType: ReturnTypeOption
|
|
48
|
+
throwOnErrorDefault: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Adds the per-call stream options to the `options` type of a `text/event-stream` operation, except for a plain custom
|
|
51
|
+
* client, whose own `client.stream` returns a `StreamResult` and types its config itself.
|
|
52
|
+
*/
|
|
53
|
+
eventStream?: boolean
|
|
54
|
+
}): GroupedOptionsSignature {
|
|
55
|
+
const optionsName = types.response.options(node)
|
|
56
|
+
const optionsType = `${eventStream && returnType !== 'plain' ? 'StreamOptions' : 'Options'}<${optionsName}, ThrowOnError>`
|
|
57
|
+
const { isOptional } = getRequestGroupOptionality(node)
|
|
58
|
+
|
|
59
|
+
const paramsSignature =
|
|
60
|
+
declarationPrinter.print(
|
|
61
|
+
createFunctionParameters({
|
|
62
|
+
params: [createFunctionParameter({ name: 'options', type: optionsType, ...(isOptional ? { default: '{}' } : {}) })],
|
|
63
|
+
}),
|
|
64
|
+
) ?? ''
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
paramsSignature,
|
|
68
|
+
returnType: buildResultType({ node, types, returnType }),
|
|
69
|
+
generics: [`ThrowOnError extends boolean = ${throwOnErrorDefault}`],
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isValidVarName } from '@internals/utils'
|
|
2
|
+
import { ast, text } from '@kubb/kit'
|
|
3
|
+
|
|
4
|
+
type StyledLocation = 'path' | 'query' | 'header' | 'cookie'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders a parameter name as an object-literal key, quoted when it is not a bare identifier. Every
|
|
8
|
+
* location keeps the spec name: the runtime looks path styles up by the `{name}` placeholder in the
|
|
9
|
+
* URL, and the other locations by the key it serializes.
|
|
10
|
+
*/
|
|
11
|
+
function toKey(name: string): string {
|
|
12
|
+
return isValidVarName(name) ? name : JSON.stringify(name)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serializes one parameter's metadata into a `{ style, explode, allowReserved, contentType }`
|
|
17
|
+
* literal, or `null` when the parameter carries none. Path and query carry the serialization
|
|
18
|
+
* `style`. Header and cookie have a fixed style (`simple` and `form`), so they emit only `explode`,
|
|
19
|
+
* plus `style: 'cookie'` when a cookie parameter sets the OpenAPI 3.2 `cookie` style.
|
|
20
|
+
* `allowReserved` only applies to query parameters, and `contentType` to every location except path.
|
|
21
|
+
*/
|
|
22
|
+
function serializeParameter(parameter: ast.ParameterNode): string | null {
|
|
23
|
+
const parts = [
|
|
24
|
+
(parameter.in === 'path' || parameter.in === 'query') && parameter.style && `style: '${parameter.style}'`,
|
|
25
|
+
parameter.in === 'cookie' && parameter.style === 'cookie' && `style: 'cookie'`,
|
|
26
|
+
parameter.explode !== undefined && `explode: ${parameter.explode}`,
|
|
27
|
+
parameter.in === 'query' && parameter.allowReserved !== undefined && `allowReserved: ${parameter.allowReserved}`,
|
|
28
|
+
parameter.in !== 'path' && parameter.contentType && `contentType: ${text.quote(parameter.contentType)}`,
|
|
29
|
+
parameter.in === 'querystring' && 'querystring: true',
|
|
30
|
+
].filter(Boolean)
|
|
31
|
+
return parts.length > 0 ? `{ ${parts.join(', ')} }` : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a query parameter of the operation has a style entry, so the call needs the styled query serializer.
|
|
36
|
+
*/
|
|
37
|
+
export function hasQueryStyles(node: ast.OperationNode): boolean {
|
|
38
|
+
return ast.http.isOperation(node) && ast.http.groupParameters(node).query.some((parameter) => serializeParameter(parameter) !== null)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Serializes the `encoding` of the request body's default content type into `property: { ... }`
|
|
43
|
+
* entries, empty when it carries none. Only an operation with a single request content type
|
|
44
|
+
* emits it, because the runtime applies `styles.body` to whichever type the caller selects.
|
|
45
|
+
*/
|
|
46
|
+
function serializeBodyEncoding(node: ast.HttpOperationNode): Array<string> {
|
|
47
|
+
const content = node.requestBody?.content
|
|
48
|
+
return Object.entries(content?.length === 1 ? (content[0]?.encoding ?? {}) : {}).map(([property, { contentType, style, explode, allowReserved }]) => {
|
|
49
|
+
const parts = [
|
|
50
|
+
style && `style: ${JSON.stringify(style)}`,
|
|
51
|
+
explode !== undefined && `explode: ${explode}`,
|
|
52
|
+
allowReserved !== undefined && `allowReserved: ${allowReserved}`,
|
|
53
|
+
contentType && `contentType: ${JSON.stringify(contentType)}`,
|
|
54
|
+
].filter(Boolean)
|
|
55
|
+
return `${isValidVarName(property) ? property : JSON.stringify(property)}: { ${parts.join(', ')} }`
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds the per-operation `styles` literal from the operation's parameters, grouped by location.
|
|
61
|
+
* Entries are keyed by the spec name of the parameter.
|
|
62
|
+
* Only parameters whose source defines `style`, `explode`, `allowReserved`, or `contentType` are
|
|
63
|
+
* emitted, so calls without serialization metadata keep the runtime defaults and existing output
|
|
64
|
+
* is unchanged. A form request body adds a `body` group keyed by property name from its `encoding`.
|
|
65
|
+
* Returns `null` when nothing carries metadata.
|
|
66
|
+
*
|
|
67
|
+
* With `merge`, the literal spreads `config.styles` per parameter, so a per-call entry overrides the generated one for
|
|
68
|
+
* that parameter and leaves the others in place.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* // a path param with { style: 'matrix', explode: true } and a query param with { explode: false }
|
|
73
|
+
* buildStyles({ node }) // "{ path: { id: { style: 'matrix', explode: true } }, query: { tags: { explode: false } } }"
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function buildStyles({ node, merge }: { node: ast.OperationNode; merge?: boolean }): string | null {
|
|
77
|
+
if (!ast.http.isOperation(node)) return null
|
|
78
|
+
|
|
79
|
+
const groups: Record<StyledLocation | 'body', Array<string>> = { path: [], query: [], header: [], cookie: [], body: serializeBodyEncoding(node) }
|
|
80
|
+
|
|
81
|
+
for (const [location, parameters] of Object.entries(ast.http.groupParameters(node)) as Array<[StyledLocation, Array<ast.ParameterNode>]>) {
|
|
82
|
+
for (const parameter of parameters) {
|
|
83
|
+
const literal = serializeParameter(parameter)
|
|
84
|
+
if (literal) groups[location].push(`${toKey(parameter.name)}: ${literal}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const entries = Object.entries(groups)
|
|
89
|
+
.filter(([, items]) => items.length > 0)
|
|
90
|
+
.map(([location, items]) => `${location}: { ${(merge ? [...items, `...config.styles?.${location}`] : items).join(', ')} }`)
|
|
91
|
+
|
|
92
|
+
if (entries.length === 0) return null
|
|
93
|
+
return `{ ${(merge ? ['...config.styles', ...entries] : entries).join(', ')} }`
|
|
94
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ast } from '@kubb/kit'
|
|
2
|
+
import type { ResolverZod } from '@kubb/plugin-zod'
|
|
3
|
+
import type { ValidatorOptions } from '../types.ts'
|
|
4
|
+
import { buildZodErrorParse, resolveParamsValidator, resolveRequestValidator, resolveResponseValidator } from './validatorOptions.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The per-call validator references a generated function wires into its request config. Each hook is
|
|
8
|
+
* the bare schema reference passed to the runtime's `validator.request` / `validator.path` /
|
|
9
|
+
* `validator.query` / `validator.headers` / `validator.cookies` / `validator.response` / `validator.error` slot. `client.ts`
|
|
10
|
+
* runs it through `validateStandardSchema`. The response validator only ever sees success (2xx) bodies.
|
|
11
|
+
*/
|
|
12
|
+
export type ValidatorHooks = {
|
|
13
|
+
/**
|
|
14
|
+
* Schema reference for the `validator.request` hook, or `null` when request validation is off.
|
|
15
|
+
*/
|
|
16
|
+
request: string | null
|
|
17
|
+
/**
|
|
18
|
+
* Schema references for the `validator.path` / `query` / `headers` / `cookies` hooks (`headers` wrapped in
|
|
19
|
+
* `toHeadersStandardSchema`), or `null` when params validation is off or the group is empty.
|
|
20
|
+
*/
|
|
21
|
+
path: string | null
|
|
22
|
+
query: string | null
|
|
23
|
+
headers: string | null
|
|
24
|
+
cookies: string | null
|
|
25
|
+
/**
|
|
26
|
+
* Schema reference for the `validator.response` hook, or `null` when response validation is off.
|
|
27
|
+
*/
|
|
28
|
+
response: string | null
|
|
29
|
+
/** Per-status schema map for `validator.error`, such as `{ 404: getPetStatus404Schema }`, or `null` when off or absent. */
|
|
30
|
+
error: string | null
|
|
31
|
+
/**
|
|
32
|
+
* Zod schema names the generated file imports from the zod plugin output.
|
|
33
|
+
*/
|
|
34
|
+
importedZodNames: Array<string>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the validator-hook references for one operation. Request body and params validation run before the send.
|
|
39
|
+
* Response validation runs on the success body only. Returns `null` references when the matching
|
|
40
|
+
* direction is disabled or the schema is absent.
|
|
41
|
+
*/
|
|
42
|
+
export function buildValidatorHooks({
|
|
43
|
+
node,
|
|
44
|
+
validator,
|
|
45
|
+
zodResolver,
|
|
46
|
+
}: {
|
|
47
|
+
node: ast.OperationNode
|
|
48
|
+
validator: ValidatorOptions | undefined
|
|
49
|
+
zodResolver: ResolverZod | null | undefined
|
|
50
|
+
}): ValidatorHooks {
|
|
51
|
+
const hasRequestBody = Boolean(ast.http.requestBody.getSchema(node))
|
|
52
|
+
const parsesResponse = zodResolver && resolveResponseValidator(validator) === 'zod'
|
|
53
|
+
|
|
54
|
+
// Success responses validate with the success-only `<operation>ResponseSchema`; error bodies with the schema of their status.
|
|
55
|
+
// plugin-zod emits that schema only for an operation with a response body schema.
|
|
56
|
+
const hasResponseSchema = node.responses.some((res) => res.content?.some((entry) => entry.schema))
|
|
57
|
+
const response = parsesResponse && hasResponseSchema ? zodResolver.response.response(node) || null : null
|
|
58
|
+
const errorParse = parsesResponse ? buildZodErrorParse(node, zodResolver) : null
|
|
59
|
+
const request = zodResolver && resolveRequestValidator(validator) === 'zod' && hasRequestBody ? zodResolver.response.body(node) || null : null
|
|
60
|
+
|
|
61
|
+
const paramGroups = ast.http.groupParameters(node)
|
|
62
|
+
const resolveParamsName = (kind: 'path' | 'query' | 'headers' | 'cookies', params: Array<ast.ParameterNode>): string | null =>
|
|
63
|
+
zodResolver && resolveParamsValidator(validator) === 'zod' && params.length > 0 ? zodResolver.param[kind](node, params[0]!) : null
|
|
64
|
+
const path = resolveParamsName('path', paramGroups.path)
|
|
65
|
+
const query = resolveParamsName('query', paramGroups.query)
|
|
66
|
+
const headers = resolveParamsName('headers', paramGroups.header)
|
|
67
|
+
const cookies = resolveParamsName('cookies', paramGroups.cookie)
|
|
68
|
+
|
|
69
|
+
// Order is output: the generated file imports the response schema, then the error schemas, then the request body and params schemas.
|
|
70
|
+
const importedZodNames = [response, ...(errorParse?.names ?? []), request, path, query, headers, cookies].filter((name): name is string => Boolean(name))
|
|
71
|
+
|
|
72
|
+
const headersValidator = headers ? `toHeadersStandardSchema(${headers}, ${headers}.shape)` : null
|
|
73
|
+
|
|
74
|
+
return { request, path, query, headers: headersValidator, cookies, response, error: errorParse?.literal ?? null, importedZodNames }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Prints the `validator: { … }` entry of a call config from the hooks named in `keys`, or `null` when none is set.
|
|
79
|
+
* The entry spreads `config.validator` last, so a per-call hook overrides the generated one for its key only.
|
|
80
|
+
*/
|
|
81
|
+
export function buildValidatorLiteral(hooks: ValidatorHooks | null, keys: ReadonlyArray<keyof Omit<ValidatorHooks, 'importedZodNames'>>): string | null {
|
|
82
|
+
const entries = keys.flatMap((key) => (hooks?.[key] ? [`${key}: ${hooks[key]}`] : []))
|
|
83
|
+
return entries.length ? `validator: { ${entries.join(', ')}, ...config.validator }` : null
|
|
84
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ast, text } from '@kubb/kit'
|
|
2
|
+
import type { ResolverZod } from '@kubb/plugin-zod'
|
|
3
|
+
import type { ValidatorOptions } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns `true` when any direction of the validator uses zod (used for dependency checks).
|
|
7
|
+
*/
|
|
8
|
+
export function isValidatorEnabled(validator: ValidatorOptions | undefined): boolean {
|
|
9
|
+
if (!validator) return false
|
|
10
|
+
if (validator === 'zod') return true
|
|
11
|
+
return Boolean(validator.request || validator.params || validator.response)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns `'zod'` when request body parsing is enabled, `null` otherwise. The string shorthand
|
|
16
|
+
* `'zod'` validates the response only, so it does not enable request parsing.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveRequestValidator(validator: ValidatorOptions | undefined): 'zod' | null {
|
|
19
|
+
if (!validator || validator === 'zod') return null
|
|
20
|
+
return validator.request ?? null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns `'zod'` when path, query, and header params parsing is enabled, `null` otherwise. Only the
|
|
25
|
+
* object form `{ params: 'zod' }` enables it.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveParamsValidator(validator: ValidatorOptions | undefined): 'zod' | null {
|
|
28
|
+
if (!validator || validator === 'zod') return null
|
|
29
|
+
return validator.params ?? null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns `'zod'` when response parsing is enabled, `null` otherwise. The string shorthand `'zod'`
|
|
34
|
+
* maps to response parsing.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveResponseValidator(validator: ValidatorOptions | undefined): 'zod' | null {
|
|
37
|
+
if (!validator) return null
|
|
38
|
+
if (validator === 'zod') return 'zod'
|
|
39
|
+
return validator.response ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds the `validator.error` map of a call, one plugin-zod `<operation>StatusNNNSchema` per non-2xx response with a
|
|
44
|
+
* schema, such as `{ 404: getPetStatus404Schema, '5xx': getPetStatus5xxSchema }`. Returns `null` without one.
|
|
45
|
+
*/
|
|
46
|
+
export function buildZodErrorParse(node: ast.OperationNode, zodResolver: ResolverZod): { literal: string; names: Array<string> } | null {
|
|
47
|
+
const entries = node.responses
|
|
48
|
+
.filter((res) => !ast.http.status.isSuccess(res) && res.content?.some((entry) => entry.schema))
|
|
49
|
+
.map((res) => ({ key: String(res.statusCode), name: zodResolver.response.status(node, res.statusCode) }))
|
|
50
|
+
if (!entries.length) return null
|
|
51
|
+
const literal = entries.map(({ key, name }) => `${/^\d+$/.test(key) ? key : text.quote(key)}: ${name}`).join(', ')
|
|
52
|
+
return { literal: `{ ${literal} }`, names: entries.map(({ name }) => name) }
|
|
53
|
+
}
|