@kubb/plugin-client 5.12.0 → 6.0.1

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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/client-C-0-puI2.d.ts +554 -0
  3. package/dist/index.cjs +1377 -784
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +117 -78
  6. package/dist/index.d.ts +117 -78
  7. package/dist/index.js +1375 -786
  8. package/dist/index.js.map +1 -1
  9. package/dist/templates/client.d.ts +3 -2
  10. package/dist/templates/core.d.ts +3 -3
  11. package/dist/templates/mediaTypes/formData.d.ts +5 -0
  12. package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
  13. package/dist/templates/mediaTypes.d.ts +2 -0
  14. package/dist/templates/serializers/cookies.d.ts +5 -0
  15. package/dist/templates/serializers/headers.d.ts +5 -0
  16. package/dist/templates/serializers/path.d.ts +5 -0
  17. package/dist/templates/serializers/pathStyles.d.ts +5 -0
  18. package/dist/templates/serializers/query.d.ts +5 -0
  19. package/dist/templates/serializers/queryStyles.d.ts +14 -0
  20. package/dist/templates/serializers.d.ts +118 -2
  21. package/dist/templates/sse.d.ts +20 -113
  22. package/dist/templates/standardSchema.d.ts +81 -2
  23. package/docs/guides/authentication.md +12 -2
  24. package/docs/guides/calling-operations.md +8 -6
  25. package/docs/guides/migrate-to-v6.md +139 -0
  26. package/docs/guides/validate-requests-and-responses.md +13 -8
  27. package/docs/guides/write-your-client.md +118 -12
  28. package/docs/options.md +5 -3
  29. package/docs/overview.md +3 -1
  30. package/kubb-extension.json +21 -3
  31. package/package.json +6 -5
  32. package/src/builders/callConfig.ts +80 -0
  33. package/src/builders/contentType.ts +35 -0
  34. package/src/builders/generics.ts +29 -0
  35. package/src/builders/operationBody.ts +43 -0
  36. package/src/builders/operationData.ts +64 -0
  37. package/src/builders/returnStatement.ts +103 -0
  38. package/src/builders/security.ts +125 -0
  39. package/src/builders/signature.ts +71 -0
  40. package/src/builders/styles.ts +94 -0
  41. package/src/builders/validator.ts +84 -0
  42. package/src/builders/validatorOptions.ts +53 -0
  43. package/src/clientTypes.ts +195 -0
  44. package/src/components/ClientRuntime.tsx +97 -0
  45. package/src/components/Operation.tsx +72 -0
  46. package/src/components/SdkClient.tsx +100 -0
  47. package/src/components/SdkFacade.tsx +35 -0
  48. package/src/generators/clientGenerator.tsx +91 -6
  49. package/src/generators/sdkGenerator.tsx +208 -0
  50. package/src/index.ts +3 -1
  51. package/src/macros.ts +9 -0
  52. package/src/plugin.ts +7 -8
  53. package/src/resolver.ts +28 -0
  54. package/src/setup.ts +90 -0
  55. package/src/types.ts +4 -2
  56. package/templates/client.ts +2 -1
  57. package/templates/core.ts +735 -286
  58. package/templates/mediaTypes/formData.ts +59 -0
  59. package/templates/mediaTypes/urlencoded.ts +33 -0
  60. package/templates/mediaTypes.ts +147 -0
  61. package/templates/serializers/cookies.ts +58 -0
  62. package/templates/serializers/headers.ts +35 -0
  63. package/templates/serializers/path.ts +19 -0
  64. package/templates/serializers/pathStyles.ts +27 -0
  65. package/templates/serializers/query.ts +21 -0
  66. package/templates/serializers/queryStyles.ts +107 -0
  67. package/templates/serializers.ts +48 -364
  68. package/templates/sse.ts +87 -178
  69. package/templates/standardSchema.ts +16 -37
  70. package/dist/client-BxO6Dygu.d.ts +0 -355
  71. package/dist/serializers-Ds8zZwOO.d.ts +0 -190
  72. package/dist/standardSchema-BShJWz2i.d.ts +0 -104
