@kubb/plugin-client 5.12.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/client-Dq0z61hu.d.ts +554 -0
  3. package/dist/index.cjs +1378 -784
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +114 -74
  6. package/dist/index.d.ts +114 -74
  7. package/dist/index.js +1376 -786
  8. package/dist/index.js.map +1 -1
  9. package/dist/serializers-BRgfPGr1.d.ts +119 -0
  10. package/dist/{standardSchema-BShJWz2i.d.ts → standardSchema-CGuiSK7F.d.ts} +15 -37
  11. package/dist/templates/client.d.ts +3 -2
  12. package/dist/templates/core.d.ts +3 -3
  13. package/dist/templates/mediaTypes/formData.d.ts +5 -0
  14. package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
  15. package/dist/templates/mediaTypes.d.ts +2 -0
  16. package/dist/templates/serializers/cookies.d.ts +5 -0
  17. package/dist/templates/serializers/headers.d.ts +5 -0
  18. package/dist/templates/serializers/path.d.ts +5 -0
  19. package/dist/templates/serializers/pathStyles.d.ts +5 -0
  20. package/dist/templates/serializers/query.d.ts +5 -0
  21. package/dist/templates/serializers/queryStyles.d.ts +14 -0
  22. package/dist/templates/serializers.d.ts +2 -2
  23. package/dist/templates/sse.d.ts +20 -113
  24. package/dist/templates/standardSchema.d.ts +1 -1
  25. package/docs/guides/authentication.md +9 -1
  26. package/docs/guides/migrate-to-v6.md +139 -0
  27. package/docs/guides/validate-requests-and-responses.md +8 -3
  28. package/docs/guides/write-your-client.md +117 -11
  29. package/docs/options.md +2 -2
  30. package/docs/overview.md +1 -1
  31. package/kubb-extension.json +1 -1
  32. package/package.json +6 -5
  33. package/src/builders/callConfig.ts +80 -0
  34. package/src/builders/contentType.ts +35 -0
  35. package/src/builders/generics.ts +29 -0
  36. package/src/builders/operationBody.ts +43 -0
  37. package/src/builders/operationData.ts +64 -0
  38. package/src/builders/returnStatement.ts +103 -0
  39. package/src/builders/security.ts +125 -0
  40. package/src/builders/signature.ts +71 -0
  41. package/src/builders/styles.ts +94 -0
  42. package/src/builders/validator.ts +84 -0
  43. package/src/builders/validatorOptions.ts +53 -0
  44. package/src/clientTypes.ts +195 -0
  45. package/src/components/ClientRuntime.tsx +97 -0
  46. package/src/components/Operation.tsx +72 -0
  47. package/src/components/SdkClient.tsx +100 -0
  48. package/src/components/SdkFacade.tsx +35 -0
  49. package/src/generators/clientGenerator.tsx +91 -6
  50. package/src/generators/sdkGenerator.tsx +209 -0
  51. package/src/index.ts +3 -1
  52. package/src/macros.ts +9 -0
  53. package/src/plugin.ts +7 -8
  54. package/src/resolver.ts +28 -0
  55. package/src/setup.ts +90 -0
  56. package/src/types.ts +4 -2
  57. package/templates/client.ts +2 -1
  58. package/templates/core.ts +735 -286
  59. package/templates/mediaTypes/formData.ts +59 -0
  60. package/templates/mediaTypes/urlencoded.ts +33 -0
  61. package/templates/mediaTypes.ts +147 -0
  62. package/templates/serializers/cookies.ts +58 -0
  63. package/templates/serializers/headers.ts +35 -0
  64. package/templates/serializers/path.ts +19 -0
  65. package/templates/serializers/pathStyles.ts +27 -0
  66. package/templates/serializers/query.ts +21 -0
  67. package/templates/serializers/queryStyles.ts +107 -0
  68. package/templates/serializers.ts +48 -364
  69. package/templates/sse.ts +87 -178
  70. package/templates/standardSchema.ts +16 -37
  71. package/dist/client-BxO6Dygu.d.ts +0 -355
  72. package/dist/serializers-Ds8zZwOO.d.ts +0 -190
