@kubb/plugin-client 5.10.0-canary.20261105T162357 → 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.
- package/README.md +1 -1
- package/dist/client-CiHb_Cfg.d.ts +345 -0
- package/dist/index.cjs +288 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +290 -142
- package/dist/index.js.map +1 -1
- package/dist/serializers-Ds8zZwOO.d.ts +190 -0
- package/dist/standardSchema-BShJWz2i.d.ts +104 -0
- package/dist/templates/client.d.ts +2 -0
- package/dist/templates/core.d.ts +3 -0
- package/dist/templates/serializers.d.ts +2 -0
- package/dist/templates/sse.d.ts +124 -0
- package/dist/templates/standardSchema.d.ts +2 -0
- package/docs/guides/write-your-client.md +1 -1
- package/docs/overview.md +1 -1
- package/kubb-extension.json +1 -1
- package/package.json +17 -8
- package/templates/client.ts +5 -0
- package/templates/core.ts +694 -0
- package/templates/serializers.ts +493 -0
- package/templates/sse.ts +274 -0
- package/templates/standardSchema.ts +162 -0
|
@@ -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
|
+
}
|