@@ -0,0 +1,29 @@
1
+ import type { ast } from '@kubb/kit'
2
+ import { buildResponseHeadersType, buildResultTypeRef, type OperationTypeNames } from '@internals/client'
3
+ import type { ReturnTypeOption } from '../types.ts'
4
+
5
+ /**
6
+ * Builds the `RequestResult` type arguments of one operation: responses, `ThrowOnError` and the response headers, when a response declares any.
7
+ *
8
+ * @example
9
+ * `buildRequestResultGenerics({ node, types }) // "LoginUserResponses, ThrowOnError, { '200': LoginUserStatus200Headers }"`
10
+ */
11
+ export function buildRequestResultGenerics({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): string {
12
+ return [types.response.responses(node), 'ThrowOnError', buildResponseHeadersType({ node, types })].filter(Boolean).join(', ')
13
+ }
14
+
15
+ /**
16
+ * Builds the full return type an operation's function signature uses: `Unwrappable<RequestResult>`
17
+ * for the default `returnType: 'full'`, already a promise so no further wrapping is needed, a plain
18
+ * `Promise<RequestResult>` for the internal `'plain'` shape, or a
19
+ * `Promise` of the runtime's `UnwrappedResult` when `returnType: 'data'` narrows a resolved call
20
+ * down to the bare success body.
21
+ *
22
+ * @example
23
+ * `buildResultType({ node, types, returnType: 'data' }) // 'Promise<UnwrappedResult<AddPetResponses, ThrowOnError>>'`
24
+ */
25
+ export function buildResultType({ node, types, returnType }: { node: ast.OperationNode; types: OperationTypeNames; returnType: ReturnTypeOption }): string {
26
+ const name = returnType === 'data' ? 'UnwrappedResult' : 'RequestResult'
27
+ const result = buildResultTypeRef(name, types.response.responses(node), buildResponseHeadersType({ node, types }), ['ThrowOnError'])
28
+ return returnType === 'full' ? `Unwrappable<${result}>` : `Promise<${result}>`
29
+ }
@@ -0,0 +1,43 @@
1
+ import type { ast } from '@kubb/kit'
2
+ import type { OperationTypeNames } from '@internals/client'
3
+ import type { ReturnTypeOption } from '../types.ts'
4
+ import { buildCallConfig } from './callConfig.ts'
5
+ import { buildEventStreamReturn, buildReturnStatement } from './returnStatement.ts'
6
+ import type { SecurityRequirement } from './security.ts'
7
+ import { buildGroupedOptionsSignature } from './signature.ts'
8
+ import type { ValidatorHooks } from './validator.ts'
9
+
10
+ /**
11
+ * The pieces of a generated operation: its generics, `options` parameter, return type, return statement, and whether the
12
+ * body destructures `contentType` from `options`. Shared by the standalone functions and the SDK methods, so a
13
+ * streamed operation is wired to `apiClient.stream` in one place.
14
+ */
15
+ export function buildOperationBody({
16
+ node,
17
+ types,
18
+ validatorHooks,
19
+ security,
20
+ baseURL,
21
+ returnType,
22
+ throwOnErrorDefault,
23
+ }: {
24
+ node: ast.HttpOperationNode
25
+ types: OperationTypeNames
26
+ validatorHooks: ValidatorHooks | null
27
+ security?: Array<SecurityRequirement>
28
+ baseURL?: string
29
+ returnType: ReturnTypeOption
30
+ throwOnErrorDefault: boolean
31
+ }): { generics: Array<string>; paramsSignature: string; returnType: string; statement: string; mergeContentType: boolean } {
32
+ const { callConfig, mergeContentType, eventStream } = buildCallConfig({ node, validatorHooks, security, baseURL, throwOnErrorDefault, returnType })
33
+ const signature = buildGroupedOptionsSignature({ node, types, returnType, throwOnErrorDefault, eventStream })
34
+ const stream = eventStream ? buildEventStreamReturn({ node, types, callConfig, returnType, throwOnErrorDefault }) : null
35
+
36
+ return {
37
+ generics: signature.generics,
38
+ paramsSignature: signature.paramsSignature,
39
+ returnType: stream?.returnType ?? signature.returnType,
40
+ statement: stream?.statement ?? buildReturnStatement({ node, types, callConfig, returnType, throwOnErrorDefault }),
41
+ mergeContentType,
42
+ }
43
+ }
@@ -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
+ }