@@ -2,120 +2,100 @@ export type HeaderValue = string | number | boolean | null | undefined | object
2
2
  export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
3
3
 
4
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.
5
+ * A body `fetch` accepts, read from `RequestInit` so a project without the `dom` lib still has it.
7
6
  */
8
7
  export type RequestBody = NonNullable<RequestInit['body']>
9
8
 
10
- export type HttpMethod = 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'QUERY'
9
+ export type HttpMethod = 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'QUERY' | 'TRACE' | (string & {})
11
10
 
12
11
  /**
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`.
12
+ * The OpenAPI query style. `form` is the default.
16
13
  */
17
14
  export type QueryStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject'
18
15
 
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
16
  export type SerializationStyle<TStyle = string> = {
25
17
  style?: TStyle
26
18
  explode?: boolean
27
19
  allowReserved?: boolean
28
20
  }
29
21
 
30
- /** The media type of a parameter described with `content`; JSON sends one JSON-encoded value. */
22
+ /**
23
+ * The media type of a parameter described with `content`. JSON sends one JSON-encoded value.
24
+ */
31
25
  export type ParamContentType = {
32
26
  contentType?: string
33
27
  }
34
28
 
35
29
  /**
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.
30
+ * `querystring` marks an OpenAPI 3.2 `in: querystring` parameter, sent as the whole query string.
38
31
  */
39
32
  export type QueryParamStyle = SerializationStyle<QueryStyle> & ParamContentType & { querystring?: boolean }
40
33
 
41
34
  /**
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.
35
+ * Turns the query object into a search string, following each parameter's OpenAPI style when given.
45
36
  */
46
37
  export type QuerySerializer = (params: Record<string, unknown>, options?: Record<string, QueryParamStyle>) => string
47
38
 
48
39
  /**
49
- * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the
50
- * OpenAPI `form` style, so only `explode` is configurable.
40
+ * `cookie` is the OpenAPI 3.2 style that sends values as they are, without percent-encoding (RFC 6265). `form` is the default.
51
41
  */
52
42
  export type CookieParamStyle = ParamContentType & {
43
+ style?: 'form' | 'cookie'
53
44
  explode?: boolean
54
45
  }
55
46
 
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
47
  export type HeaderParamStyle = ParamContentType & {
67
48
  explode?: boolean
68
49
  }
69
50
 
70
51
  /**
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.
52
+ * The OpenAPI `encoding` of one form body property. `contentType` sets a multipart part's media type.
74
53
  */
75
54
  export type BodyEncoding = SerializationStyle<QueryStyle> & {
76
55
  contentType?: string
77
56
  }
78
57
 
79
58
  /**
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.
59
+ * The OpenAPI path style. `simple` is the default.
83
60
  */
84
- export type BodySerializer = (args: { body: unknown; contentType?: string; encoding?: Record<string, BodyEncoding> }) => RequestBody | undefined
61
+ export type PathStyle = 'simple' | 'label' | 'matrix'
62
+
63
+ export type PathParamStyle = SerializationStyle<PathStyle>
85
64
 
86
65
  /**
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.
66
+ * Joins the cookie parameters of a call into a `Cookie` header value.
89
67
  */
90
- export type PathStyle = 'simple' | 'label' | 'matrix'
68
+ export type CookieSerializer = (cookies: Record<string, unknown>, styles?: Record<string, CookieParamStyle>) => string
91
69
 
92
70
  /**
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.
71
+ * Serializes the header parameters of a call that are arrays or objects.
95
72
  */
96
- export type PathParamStyle = SerializationStyle<PathStyle>
73
+ export type HeaderSerializer = (headers: HeadersInit | undefined, styles?: Record<string, HeaderParamStyle>) => HeadersInit | undefined
97
74
 
98
75
  /**
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.
76
+ * Turns one path parameter into its URL segment.
102
77
  */
103
- export type PathSerializer = (args: { name: string; value: unknown; options?: PathParamStyle }) => string
78
+ export type PathSerializer = (args: { name: string; value: unknown; options?: PathParamStyle | undefined }) => string
104
79
 
