@kubb/plugin-fetch 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,428 @@
1
+ export type HeaderValue = string | number | boolean | null | undefined | object
2
+ export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
3
+
4
+ /**
5
+ * The OpenAPI query-parameter serialization style. `form` is the default; `spaceDelimited` and
6
+ * `pipeDelimited` join arrays with a space or pipe, and `deepObject` renders objects as
7
+ * `key[prop]=value`.
8
+ */
9
+ export type QueryStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject'
10
+
11
+ /**
12
+ * The serialization metadata shared by the styled parameter locations: the OpenAPI `style` (typed per
13
+ * location through `TStyle`), `explode`, and `allowReserved` (keeps RFC 3986 reserved characters
14
+ * unencoded, used by query and request bodies).
15
+ */
16
+ export type SerializationStyle<TStyle = string> = {
17
+ style?: TStyle
18
+ explode?: boolean
19
+ allowReserved?: boolean
20
+ }
21
+
22
+ /**
23
+ * The per-parameter query serialization metadata carried by the generated request.
24
+ */
25
+ export type QueryParamStyle = SerializationStyle<QueryStyle>
26
+
27
+ /**
28
+ * Serializes the query object into a search string. The optional second argument carries the
29
+ * per-parameter OpenAPI `style` / `explode` / `allowReserved` metadata; without it arrays explode
30
+ * into repeated keys and nested objects use the `deepObject` style.
31
+ */
32
+ export type QuerySerializer = (params: Record<string, unknown>, options?: Record<string, QueryParamStyle>) => string
33
+
34
+ /**
35
+ * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the
36
+ * OpenAPI `form` style, so only `explode` is configurable.
37
+ */
38
+ export type CookieParamStyle = {
39
+ explode?: boolean
40
+ }
41
+
42
+ /**
43
+ * The per-parameter header serialization metadata carried by the generated request. Headers use the
44
+ * OpenAPI `simple` style, so only `explode` is configurable.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * // styles.header: { 'X-Ids': { explode: false } }, header [3, 4] -> 'X-Ids: 3,4'
49
+ * // styles.header: { 'X-Filter': { explode: true } }, header { role: 'admin' } -> 'X-Filter: role=admin'
50
+ * ```
51
+ */
52
+ export type HeaderParamStyle = {
53
+ explode?: boolean
54
+ }
55
+
56
+ /**
57
+ * The per-property `encoding` metadata for an `application/x-www-form-urlencoded` or
58
+ * `multipart/form-data` request body. `contentType` overrides the part's media type; `style` /
59
+ * `explode` / `allowReserved` follow the OpenAPI query rules for urlencoded bodies.
60
+ */
61
+ export type BodyEncoding = SerializationStyle<QueryStyle> & {
62
+ contentType?: string
63
+ }
64
+
65
+ /**
66
+ * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`,
67
+ * `ArrayBuffer`, and string bodies pass through untouched. The optional `encoding` argument carries
68
+ * the per-property OpenAPI `encoding` metadata for form bodies.
69
+ */
70
+ export type BodySerializer = (args: { body: unknown; contentType?: string; encoding?: Record<string, BodyEncoding> }) => BodyInit | undefined
71
+
72
+ /**
73
+ * The OpenAPI path-parameter serialization style. `simple` is the default and emits the bare value;
74
+ * `label` prefixes a `.` and `matrix` prefixes a `;name=` segment.
75
+ */
76
+ export type PathStyle = 'simple' | 'label' | 'matrix'
77
+
78
+ /**
79
+ * The per-parameter serialization metadata carried by the generated request. `style` selects the
80
+ * OpenAPI style and `explode` controls how arrays and objects expand.
81
+ */
82
+ export type PathParamStyle = SerializationStyle<PathStyle>
83
+
84
+ /**
85
+ * Serializes a single path parameter for interpolation into the URL, honoring the OpenAPI `style` /
86
+ * `explode` passed as `options`. Defaults to `simple` style with `explode: false`: primitives are
87
+ * URL-encoded, arrays join their members with commas, and objects flatten to `key,value` pairs.
88
+ */
89
+ export type PathSerializer = (args: { name: string; value: unknown; options?: PathParamStyle }) => string
90
+
91
+ /**
92
+ * The per-concern serializers, grouped so they can be set in one place and overridden per client or
93
+ * per call. Each field falls back to the matching `default*Serializer` when omitted.
94
+ */
95
+ export type Serializers = {
96
+ query?: QuerySerializer
97
+ body?: BodySerializer
98
+ path?: PathSerializer
99
+ }
100
+
101
+ /**
102
+ * The per-parameter OpenAPI `style` / `explode` metadata a generated request carries, grouped by
103
+ * location and keyed by parameter name. Mirrors the `serializer` grouping and feeds the default
104
+ * serializers; `body` carries the form `encoding` for a urlencoded or multipart body.
105
+ */
106
+ export type Styles = {
107
+ path?: Record<string, PathParamStyle>
108
+ query?: Record<string, QueryParamStyle>
109
+ header?: Record<string, HeaderParamStyle>
110
+ cookie?: Record<string, CookieParamStyle>
111
+ body?: Record<string, BodyEncoding>
112
+ }
113
+
114
+ function isFormBody(body: unknown): body is BodyInit {
115
+ return (
116
+ body instanceof FormData ||
117
+ body instanceof URLSearchParams ||
118
+ body instanceof Blob ||
119
+ body instanceof ArrayBuffer ||
120
+ ArrayBuffer.isView(body) ||
121
+ typeof body === 'string'
122
+ )
123
+ }
124
+
125
+ export function isDefaultJsonBody(body: unknown): boolean {
126
+ return body !== undefined && body !== null && !isFormBody(body)
127
+ }
128
+
129
+ function appendFormDataValue({ formData, key, value, contentType }: { formData: FormData; key: string; value: unknown; contentType?: string }): void {
130
+ if (value === undefined || value === null) return
131
+ if (value instanceof Blob) formData.append(key, value)
132
+ else if (typeof value === 'object' && !(value instanceof Date)) {
133
+ const json = JSON.stringify(value)
134
+ // A part's media type can only be set by wrapping the value in a typed Blob.
135
+ formData.append(key, contentType ? new Blob([json], { type: contentType }) : json)
136
+ } else formData.append(key, toValue(value))
137
+ }
138
+
139
+ /**
140
+ * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
141
+ * For `multipart/form-data` plain objects become `FormData` and for
142
+ * `application/x-www-form-urlencoded` they become `URLSearchParams`. When `encoding` is supplied each
143
+ * urlencoded property follows its OpenAPI `style` / `explode` / `allowReserved`.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * defaultBodySerializer({ body: { name: 'odie' } }) // '{"name":"odie"}'
148
+ * defaultBodySerializer({ body: { field: 'x' }, contentType: 'multipart/form-data' }) // FormData
149
+ * defaultBodySerializer({ body: { tags: ['a', 'b'] }, contentType: 'application/x-www-form-urlencoded', encoding: { tags: { explode: false } } }) // 'tags=a,b'
150
+ * defaultBodySerializer({ body: { meta: { a: 1 } }, contentType: 'multipart/form-data', encoding: { meta: { contentType: 'application/json' } } }) // FormData with a typed Blob part
151
+ * ```
152
+ */
153
+ export const defaultBodySerializer: BodySerializer = ({ body, contentType, encoding }) => {
154
+ if (body === undefined || body === null) return undefined
155
+ if (isFormBody(body)) return body as BodyInit
156
+ if (contentType?.includes('multipart/form-data')) {
157
+ const formData = new FormData()
158
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
159
+ const partContentType = encoding?.[key]?.contentType
160
+ if (Array.isArray(value)) for (const item of value) appendFormDataValue({ formData, key, value: item, contentType: partContentType })
161
+ else appendFormDataValue({ formData, key, value, contentType: partContentType })
162
+ }
163
+ return formData
164
+ }
165
+ if (contentType?.includes('application/x-www-form-urlencoded')) {
166
+ if (encoding) return serializeUrlencodedBody(body as Record<string, unknown>, encoding)
167
+ return new URLSearchParams(body as Record<string, string>)
168
+ }
169
+ return JSON.stringify(body)
170
+ }
171
+
172
+ function serializeUrlencodedBody(body: Record<string, unknown>, encoding: Record<string, BodyEncoding>): string {
173
+ const parts: Array<string> = []
174
+ for (const [key, value] of Object.entries(body)) {
175
+ const propertyEncoding = encoding[key]
176
+ parts.push(...(propertyEncoding ? serializeStyledQueryParam({ key, value, options: propertyEncoding }) : serializeDefaultQueryParam(key, value)))
177
+ }
178
+ return parts.join('&')
179
+ }
180
+
181
+ function serializeCookie({ name, value, explode }: { name: string; value: unknown; explode: boolean }): string {
182
+ if (Array.isArray(value)) {
183
+ const items = value.filter(notNullish).map((item) => encodeURIComponent(toValue(item)))
184
+ return explode ? items.map((item) => `${name}=${item}`).join('; ') : `${name}=${items.join(',')}`
185
+ }
186
+ if (isRecord(value)) {
187
+ const entries = Object.entries(value).filter(([, item]) => notNullish(item))
188
+ if (explode) return entries.map(([key, item]) => `${key}=${encodeURIComponent(toValue(item))}`).join('; ')
189
+ return `${name}=${entries
190
+ .flatMap(([key, item]) => [key, item])
191
+ .map((item) => encodeURIComponent(toValue(item)))
192
+ .join(',')}`
193
+ }
194
+ return `${name}=${encodeURIComponent(toValue(value))}`
195
+ }
196
+
197
+ /**
198
+ * Serializes cookie parameters into a `Cookie` header value using the OpenAPI `form` style, joined
199
+ * with `; `. Values are URL-encoded and `explode` is honored per parameter.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * serializeCookies({ session: 'abc', ids: [1, 2] }) // 'session=abc; ids=1,2'
204
+ * serializeCookies({ ids: [1, 2] }, { ids: { explode: true } }) // 'ids=1; ids=2'
205
+ * ```
206
+ */
207
+ export function serializeCookies(cookies: Record<string, unknown>, styles?: Record<string, CookieParamStyle>): string {
208
+ const parts: Array<string> = []
209
+ for (const [name, value] of Object.entries(cookies)) {
210
+ if (value === undefined || value === null) continue
211
+ parts.push(serializeCookie({ name, value, explode: styles?.[name]?.explode ?? false }))
212
+ }
213
+ return parts.join('; ')
214
+ }
215
+
216
+ function appendQueryValue({ search, key, value }: { search: URLSearchParams; key: string; value: unknown }): void {
217
+ if (value === undefined || value === null) return
218
+ if (Array.isArray(value)) {
219
+ for (const item of value) appendQueryValue({ search, key, value: item })
220
+ return
221
+ }
222
+ if (isRecord(value)) {
223
+ for (const [prop, propValue] of Object.entries(value)) {
224
+ appendQueryValue({ search, key: `${key}[${prop}]`, value: propValue })
225
+ }
226
+ return
227
+ }
228
+ search.append(key, toValue(value))
229
+ }
230
+
231
+ const queryDelimiters: Record<QueryStyle, string> = { form: ',', spaceDelimited: '%20', pipeDelimited: '|', deepObject: ',' }
232
+
233
+ function notNullish(value: unknown): boolean {
234
+ return value !== undefined && value !== null
235
+ }
236
+
237
+ /**
238
+ * Renders a primitive parameter value as a string, serializing `Date` to ISO-8601 so dates are
239
+ * stable across path, query, cookie, and header locations rather than locale-dependent.
240
+ */
241
+ function toValue(value: unknown): string {
242
+ return value instanceof Date ? value.toISOString() : String(value)
243
+ }
244
+
245
+ /**
246
+ * Percent-encodes a value, keeping RFC 3986 reserved characters intact (used when `allowReserved` is set).
247
+ */
248
+ function encodeReserved(value: unknown): string {
249
+ return encodeURI(toValue(value))
250
+ }
251
+
252
+ /**
253
+ * Percent-encodes a value, escaping reserved characters (the default query/path encoder).
254
+ */
255
+ function encodeComponent(value: unknown): string {
256
+ return encodeURIComponent(toValue(value))
257
+ }
258
+
259
+ /**
260
+ * Whether a value should expand into bracketed/keyed parts. Arrays and `Date` are excluded so they
261
+ * are serialized as a unit (a `Date` becomes an ISO string, not its enumerable own properties).
262
+ */
263
+ function isRecord(value: unknown): value is Record<string, unknown> {
264
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)
265
+ }
266
+
267
+ /**
268
+ * Expands an object or array into `deepObject` query parts, recursing into nested values so
269
+ * `{ a: { b: { c: 1 } } }` becomes `a[b][c]=1`. Primitives terminate the recursion.
270
+ */
271
+ function serializeDeepObject({ key, value, encode }: { key: string; value: unknown; encode: (value: unknown) => string }): Array<string> {
272
+ if (value === undefined || value === null) return []
273
+ if (Array.isArray(value)) return value.flatMap((item, index) => serializeDeepObject({ key: `${key}[${index}]`, value: item, encode }))
274
+ if (isRecord(value)) {
275
+ return Object.entries(value)
276
+ .filter(([, item]) => notNullish(item))
277
+ .flatMap(([prop, item]) => serializeDeepObject({ key: `${key}[${prop}]`, value: item, encode }))
278
+ }
279
+ return [`${encode(key)}=${encode(value)}`]
280
+ }
281
+
282
+ function serializeStyledQueryArray({
283
+ key,
284
+ value,
285
+ options,
286
+ encode,
287
+ }: {
288
+ key: string
289
+ value: Array<unknown>
290
+ options: QueryParamStyle
291
+ encode: (value: unknown) => string
292
+ }): Array<string> {
293
+ const items = value.filter(notNullish)
294
+ if (options.explode ?? true) return items.map((item) => `${encode(key)}=${encode(item)}`)
295
+ return [`${encode(key)}=${items.map(encode).join(queryDelimiters[options.style ?? 'form'])}`]
296
+ }
297
+
298
+ function serializeStyledQueryObject({
299
+ key,
300
+ value,
301
+ options,
302
+ encode,
303
+ }: {
304
+ key: string
305
+ value: Record<string, unknown>
306
+ options: QueryParamStyle
307
+ encode: (value: unknown) => string
308
+ }): Array<string> {
309
+ if ((options.style ?? 'form') === 'deepObject') return serializeDeepObject({ key, value, encode })
310
+ const entries = Object.entries(value).filter(([, item]) => notNullish(item))
311
+ if (options.explode ?? true) return entries.map(([prop, item]) => `${encode(prop)}=${encode(item)}`)
312
+ return [
313
+ `${encode(key)}=${entries
314
+ .flatMap(([prop, item]) => [prop, item])
315
+ .map(encode)
316
+ .join(',')}`,
317
+ ]
318
+ }
319
+
320
+ function serializeStyledQueryParam({ key, value, options }: { key: string; value: unknown; options: QueryParamStyle }): Array<string> {
321
+ if (value === undefined || value === null) return []
322
+ const encode = options.allowReserved ? encodeReserved : encodeComponent
323
+ if (Array.isArray(value)) return serializeStyledQueryArray({ key, value, options, encode })
324
+ if (isRecord(value)) return serializeStyledQueryObject({ key, value, options, encode })
325
+ return [`${encode(key)}=${encode(value)}`]
326
+ }
327
+
328
+ function serializeDefaultQueryParam(key: string, value: unknown): Array<string> {
329
+ const search = new URLSearchParams()
330
+ appendQueryValue({ search, key, value })
331
+ const result = search.toString()
332
+ return result ? [result] : []
333
+ }
334
+
335
+ /**
336
+ * Default query serializer. Members with `options` metadata follow their OpenAPI `style` / `explode`
337
+ * / `allowReserved`. Members without it keep the defaults: arrays explode into repeated keys and
338
+ * nested objects use the `deepObject` style (`key[prop]=value`).
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * defaultQuerySerializer({ id: [3, 4, 5] }) // 'id=3&id=4&id=5'
343
+ * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'form', explode: false } }) // 'id=3,4,5'
344
+ * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'spaceDelimited', explode: false } }) // 'id=3%204%205'
345
+ * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'pipeDelimited', explode: false } }) // 'id=3|4|5'
346
+ * defaultQuerySerializer({ a: { b: 1 } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D=1'
347
+ * defaultQuerySerializer({ a: { b: { c: 1 } } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D%5Bc%5D=1'
348
+ * ```
349
+ */
350
+ export const defaultQuerySerializer: QuerySerializer = (params, options) => {
351
+ const parts: Array<string> = []
352
+ for (const [key, value] of Object.entries(params)) {
353
+ const paramOptions = options?.[key]
354
+ parts.push(...(paramOptions ? serializeStyledQueryParam({ key, value, options: paramOptions }) : serializeDefaultQueryParam(key, value)))
355
+ }
356
+ return parts.join('&')
357
+ }
358
+
359
+ function serializePathPrimitive({ name, value, style }: { name: string; value: unknown; style: PathStyle }): string {
360
+ const encoded = encodeComponent(value)
361
+ if (style === 'label') return `.${encoded}`
362
+ if (style === 'matrix') return `;${name}=${encoded}`
363
+ return encoded
364
+ }
365
+
366
+ function serializePathArray({ name, value, style, explode }: { name: string; value: Array<unknown>; style: PathStyle; explode: boolean }): string {
367
+ const items = value.map(encodeComponent)
368
+ if (style === 'label') return `.${items.join(explode ? '.' : ',')}`
369
+ if (style === 'matrix') return explode ? items.map((item) => `;${name}=${item}`).join('') : `;${name}=${items.join(',')}`
370
+ return items.join(',')
371
+ }
372
+
373
+ function serializePathObject({ name, value, style, explode }: { name: string; value: Record<string, unknown>; style: PathStyle; explode: boolean }): string {
374
+ const members = Object.entries(value).map(([key, item]) =>
375
+ explode ? `${encodeComponent(key)}=${encodeComponent(item)}` : `${encodeComponent(key)},${encodeComponent(item)}`,
376
+ )
377
+ if (style === 'label') return `.${members.join(explode ? '.' : ',')}`
378
+ if (style === 'matrix') return explode ? members.map((member) => `;${member}`).join('') : `;${name}=${members.join(',')}`
379
+ return members.join(',')
380
+ }
381
+
382
+ /**
383
+ * Default path serializer honoring the OpenAPI `style` / `explode` metadata. Without metadata it
384
+ * falls back to `simple` style with `explode: false`. Replaces the previous `String(value)`
385
+ * interpolation, which emitted `[object Object]` for object path params.
386
+ *
387
+ * @example
388
+ * ```ts
389
+ * defaultPathSerializer({ name: 'id', value: [3, 4, 5] }) // '3,4,5'
390
+ * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'label', explode: true } }) // '.3.4.5'
391
+ * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'matrix', explode: true } }) // ';id=3;id=4;id=5'
392
+ * defaultPathSerializer({ name: 'pt', value: { x: 1, y: 2 } }) // 'x,1,y,2'
393
+ * ```
394
+ */
395
+ export const defaultPathSerializer: PathSerializer = ({ name, value, options }) => {
396
+ if (value === undefined || value === null) return ''
397
+ const style = options?.style ?? 'simple'
398
+ const explode = options?.explode ?? false
399
+ if (Array.isArray(value)) return serializePathArray({ name, value, style, explode })
400
+ if (isRecord(value)) return serializePathObject({ name, value, style, explode })
401
+ return serializePathPrimitive({ name, value, style })
402
+ }
403
+
404
+ function serializeHeaderValue(value: unknown, explode: boolean): string {
405
+ if (Array.isArray(value)) return value.filter(notNullish).map(toValue).join(',')
406
+ if (!isRecord(value)) return toValue(value)
407
+ const entries = Object.entries(value).filter(([, item]) => notNullish(item))
408
+ if (explode) return entries.map(([key, item]) => `${key}=${toValue(item)}`).join(',')
409
+ return entries
410
+ .flatMap(([key, item]) => [key, item])
411
+ .map(toValue)
412
+ .join(',')
413
+ }
414
+
415
+ /**
416
+ * Serializes array and object header parameters with the OpenAPI `simple` style before they are
417
+ * merged. Header values are not URL-encoded. Primitive values and headers without metadata pass
418
+ * through untouched.
419
+ */
420
+ export function applyHeaderStyles(headers: HeadersInit | undefined, styles: Record<string, HeaderParamStyle> | undefined): HeadersInit | undefined {
421
+ if (!headers || !styles) return headers
422
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
423
+ return entries.map(([key, value]) => {
424
+ const style = styles[key]
425
+ if (!style || value === undefined || value === null || typeof value !== 'object') return [key, value] as [string, HeaderValue]
426
+ return [key, serializeHeaderValue(value, style.explode ?? false)] as [string, HeaderValue]
427
+ })
428
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A Standard Schema-compatible validator: a minimal duck-type covering Zod v3/v4, valibot, and
3
+ * arktype schemas. Only the `~standard.validate` method is required at runtime.
4
+ */
5
+ export type StandardSchemaValidator<TOutput = unknown> = {
6
+ readonly '~standard': {
7
+ validate(value: unknown): StandardSchemaResult<TOutput> | Promise<StandardSchemaResult<TOutput>>
8
+ }
9
+ }
10
+
11
+ /**
12
+ * The two possible outcomes of a Standard Schema `validate` call. A successful result carries
13
+ * `value`; a failed result carries `issues`.
14
+ */
15
+ export type StandardSchemaResult<TOutput> = { readonly value: TOutput; readonly issues?: undefined } | { readonly issues: ReadonlyArray<StandardSchemaIssue> }
16
+
17
+ /**
18
+ * One validation issue from a Standard Schema `validate` call.
19
+ */
20
+ export type StandardSchemaIssue = {
21
+ readonly message?: string
22
+ readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>
23
+ }
24
+
25
+ /**
26
+ * Thrown by `validateStandardSchema` when validation fails. Carries the raw `issues` array from
27
+ * the schema's `validate` result so callers receive a uniform error shape regardless of which
28
+ * schema library is in use.
29
+ */
30
+ export class ParseError extends Error {
31
+ readonly issues: ReadonlyArray<StandardSchemaIssue>
32
+
33
+ constructor({ issues, message }: { issues: ReadonlyArray<StandardSchemaIssue>; message?: string }) {
34
+ super(message ?? 'Validation failed')
35
+ this.name = 'ParseError'
36
+ this.issues = issues
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Validates `value` against a Standard Schema-compatible `schema`. Returns the parsed output on
42
+ * success; throws `ParseError` with the schema's `issues` on failure. Handles both sync and async
43
+ * `validate` implementations transparently.
44
+ *
45
+ * @example
46
+ * const pet = await validateStandardSchema(PetSchema, rawData)
47
+ */
48
+ export async function validateStandardSchema<TOutput>(schema: StandardSchemaValidator<TOutput>, value: unknown): Promise<TOutput> {
49
+ const result = await schema['~standard'].validate(value)
50
+ if (result.issues) {
51
+ throw new ParseError({ issues: result.issues })
52
+ }
53
+ return result.value as TOutput
54
+ }