@kubb/plugin-axios 5.0.0-beta.100

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.
@@ -0,0 +1,724 @@
1
+ import axios from 'axios'
2
+ import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
3
+ import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers'
4
+ import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
5
+ import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
6
+
7
+ /**
8
+ * HTTP status codes treated as a success, everything else is an error.
9
+ */
10
+ export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
11
+
12
+ /**
13
+ * The success members of a per-status responses record.
14
+ */
15
+ export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
16
+
17
+ /**
18
+ * The error members of a per-status responses record, every documented status that is not a 2xx.
19
+ */
20
+ export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
21
+
22
+ /**
23
+ * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
24
+ */
25
+ export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
26
+
27
+ /**
28
+ * The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`.
29
+ */
30
+ export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
31
+
32
+ /**
33
+ * The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it.
34
+ */
35
+ export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
36
+ ? TEntry extends { contentType: infer TContentType; data: infer TData }
37
+ ? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
38
+ : never
39
+ : { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
40
+
41
+ /**
42
+ * One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`.
43
+ */
44
+ export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
45
+ ? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
46
+ : {
47
+ status: ToStatusNumber<TStatus>
48
+ data: undefined
49
+ error: DataOf<TResponses[TStatus]>
50
+ contentType: string | undefined
51
+ request: TRequest
52
+ response: TResponse
53
+ }
54
+
55
+ /**
56
+ * The union of every documented status' result variant.
57
+ */
58
+ export type ResultUnion<TResponses, TRequest, TResponse> = {
59
+ [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
60
+ }[keyof TResponses]
61
+
62
+ /**
63
+ * The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`.
64
+ */
65
+ export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
66
+ [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
67
+ }[Extract<keyof TResponses, SuccessStatusCode>]
68
+
69
+ /**
70
+ * The shape every generated function returns, discriminated by the top-level `status`, narrowing to the 2xx variants under `throwOnError` and to every documented status without it.
71
+ */
72
+ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = ThrowOnError extends true
73
+ ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
74
+ ? {
75
+ status: number
76
+ data: SuccessOf<TResponses>
77
+ error: undefined
78
+ contentType: string | undefined
79
+ request: TRequest
80
+ response: TResponse
81
+ }
82
+ : SuccessResultUnion<TResponses, TRequest, TResponse>
83
+ : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
84
+ ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
85
+ : ResultUnion<TResponses, TRequest, TResponse>
86
+
87
+ /**
88
+ * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
89
+ */
90
+ export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
91
+
92
+ export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
93
+
94
+ /**
95
+ * Turns a raw response body into a parsed value, registered per media type as a codec's `deserialize` to handle formats the runtime does not decode itself.
96
+ */
97
+ export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
98
+
99
+ /**
100
+ * Serializes a request body for a single media type, registered per content type as a codec's `serialize` to encode formats the default serializer does not handle.
101
+ */
102
+ export type ContentBodySerializer = (body: unknown, contentType?: string) => unknown
103
+
104
+ /**
105
+ * A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the
106
+ * request body for that media type and `deserialize` decodes the response body. Either half is
107
+ * optional, so a codec can handle one direction.
108
+ */
109
+ export type Codec = {
110
+ serialize?: ContentBodySerializer
111
+ deserialize?: Deserializer
112
+ }
113
+
114
+ /**
115
+ * The per-call content type selection, where a bare string sets the request content type and the object form also sets the response format sent as `Accept`.
116
+ */
117
+ export type ContentType = string | { request?: string; response?: string }
118
+
119
+ /**
120
+ * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
121
+ * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
122
+ * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
123
+ * non-2xx call does not throw).
124
+ */
125
+ export type Validator<T = unknown> = StandardSchemaValidator<T>
126
+
127
+ /**
128
+ * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
129
+ */
130
+ export type Auth = {
131
+ type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
132
+ scheme?: 'bearer' | 'basic'
133
+ name?: string
134
+ in?: 'header' | 'query' | 'cookie'
135
+ }
136
+
137
+ /**
138
+ * The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
139
+ */
140
+ export type AuthToken = string | undefined
141
+
142
+ /**
143
+ * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
144
+ */
145
+ export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
146
+
147
+ /**
148
+ * Extra axios config the runtime spreads onto every request, an escape hatch for per-call fields it does not set itself such as `timeout`, `proxy`, and the progress callbacks.
149
+ */
150
+ export type AxiosOptions = AxiosRequestConfig
151
+
152
+ /**
153
+ * The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
154
+ */
155
+ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
156
+ baseURL?: string
157
+ url?: string
158
+ method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
159
+ path?: Record<string, unknown>
160
+ query?: unknown
161
+ params?: unknown
162
+ cookies?: Record<string, unknown>
163
+ body?: TBody
164
+ headers?: HeadersInit
165
+ styles?: Styles
166
+ signal?: AbortSignal
167
+ options?: AxiosOptions
168
+ contentType?: ContentType
169
+ responseType?: ResponseType
170
+ throwOnError?: boolean
171
+ validateStatus?: (status: number) => boolean
172
+ client?: ClientInstance<TRequest, TResponse>
173
+ transport?: AxiosInstance
174
+ serializer?: Serializers
175
+ codecs?: Record<string, Codec>
176
+ validator?: { request?: Validator; response?: Validator; error?: Validator }
177
+ security?: Array<Auth>
178
+ auth?: AuthResolver
179
+ }
180
+
181
+ /**
182
+ * The grouped options object passed to every generated function: the request config minus the
183
+ * data-shaped keys and the literal `url`, plus the per-operation `<Name>Request`.
184
+ */
185
+ export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = Omit<
186
+ RequestConfig<unknown, TRequest, TResponse>,
187
+ keyof DataShape | 'url'
188
+ > &
189
+ TData & {
190
+ client?: ClientInstance<TRequest, TResponse>
191
+ throwOnError?: ThrowOnError
192
+ }
193
+
194
+ /**
195
+ * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
196
+ */
197
+ export type ClientConfig = {
198
+ baseURL?: string
199
+ headers?: HeadersInit
200
+ options?: AxiosOptions
201
+ throwOnError?: boolean
202
+ validateStatus?: (status: number) => boolean
203
+ transport?: AxiosInstance
204
+ serializer?: Serializers
205
+ codecs?: Record<string, Codec>
206
+ auth?: AuthResolver
207
+ }
208
+
209
+ /**
210
+ * The result a resolved call produces before it is cast to `RequestResult` by the generated wrapper.
211
+ */
212
+ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
213
+ status: number
214
+ data: unknown
215
+ error: unknown
216
+ contentType: string | undefined
217
+ request: TRequest
218
+ response: TResponse
219
+ }
220
+
221
+ export type InterceptorFn<T> = (value: T) => T | Promise<T>
222
+
223
+ /**
224
+ * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API, backed by axios's native interceptor managers.
225
+ */
226
+ export type InterceptorChannel<T> = {
227
+ use: (fn: InterceptorFn<T>) => number
228
+ eject: (id: number) => void
229
+ update: (id: number, fn: InterceptorFn<T>) => void
230
+ }
231
+
232
+ /**
233
+ * The three interceptor channels every client instance exposes, wrapping axios's native managers with `error` mapped onto the response rejection handler.
234
+ */
235
+ export type Interceptors = {
236
+ request: InterceptorChannel<InternalAxiosRequestConfig>
237
+ response: InterceptorChannel<AxiosResponse>
238
+ error: InterceptorChannel<AxiosError>
239
+ }
240
+
241
+ /**
242
+ * A client instance: the callable send plus configuration, interceptors, and an isolated
243
+ * `createClient` factory.
244
+ */
245
+ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
246
+ <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
247
+ getConfig: () => ClientConfig
248
+ setConfig: (config: ClientConfig) => ClientConfig
249
+ getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string
250
+ interceptors: Interceptors
251
+ createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>
252
+ }
253
+
254
+ /**
255
+ * Thrown for a non-2xx response, so a resolved call always means success.
256
+ */
257
+ export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
258
+ data: TError
259
+ status: number
260
+ statusText: string
261
+ contentType: string | undefined
262
+ request: TRequest
263
+ response: TResponse
264
+
265
+ constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) {
266
+ super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
267
+ this.name = 'ResponseError'
268
+ this.data = config.data
269
+ this.status = config.status
270
+ this.statusText = config.statusText
271
+ this.contentType = config.contentType
272
+ this.request = config.request
273
+ this.response = config.response
274
+ }
275
+ }
276
+
277
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
278
+
279
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
280
+ if (!headers) return {}
281
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
282
+ const result: Record<string, string> = {}
283
+ for (const [key, value] of entries) {
284
+ if (value === undefined || value === null) continue
285
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
286
+ }
287
+ return result
288
+ }
289
+
290
+ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string, string> {
291
+ return Object.assign({}, ...sources.map(serializeHeaders))
292
+ }
293
+
294
+ function getHeader(headers: Record<string, string>, name: string): string | undefined {
295
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
296
+ return key ? headers[key] : undefined
297
+ }
298
+
299
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
300
+ return Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase())
301
+ }
302
+
303
+ /**
304
+ * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`.
305
+ */
306
+ function serializeUrl({
307
+ parts,
308
+ pathParams,
309
+ search,
310
+ pathSerializer = defaultPathSerializer,
311
+ pathStyles,
312
+ }: {
313
+ parts: Array<string | undefined>
314
+ pathParams: Record<string, unknown>
315
+ search: string
316
+ pathSerializer?: PathSerializer
317
+ pathStyles?: Record<string, PathParamStyle>
318
+ }): string {
319
+ const path = parts
320
+ .filter(Boolean)
321
+ .join('')
322
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
323
+ return path + (search ? `?${search}` : '')
324
+ }
325
+
326
+ /**
327
+ * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
328
+ */
329
+ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
330
+ const ids = new Map<number, number>()
331
+ let counter = 0
332
+ return {
333
+ use(fn) {
334
+ const id = ++counter
335
+ ids.set(id, register(fn))
336
+ return id
337
+ },
338
+ eject(id) {
339
+ const nativeId = ids.get(id)
340
+ if (nativeId === undefined) return
341
+ ejectNative(nativeId)
342
+ ids.delete(id)
343
+ },
344
+ update(id, fn) {
345
+ const nativeId = ids.get(id)
346
+ if (nativeId !== undefined) ejectNative(nativeId)
347
+ ids.set(id, register(fn))
348
+ },
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
354
+ */
355
+ export async function resolveAuth(params: {
356
+ security: Array<Auth> | undefined
357
+ auth: AuthResolver | undefined
358
+ headers: Record<string, string>
359
+ query: Record<string, unknown>
360
+ }): Promise<void> {
361
+ const { security, auth, headers, query } = params
362
+ if (!security?.length || auth === undefined) return
363
+
364
+ for (const scheme of security) {
365
+ const token = typeof auth === 'function' ? await auth(scheme) : auth
366
+ if (token === undefined) continue
367
+
368
+ if (scheme.type === 'apiKey') {
369
+ const name = scheme.name ?? 'Authorization'
370
+ if (scheme.in === 'query') {
371
+ if (query[name] === undefined) query[name] = token
372
+ } else if (scheme.in === 'cookie') {
373
+ headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
374
+ } else if (!hasHeader(headers, name)) {
375
+ headers[name] = token
376
+ }
377
+ } else if (!hasHeader(headers, 'Authorization')) {
378
+ headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
379
+ }
380
+ return
381
+ }
382
+ }
383
+
384
+ async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
385
+ if (!validator) return value
386
+ return validateStandardSchema(validator, value)
387
+ }
388
+
389
+ /**
390
+ * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
391
+ */
392
+ function baseContentType(value: string | null | undefined): string | undefined {
393
+ if (!value) return undefined
394
+ return value.split(';')[0]!.trim().toLowerCase() || undefined
395
+ }
396
+
397
+ /**
398
+ * Reads the negotiated response content type from the response headers as a base media type.
399
+ */
400
+ function getResponseContentType(headers: Record<string, unknown> | undefined): string | undefined {
401
+ if (!headers) return undefined
402
+ const value = headers['content-type'] ?? headers['Content-Type']
403
+ return baseContentType(typeof value === 'string' ? value : undefined)
404
+ }
405
+
406
+ /**
407
+ * Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type.
408
+ */
409
+ function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } {
410
+ if (typeof contentType === 'string') return { request: contentType }
411
+ return contentType ?? {}
412
+ }
413
+
414
+ /**
415
+ * The per-concern serializers for a call, the per-call serializer winning over the client's and
416
+ * falling back to the defaults.
417
+ */
418
+ function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
419
+ return {
420
+ querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
421
+ bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
422
+ pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Resolves everything a call needs before it touches axios: merged headers with the negotiated
428
+ * content type, auth on headers or query, serialized cookies, the validated and serialized body,
429
+ * and the final axios request config with `throwOnError` riding `validateStatus`.
430
+ */
431
+ async function resolveRequest<TBody, TRequest, TResponse>({
432
+ config,
433
+ requestConfig,
434
+ }: {
435
+ config: ClientConfig
436
+ requestConfig: RequestConfig<TBody, TRequest, TResponse>
437
+ }): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record<string, Codec>; throwOnError: boolean }> {
438
+ const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
439
+ const codecs = { ...config.codecs, ...requestConfig.codecs }
440
+
441
+ const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
442
+ const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
443
+ const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
444
+ if (responseContentType && !hasHeader(headers, 'accept')) {
445
+ headers['Accept'] = responseContentType
446
+ }
447
+
448
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
449
+
450
+ await resolveAuth({
451
+ security: requestConfig.security,
452
+ auth: requestConfig.auth ?? config.auth,
453
+ headers,
454
+ query,
455
+ })
456
+
457
+ if (requestConfig.cookies) {
458
+ const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
459
+ if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
460
+ }
461
+
462
+ const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
463
+ const requestContentTypeBase = baseContentType(requestContentType)
464
+ const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
465
+ const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
466
+ const body = contentCodec?.serialize
467
+ ? contentCodec.serialize(validatedBody, requestContentType)
468
+ : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
469
+ // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
470
+ if (body instanceof FormData) {
471
+ for (const key of Object.keys(headers)) {
472
+ if (key.toLowerCase() === 'content-type') delete headers[key]
473
+ }
474
+ } else if (requestContentTypeOption) {
475
+ headers['Content-Type'] = requestContentTypeOption
476
+ } else if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && !hasHeader(headers, 'content-type')) {
477
+ headers['Content-Type'] = 'application/json'
478
+ }
479
+
480
+ const pathParams = requestConfig.path ?? {}
481
+ const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
482
+ pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
483
+ )
484
+
485
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
486
+ const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
487
+
488
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
489
+
490
+ const axiosConfig: AxiosRequestConfig = {
491
+ ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
492
+ url,
493
+ baseURL: requestConfig.baseURL ?? config.baseURL,
494
+ method: requestConfig.method ?? 'GET',
495
+ headers,
496
+ params: query,
497
+ paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
498
+ data: body,
499
+ transformRequest: (data) => data,
500
+ signal: requestConfig.signal,
501
+ responseType: requestConfig.responseType,
502
+ validateStatus,
503
+ }
504
+
505
+ // Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser.
506
+ // The default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
507
+ if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
508
+ axiosConfig.adapter = 'fetch'
509
+ }
510
+
511
+ return { axiosConfig, codecs, throwOnError }
512
+ }
513
+
514
+ /**
515
+ * Turns an axios response into the call result: decodes the body through the matching codec and
516
+ * validates the success or error body. A thrown axios error never reaches this, so `error` here is
517
+ * always the body of a non-2xx response that `validateStatus` let through.
518
+ */
519
+ async function settleResponse<TRequest, TResponse>({
520
+ response,
521
+ codecs,
522
+ validator,
523
+ }: {
524
+ response: AxiosResponse
525
+ codecs: Record<string, Codec>
526
+ validator: { response?: Validator; error?: Validator } | undefined
527
+ }): Promise<CallResult<TRequest, TResponse>> {
528
+ const isSuccess = response.status >= 200 && response.status < 300
529
+ const contentType = getResponseContentType(response.headers as Record<string, unknown>)
530
+ let decoded: unknown = response.data
531
+ if (contentType) {
532
+ const codec = codecs[contentType]
533
+ if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
534
+ }
535
+ const data = isSuccess ? await runValidator(validator?.response, decoded) : undefined
536
+ const error = isSuccess ? undefined : await runValidator(validator?.error, decoded)
537
+ return {
538
+ status: response.status,
539
+ data,
540
+ error,
541
+ contentType,
542
+ request: response.config as TRequest,
543
+ response: response as TResponse,
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`.
549
+ */
550
+ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
551
+ let config: ClientConfig = { ...options }
552
+ const instance = config.transport ?? axios.create()
553
+
554
+ const requestManager = instance.interceptors.request
555
+ const responseManager = instance.interceptors.response
556
+ const interceptors: Interceptors = {
557
+ request: createInterceptorChannel<InternalAxiosRequestConfig>(
558
+ (fn) => requestManager.use(fn),
559
+ (id) => requestManager.eject(id),
560
+ ),
561
+ response: createInterceptorChannel<AxiosResponse>(
562
+ (fn) => responseManager.use(fn),
563
+ (id) => responseManager.eject(id),
564
+ ),
565
+ error: createInterceptorChannel<AxiosError>(
566
+ (fn) =>
567
+ responseManager.use(undefined, async (error: unknown) => {
568
+ await fn(error as AxiosError)
569
+ return Promise.reject(error)
570
+ }),
571
+ (id) => responseManager.eject(id),
572
+ ),
573
+ }
574
+
575
+ const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
576
+ const activeInstance = requestConfig.transport ?? config.transport ?? instance
577
+ const { axiosConfig, codecs, throwOnError } = await resolveRequest({ config, requestConfig })
578
+
579
+ try {
580
+ const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
581
+ return await settleResponse({ response, codecs, validator: requestConfig.validator })
582
+ } catch (error) {
583
+ const axiosError = error as AxiosError
584
+ if (throwOnError && axiosError.response) {
585
+ throw new ResponseError({
586
+ data: axiosError.response.data,
587
+ status: axiosError.response.status,
588
+ statusText: axiosError.response.statusText,
589
+ contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
590
+ request: axiosError.config as TRequest,
591
+ response: axiosError.response as TResponse,
592
+ })
593
+ }
594
+ throw error
595
+ }
596
+ }) as ClientInstance<TRequest, TResponse>
597
+
598
+ client.getConfig = () => config
599
+ client.setConfig = (next) => {
600
+ config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
601
+ return config
602
+ }
603
+ client.getUrl = (requestConfig) => {
604
+ const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
605
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
606
+ return serializeUrl({
607
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
608
+ pathParams: requestConfig.path ?? {},
609
+ search: querySerializer(query, requestConfig.styles?.query),
610
+ pathSerializer,
611
+ pathStyles: requestConfig.styles?.path,
612
+ })
613
+ }
614
+ client.interceptors = interceptors
615
+ client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })
616
+
617
+ return client
618
+ }
619
+
620
+ /**
621
+ * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
622
+ */
623
+ export type ServerSentEvent<TData = unknown> = {
624
+ data: TData
625
+ event?: string
626
+ id?: string
627
+ retry?: number
628
+ }
629
+
630
+ async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
631
+ if (!('getReader' in stream)) {
632
+ yield* stream
633
+ return
634
+ }
635
+
636
+ const reader = stream.getReader()
637
+ try {
638
+ while (true) {
639
+ const { done, value } = await reader.read()
640
+ if (done) return
641
+ yield value
642
+ }
643
+ } finally {
644
+ await reader.cancel().catch(() => {})
645
+ }
646
+ }
647
+
648
+ function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
649
+ const data: Array<string> = []
650
+ const event: ServerSentEvent<TData> = { data: undefined as TData }
651
+ let seen = false
652
+
653
+ for (const line of raw.split('\n')) {
654
+ if (!line || line.startsWith(':')) continue
655
+ seen = true
656
+ const index = line.indexOf(':')
657
+ const field = index === -1 ? line : line.slice(0, index)
658
+ const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
659
+ if (field === 'data') data.push(value)
660
+ else if (field === 'event') event.event = value
661
+ else if (field === 'id') event.id = value
662
+ else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
663
+ }
664
+
665
+ if (!seen) return undefined
666
+
667
+ if (data.length) {
668
+ const joined = data.join('\n')
669
+ try {
670
+ event.data = JSON.parse(joined) as TData
671
+ } catch {
672
+ event.data = joined as TData
673
+ }
674
+ }
675
+ return event
676
+ }
677
+
678
+ /**
679
+ * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
680
+ */
681
+ export async function* parseEventStream<TData = unknown>(
682
+ stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
683
+ ): AsyncGenerator<ServerSentEvent<TData>> {
684
+ const decoder = new TextDecoder()
685
+ const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
686
+ let buffer = ''
687
+
688
+ for await (const chunk of readBytes(stream)) {
689
+ const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
690
+ buffer = blocks.pop() ?? ''
691
+ for (const block of blocks) {
692
+ const event = parseEvent<TData>(block)
693
+ if (event) yield event
694
+ }
695
+ }
696
+
697
+ const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
698
+ if (event) yield event
699
+ }
700
+
701
+ /**
702
+ * The resolved shape returned by a generated `text/event-stream` operation: the typed event
703
+ * `stream` plus the native `response`.
704
+ */
705
+ export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
706
+ stream: AsyncGenerator<ServerSentEvent<TData>>
707
+ response: TResponse
708
+ }
709
+
710
+ /**
711
+ * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
712
+ * the parsed events as a typed async iterator. Generated SSE operations call this.
713
+ */
714
+ export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
715
+ const { data, response } = await result
716
+ return {
717
+ response,
718
+ stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
719
+ }
720
+ }
721
+
722
+ export const client = createClientCore()
723
+
724
+ export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)