105
80
  /**
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.
81
+ * Parameter serializers, set per client or per call. `.kubb/client.ts` registers the ones the spec needs. Bodies are
82
+ * encoded by `mediaTypes`.
108
83
  */
109
84
  export type Serializers = {
110
85
  query?: QuerySerializer
111
- body?: BodySerializer
112
86
  path?: PathSerializer
87
+ /**
88
+ * Turns array and object header values into strings. Without one, such a value is sent as JSON.
89
+ */
90
+ header?: HeaderSerializer
91
+ /**
92
+ * Sends the `cookies` of a call. Kubb registers `serializeCookies` when the spec has a cookie parameter.
93
+ */
94
+ cookie?: CookieSerializer
113
95
  }
114
96
 
115
97
  /**
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.
98
+ * The OpenAPI styles of a generated call's parameters, by location and name. `body` holds the form `encoding`.
119
99
  */
120
100
  export type Styles = {
121
101
  path?: Record<string, PathParamStyle>
@@ -125,145 +105,30 @@ export type Styles = {
125
105
  body?: Record<string, BodyEncoding>
126
106
  }
127
107
 
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
108
  /**
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.
109
+ * Sends a `bigint` as a JSON number, and throws past the safe-integer range instead of losing precision.
146
110
  */
147
- function jsonReplacer(_key: string, value: unknown): unknown {
111
+ export function jsonReplacer(_key: string, value: unknown): unknown {
148
112
  if (typeof value !== 'bigint') return value
149
113
  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.`)
114
+ throw new TypeError(`Cannot serialize ${value}n as JSON without losing precision, register a media type to send it another way.`)
151
115
  return Number(value)
152
116
  }
153
117
 
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
118
  /**
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`.
119
+ * A `Content-Type` without its parameters, in lower case.
218
120
  */
219
121
  export function baseContentType(value: string | null | undefined): string | undefined {
220
122
  if (!value) return undefined
221
123
  return value.split(';')[0]!.trim().toLowerCase() || undefined
222
124
  }
223
125
 
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
126
  /**
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
- * ```
127
+ * Whether a media type is JSON, such as `application/json` or `application/problem+json`.
254
128
  */
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('; ')
129
+ export function isJsonMediaType(contentType: string | null | undefined): boolean {
130
+ const type = baseContentType(contentType) ?? ''
131
+ return type.endsWith('/json') || type.endsWith('+json')
267
132
  }
268
133
 
269
134
  function appendQueryValue({ search, key, value }: { search: URLSearchParams; key: string; value: unknown }): void {
@@ -281,213 +146,32 @@ function appendQueryValue({ search, key, value }: { search: URLSearchParams; key
281
146
  search.append(key, toValue(value))
282
147
  }
283
148
 
284
- const queryDelimiters: Record<QueryStyle, string> = { form: ',', spaceDelimited: '%20', pipeDelimited: '|', deepObject: ',' }
285
-
286
- function notNullish(value: unknown): boolean {
149
+ export function notNullish(value: unknown): boolean {
287
150
  return value !== undefined && value !== null
288
151
  }
289
152
 
290
153
  /**
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.
154
+ * Renders a parameter value as a string. A `Date` becomes ISO-8601, so it does not depend on the locale.
293
155
  */
294
- function toValue(value: unknown): string {
156
+ export function toValue(value: unknown): string {
295
157
  return value instanceof Date ? value.toISOString() : String(value)
296
158
  }
297
159
 
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 {
160
+ export function encodeComponent(value: unknown): string {
309
161
  return encodeURIComponent(toValue(value))
310
162
  }
311
163
 
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> {
164
+ // A `Date` is a unit, not an object to expand.
165
+ export function isRecord(value: unknown): value is Record<string, unknown> {
317
166
  return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)
318
167
  }
319
168
 
320
169
  /**
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.
170
+ * One parameter as form pairs, with an array repeating its key and an object as `key[prop]`.
323
171
  */
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> {
172
+ export function serializeDefaultQueryParam(key: string, value: unknown): Array<string> {
382
173
  const search = new URLSearchParams()
383
174
  appendQueryValue({ search, key, value })
384
175
  const result = search.toString()
385
176
  return result ? [result] : []
386
177
  }
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
- }