@kubb/plugin-client 5.10.0-canary.20261105T164114 → 5.10.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.
@@ -0,0 +1,493 @@
1
+ export type HeaderValue = string | number | boolean | null | undefined | object
2
+ export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
3
+
4
+ /**
5
+ * The request body type `fetch` accepts, derived from `RequestInit` instead of the global `BodyInit`
6
+ * name, which a Node-only project (`@types/node` without the `dom` lib) does not declare.
7
+ */
8
+ export type RequestBody = NonNullable<RequestInit['body']>
9
+
10
+ export type HttpMethod = 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'QUERY'
11
+
12
+ /**
13
+ * The OpenAPI query-parameter serialization style. `form` is the default; `spaceDelimited` and
14
+ * `pipeDelimited` join arrays with a space or pipe, and `deepObject` renders objects as
15
+ * `key[prop]=value`.
16
+ */
17
+ export type QueryStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject'
18
+
19
+ /**
20
+ * The serialization metadata shared by the styled parameter locations: the OpenAPI `style` (typed per
21
+ * location through `TStyle`), `explode`, and `allowReserved` (keeps RFC 3986 reserved characters
22
+ * unencoded, used by query and request bodies).
23
+ */
24
+ export type SerializationStyle<TStyle = string> = {
25
+ style?: TStyle
26
+ explode?: boolean
27
+ allowReserved?: boolean
28
+ }
29
+
30
+ /** The media type of a parameter described with `content`; JSON sends one JSON-encoded value. */
31
+ export type ParamContentType = {
32
+ contentType?: string
33
+ }
34
+
35
+ /**
36
+ * The per-parameter query serialization metadata carried by the generated request. `querystring` marks an OpenAPI 3.2
37
+ * `in: querystring` parameter, whose JSON value is the whole query string, sent without its name.
38
+ */
39
+ export type QueryParamStyle = SerializationStyle<QueryStyle> & ParamContentType & { querystring?: boolean }
40
+
41
+ /**
42
+ * Serializes the query object into a search string. The optional second argument carries the
43
+ * per-parameter OpenAPI `style` / `explode` / `allowReserved` metadata; without it arrays explode
44
+ * into repeated keys and nested objects use the `deepObject` style.
45
+ */
46
+ export type QuerySerializer = (params: Record<string, unknown>, options?: Record<string, QueryParamStyle>) => string
47
+
48
+ /**
49
+ * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the
50
+ * OpenAPI `form` style, so only `explode` is configurable.
51
+ */
52
+ export type CookieParamStyle = ParamContentType & {
53
+ explode?: boolean
54
+ }
55
+
56
+ /**
57
+ * The per-parameter header serialization metadata carried by the generated request. Headers use the
58
+ * OpenAPI `simple` style, so only `explode` is configurable.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * // styles.header: { 'X-Ids': { explode: false } }, header [3, 4] -> 'X-Ids: 3,4'
63
+ * // styles.header: { 'X-Filter': { explode: true } }, header { role: 'admin' } -> 'X-Filter: role=admin'
64
+ * ```
65
+ */
66
+ export type HeaderParamStyle = ParamContentType & {
67
+ explode?: boolean
68
+ }
69
+
70
+ /**
71
+ * The per-property `encoding` metadata for an `application/x-www-form-urlencoded` or
72
+ * `multipart/form-data` request body. `contentType` overrides the part's media type; `style` /
73
+ * `explode` / `allowReserved` follow the OpenAPI query rules for urlencoded bodies.
74
+ */
75
+ export type BodyEncoding = SerializationStyle<QueryStyle> & {
76
+ contentType?: string
77
+ }
78
+
79
+ /**
80
+ * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`,
81
+ * `ArrayBuffer`, and string bodies pass through untouched. The optional `encoding` argument carries
82
+ * the per-property OpenAPI `encoding` metadata for form bodies.
83
+ */
84
+ export type BodySerializer = (args: { body: unknown; contentType?: string; encoding?: Record<string, BodyEncoding> }) => RequestBody | undefined
85
+
86
+ /**
87
+ * The OpenAPI path-parameter serialization style. `simple` is the default and emits the bare value;
88
+ * `label` prefixes a `.` and `matrix` prefixes a `;name=` segment.
89
+ */
90
+ export type PathStyle = 'simple' | 'label' | 'matrix'
91
+
92
+ /**
93
+ * The per-parameter serialization metadata carried by the generated request. `style` selects the
94
+ * OpenAPI style and `explode` controls how arrays and objects expand.
95
+ */
96
+ export type PathParamStyle = SerializationStyle<PathStyle>
97
+
98
+ /**
99
+ * Serializes a single path parameter for interpolation into the URL, honoring the OpenAPI `style` /
100
+ * `explode` passed as `options`. Defaults to `simple` style with `explode: false`: primitives are
101
+ * URL-encoded, arrays join their members with commas, and objects flatten to `key,value` pairs.
102
+ */
103
+ export type PathSerializer = (args: { name: string; value: unknown; options?: PathParamStyle }) => string
104
+
105
+ /**
106
+ * The per-concern serializers, grouped so they can be set in one place and overridden per client or
107
+ * per call. Each field falls back to the matching `default*Serializer` when omitted.
108
+ */
109
+ export type Serializers = {
110
+ query?: QuerySerializer
111
+ body?: BodySerializer
112
+ path?: PathSerializer
113
+ }
114
+
115
+ /**
116
+ * The per-parameter OpenAPI `style` / `explode` metadata a generated request carries, grouped by
117
+ * location and keyed by parameter name. Mirrors the `serializer` grouping and feeds the default
118
+ * serializers; `body` carries the form `encoding` for a urlencoded or multipart body.
119
+ */
120
+ export type Styles = {
121
+ path?: Record<string, PathParamStyle>
122
+ query?: Record<string, QueryParamStyle>
123
+ header?: Record<string, HeaderParamStyle>
124
+ cookie?: Record<string, CookieParamStyle>
125
+ body?: Record<string, BodyEncoding>
126
+ }
127
+
128
+ function isFormBody(body: unknown): body is RequestBody {
129
+ return (
130
+ body instanceof FormData ||
131
+ body instanceof URLSearchParams ||
132
+ body instanceof Blob ||
133
+ body instanceof ArrayBuffer ||
134
+ ArrayBuffer.isView(body) ||
135
+ typeof body === 'string'
136
+ )
137
+ }
138
+
139
+ export function isDefaultJsonBody(body: unknown): boolean {
140
+ return body !== undefined && body !== null && !isFormBody(body)
141
+ }
142
+
143
+ /**
144
+ * Emits a `bigint` (`format: int64`) as a JSON number, which `JSON.stringify` refuses to do itself.
145
+ * Past the safe-integer range it throws, so an id never goes out silently truncated.
146
+ */
147
+ function jsonReplacer(_key: string, value: unknown): unknown {
148
+ if (typeof value !== 'bigint') return value
149
+ if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER)
150
+ throw new TypeError(`Cannot serialize ${value}n as JSON without losing precision, register a serializer.body to send it another way.`)
151
+ return Number(value)
152
+ }
153
+
154
+ function appendFormDataValue({ formData, key, value, contentType }: { formData: FormData; key: string; value: unknown; contentType?: string }): void {
155
+ if (value === undefined || value === null) return
156
+ if (value instanceof Blob) {
157
+ formData.append(key, value)
158
+ return
159
+ }
160
+ if (typeof value === 'object' && !(value instanceof Date)) {
161
+ const json = JSON.stringify(value, jsonReplacer)
162
+ // A part's media type can only be set by wrapping the value in a typed Blob.
163
+ formData.append(key, contentType ? new Blob([json], { type: contentType }) : json)
164
+ return
165
+ }
166
+ formData.append(key, toValue(value))
167
+ }
168
+
169
+ /**
170
+ * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
171
+ * For `multipart/form-data` plain objects become `FormData` and for
172
+ * `application/x-www-form-urlencoded` they become `URLSearchParams`. When `encoding` is supplied each
173
+ * urlencoded property follows its OpenAPI `style` / `explode` / `allowReserved`.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * defaultBodySerializer({ body: { name: 'odie' } }) // '{"name":"odie"}'
178
+ * defaultBodySerializer({ body: { field: 'x' }, contentType: 'multipart/form-data' }) // FormData
179
+ * defaultBodySerializer({ body: { tags: ['a', 'b'] }, contentType: 'application/x-www-form-urlencoded', encoding: { tags: { explode: false } } }) // 'tags=a,b'
180
+ * defaultBodySerializer({ body: { meta: { a: 1 } }, contentType: 'multipart/form-data', encoding: { meta: { contentType: 'application/json' } } }) // FormData with a typed Blob part
181
+ * ```
182
+ */
183
+ export const defaultBodySerializer: BodySerializer = ({ body, contentType, encoding }) => {
184
+ if (body === undefined || body === null) return undefined
185
+ if (typeof body === 'string' && isJsonMediaType(contentType)) return JSON.stringify(body)
186
+ if (isFormBody(body)) return body as RequestBody
187
+ const normalizedContentType = contentType?.toLowerCase()
188
+ if (normalizedContentType?.includes('multipart/form-data')) {
189
+ const formData = new FormData()
190
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
191
+ const partContentType = encoding?.[key]?.contentType
192
+ if (Array.isArray(value)) {
193
+ for (const item of value) appendFormDataValue({ formData, key, value: item, contentType: partContentType })
194
+ continue
195
+ }
196
+ appendFormDataValue({ formData, key, value, contentType: partContentType })
197
+ }
198
+ return formData
199
+ }
200
+ if (normalizedContentType?.includes('application/x-www-form-urlencoded')) {
201
+ if (encoding) return serializeUrlencodedBody(body as Record<string, unknown>, encoding)
202
+ return new URLSearchParams(serializeUrlencodedBody(body as Record<string, unknown>, {}))
203
+ }
204
+ return JSON.stringify(body, jsonReplacer)
205
+ }
206
+
207
+ function serializeUrlencodedBody(body: Record<string, unknown>, encoding: Record<string, BodyEncoding>): string {
208
+ const parts: Array<string> = []
209
+ for (const [key, value] of Object.entries(body)) {
210
+ const propertyEncoding = encoding[key]
211
+ parts.push(...(propertyEncoding ? serializeStyledQueryParam({ key, value, options: propertyEncoding }) : serializeDefaultQueryParam(key, value)))
212
+ }
213
+ return parts.join('&')
214
+ }
215
+
216
+ /**
217
+ * The media type of a `Content-Type` value without its parameters, in lower case: `application/json; charset=utf-8` gives `application/json`.
218
+ */
219
+ export function baseContentType(value: string | null | undefined): string | undefined {
220
+ if (!value) return undefined
221
+ return value.split(';')[0]!.trim().toLowerCase() || undefined
222
+ }
223
+
224
+ /** Whether a content type carries JSON: `application/json`, `text/json`, or a `+json` suffix. */
225
+ export function isJsonMediaType(contentType: string | null | undefined): boolean {
226
+ return /[/+]json$/.test(baseContentType(contentType) ?? '')
227
+ }
228
+
229
+ function serializeCookie({ name, value, explode }: { name: string; value: unknown; explode: boolean }): string {
230
+ if (Array.isArray(value)) {
231
+ const items = value.filter(notNullish).map((item) => encodeURIComponent(toValue(item)))
232
+ return explode ? items.map((item) => `${name}=${item}`).join('; ') : `${name}=${items.join(',')}`
233
+ }
234
+ if (isRecord(value)) {
235
+ const entries = Object.entries(value).filter(([, item]) => notNullish(item))
236
+ if (explode) return entries.map(([key, item]) => `${key}=${encodeURIComponent(toValue(item))}`).join('; ')
237
+ return `${name}=${entries
238
+ .flatMap(([key, item]) => [key, item])
239
+ .map((item) => encodeURIComponent(toValue(item)))
240
+ .join(',')}`
241
+ }
242
+ return `${name}=${encodeURIComponent(toValue(value))}`
243
+ }
244
+
245
+ /**
246
+ * Serializes cookie parameters into a `Cookie` header value using the OpenAPI `form` style, joined
247
+ * with `; `. Values are URL-encoded and `explode` is honored per parameter.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * serializeCookies({ session: 'abc', ids: [1, 2] }) // 'session=abc; ids=1,2'
252
+ * serializeCookies({ ids: [1, 2] }, { ids: { explode: true } }) // 'ids=1; ids=2'
253
+ * ```
254
+ */
255
+ export function serializeCookies(cookies: Record<string, unknown>, styles?: Record<string, CookieParamStyle>): string {
256
+ const parts: Array<string> = []
257
+ for (const [name, value] of Object.entries(cookies)) {
258
+ if (value === undefined || value === null) continue
259
+ const style = styles?.[name]
260
+ if (isJsonMediaType(style?.contentType)) {
261
+ parts.push(`${name}=${encodeURIComponent(JSON.stringify(value, jsonReplacer))}`)
262
+ continue
263
+ }
264
+ parts.push(serializeCookie({ name, value, explode: style?.explode ?? false }))
265
+ }
266
+ return parts.join('; ')
267
+ }
268
+
269
+ function appendQueryValue({ search, key, value }: { search: URLSearchParams; key: string; value: unknown }): void {
270
+ if (value === undefined || value === null) return
271
+ if (Array.isArray(value)) {
272
+ for (const item of value) appendQueryValue({ search, key, value: item })
273
+ return
274
+ }
275
+ if (isRecord(value)) {
276
+ for (const [prop, propValue] of Object.entries(value)) {
277
+ appendQueryValue({ search, key: `${key}[${prop}]`, value: propValue })
278
+ }
279
+ return
280
+ }
281
+ search.append(key, toValue(value))
282
+ }
283
+
284
+ const queryDelimiters: Record<QueryStyle, string> = { form: ',', spaceDelimited: '%20', pipeDelimited: '|', deepObject: ',' }
285
+
286
+ function notNullish(value: unknown): boolean {
287
+ return value !== undefined && value !== null
288
+ }
289
+
290
+ /**
291
+ * Renders a primitive parameter value as a string, serializing `Date` to ISO-8601 so dates are
292
+ * stable across path, query, cookie, and header locations rather than locale-dependent.
293
+ */
294
+ function toValue(value: unknown): string {
295
+ return value instanceof Date ? value.toISOString() : String(value)
296
+ }
297
+
298
+ /**
299
+ * Percent-encodes a value, keeping RFC 3986 reserved characters intact (used when `allowReserved` is set).
300
+ */
301
+ function encodeReserved(value: unknown): string {
302
+ return encodeURI(toValue(value))
303
+ }
304
+
305
+ /**
306
+ * Percent-encodes a value, escaping reserved characters (the default query/path encoder).
307
+ */
308
+ function encodeComponent(value: unknown): string {
309
+ return encodeURIComponent(toValue(value))
310
+ }
311
+
312
+ /**
313
+ * Whether a value should expand into bracketed/keyed parts. Arrays and `Date` are excluded so they
314
+ * are serialized as a unit (a `Date` becomes an ISO string, not its enumerable own properties).
315
+ */
316
+ function isRecord(value: unknown): value is Record<string, unknown> {
317
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)
318
+ }
319
+
320
+ /**
321
+ * Expands an object or array into `deepObject` query parts, recursing into nested values so
322
+ * `{ a: { b: { c: 1 } } }` becomes `a[b][c]=1`. Primitives terminate the recursion.
323
+ */
324
+ function serializeDeepObject({ key, value, encode }: { key: string; value: unknown; encode: (value: unknown) => string }): Array<string> {
325
+ if (value === undefined || value === null) return []
326
+ if (Array.isArray(value)) return value.flatMap((item, index) => serializeDeepObject({ key: `${key}[${index}]`, value: item, encode }))
327
+ if (isRecord(value)) {
328
+ return Object.entries(value)
329
+ .filter(([, item]) => notNullish(item))
330
+ .flatMap(([prop, item]) => serializeDeepObject({ key: `${key}[${prop}]`, value: item, encode }))
331
+ }
332
+ return [`${encode(key)}=${encode(value)}`]
333
+ }
334
+
335
+ function serializeStyledQueryArray({
336
+ key,
337
+ value,
338
+ options,
339
+ encode,
340
+ }: {
341
+ key: string
342
+ value: Array<unknown>
343
+ options: QueryParamStyle
344
+ encode: (value: unknown) => string
345
+ }): Array<string> {
346
+ const items = value.filter(notNullish)
347
+ if (options.explode ?? true) return items.map((item) => `${encode(key)}=${encode(item)}`)
348
+ return [`${encode(key)}=${items.map(encode).join(queryDelimiters[options.style ?? 'form'])}`]
349
+ }
350
+
351
+ function serializeStyledQueryObject({
352
+ key,
353
+ value,
354
+ options,
355
+ encode,
356
+ }: {
357
+ key: string
358
+ value: Record<string, unknown>
359
+ options: QueryParamStyle
360
+ encode: (value: unknown) => string
361
+ }): Array<string> {
362
+ if ((options.style ?? 'form') === 'deepObject') return serializeDeepObject({ key, value, encode })
363
+ const entries = Object.entries(value).filter(([, item]) => notNullish(item))
364
+ if (options.explode ?? true) return entries.map(([prop, item]) => `${encode(prop)}=${encode(item)}`)
365
+ return [
366
+ `${encode(key)}=${entries
367
+ .flatMap(([prop, item]) => [prop, item])
368
+ .map(encode)
369
+ .join(',')}`,
370
+ ]
371
+ }
372
+
373
+ function serializeStyledQueryParam({ key, value, options }: { key: string; value: unknown; options: QueryParamStyle }): Array<string> {
374
+ if (value === undefined || value === null) return []
375
+ const encode = options.allowReserved ? encodeReserved : encodeComponent
376
+ if (Array.isArray(value)) return serializeStyledQueryArray({ key, value, options, encode })
377
+ if (isRecord(value)) return serializeStyledQueryObject({ key, value, options, encode })
378
+ return [`${encode(key)}=${encode(value)}`]
379
+ }
380
+
381
+ function serializeDefaultQueryParam(key: string, value: unknown): Array<string> {
382
+ const search = new URLSearchParams()
383
+ appendQueryValue({ search, key, value })
384
+ const result = search.toString()
385
+ return result ? [result] : []
386
+ }
387
+
388
+ /**
389
+ * Default query serializer. Members with `options` metadata follow their OpenAPI `style` / `explode`
390
+ * / `allowReserved`. Members without it keep the defaults: arrays explode into repeated keys and
391
+ * nested objects use the `deepObject` style (`key[prop]=value`).
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * defaultQuerySerializer({ id: [3, 4, 5] }) // 'id=3&id=4&id=5'
396
+ * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'form', explode: false } }) // 'id=3,4,5'
397
+ * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'spaceDelimited', explode: false } }) // 'id=3%204%205'
398
+ * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'pipeDelimited', explode: false } }) // 'id=3|4|5'
399
+ * defaultQuerySerializer({ a: { b: 1 } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D=1'
400
+ * defaultQuerySerializer({ a: { b: { c: 1 } } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D%5Bc%5D=1'
401
+ * ```
402
+ */
403
+ export const defaultQuerySerializer: QuerySerializer = (params, options) => {
404
+ const parts: Array<string> = []
405
+ for (const [key, value] of Object.entries(params)) {
406
+ const paramOptions = options?.[key]
407
+ if (notNullish(value) && isJsonMediaType(paramOptions?.contentType)) {
408
+ const json = encodeComponent(JSON.stringify(value, jsonReplacer))
409
+ parts.push(paramOptions?.querystring ? json : `${encodeComponent(key)}=${json}`)
410
+ continue
411
+ }
412
+ parts.push(...(paramOptions ? serializeStyledQueryParam({ key, value, options: paramOptions }) : serializeDefaultQueryParam(key, value)))
413
+ }
414
+ return parts.join('&')
415
+ }
416
+
417
+ function serializePathPrimitive({ name, value, style }: { name: string; value: unknown; style: PathStyle }): string {
418
+ const encoded = encodeComponent(value)
419
+ if (style === 'label') return `.${encoded}`
420
+ if (style === 'matrix') return `;${name}=${encoded}`
421
+ return encoded
422
+ }
423
+
424
+ function serializePathArray({ name, value, style, explode }: { name: string; value: Array<unknown>; style: PathStyle; explode: boolean }): string {
425
+ const items = value.map(encodeComponent)
426
+ if (style === 'label') return `.${items.join(explode ? '.' : ',')}`
427
+ if (style === 'matrix') return explode ? items.map((item) => `;${name}=${item}`).join('') : `;${name}=${items.join(',')}`
428
+ return items.join(',')
429
+ }
430
+
431
+ function serializePathObject({ name, value, style, explode }: { name: string; value: Record<string, unknown>; style: PathStyle; explode: boolean }): string {
432
+ const members = Object.entries(value).map(([key, item]) =>
433
+ explode ? `${encodeComponent(key)}=${encodeComponent(item)}` : `${encodeComponent(key)},${encodeComponent(item)}`,
434
+ )
435
+ if (style === 'label') return `.${members.join(explode ? '.' : ',')}`
436
+ if (style === 'matrix') return explode ? members.map((member) => `;${member}`).join('') : `;${name}=${members.join(',')}`
437
+ return members.join(',')
438
+ }
439
+
440
+ /**
441
+ * Default path serializer honoring the OpenAPI `style` / `explode` metadata. Without metadata it
442
+ * falls back to `simple` style with `explode: false`. Replaces the previous `String(value)`
443
+ * interpolation, which emitted `[object Object]` for object path params.
444
+ *
445
+ * @example
446
+ * ```ts
447
+ * defaultPathSerializer({ name: 'id', value: [3, 4, 5] }) // '3,4,5'
448
+ * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'label', explode: true } }) // '.3.4.5'
449
+ * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'matrix', explode: true } }) // ';id=3;id=4;id=5'
450
+ * defaultPathSerializer({ name: 'pt', value: { x: 1, y: 2 } }) // 'x,1,y,2'
451
+ * ```
452
+ */
453
+ export const defaultPathSerializer: PathSerializer = ({ name, value, options }) => {
454
+ if (value === undefined || value === null) return ''
455
+ const style = options?.style ?? 'simple'
456
+ const explode = options?.explode ?? false
457
+ if (Array.isArray(value)) return serializePathArray({ name, value, style, explode })
458
+ if (isRecord(value)) return serializePathObject({ name, value, style, explode })
459
+ return serializePathPrimitive({ name, value, style })
460
+ }
461
+
462
+ function serializeHeaderValue(value: unknown, explode: boolean): string {
463
+ if (Array.isArray(value)) return value.filter(notNullish).map(toValue).join(',')
464
+ if (!isRecord(value)) return toValue(value)
465
+ const entries = Object.entries(value).filter(([, item]) => notNullish(item))
466
+ if (explode) return entries.map(([key, item]) => `${key}=${toValue(item)}`).join(',')
467
+ return entries
468
+ .flatMap(([key, item]) => [key, item])
469
+ .map(toValue)
470
+ .join(',')
471
+ }
472
+
473
+ /** Header values must be Latin-1, so non-ASCII characters become JSON `\uXXXX` escapes. */
474
+ function toAsciiJson(value: unknown): string {
475
+ return JSON.stringify(value, jsonReplacer).replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)
476
+ }
477
+
478
+ /**
479
+ * Serializes array and object header parameters with the OpenAPI `simple` style before they are
480
+ * merged. Header values are not URL-encoded. Primitive values and headers without metadata pass
481
+ * through untouched.
482
+ */
483
+ export function applyHeaderStyles(headers: HeadersInit | undefined, styles: Record<string, HeaderParamStyle> | undefined): HeadersInit | undefined {
484
+ if (!headers || !styles) return headers
485
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
486
+ return entries.map(([key, value]) => {
487
+ const style = styles[key]
488
+ if (!style || value === undefined || value === null) return [key, value] as [string, HeaderValue]
489
+ if (isJsonMediaType(style.contentType)) return [key, toAsciiJson(value)] as [string, HeaderValue]
490
+ if (typeof value !== 'object') return [key, value] as [string, HeaderValue]
491
+ return [key, serializeHeaderValue(value, style.explode ?? false)] as [string, HeaderValue]
492
+ })
493
+ }