@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.
- package/README.md +1 -1
- package/dist/client-Dq0z61hu.d.ts +554 -0
- package/dist/index.cjs +1378 -784
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -74
- package/dist/index.d.ts +114 -74
- package/dist/index.js +1376 -786
- package/dist/index.js.map +1 -1
- package/dist/serializers-BRgfPGr1.d.ts +119 -0
- package/dist/{standardSchema-BShJWz2i.d.ts → standardSchema-CGuiSK7F.d.ts} +15 -37
- package/dist/templates/client.d.ts +3 -2
- package/dist/templates/core.d.ts +3 -3
- package/dist/templates/mediaTypes/formData.d.ts +5 -0
- package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
- package/dist/templates/mediaTypes.d.ts +2 -0
- package/dist/templates/serializers/cookies.d.ts +5 -0
- package/dist/templates/serializers/headers.d.ts +5 -0
- package/dist/templates/serializers/path.d.ts +5 -0
- package/dist/templates/serializers/pathStyles.d.ts +5 -0
- package/dist/templates/serializers/query.d.ts +5 -0
- package/dist/templates/serializers/queryStyles.d.ts +14 -0
- package/dist/templates/serializers.d.ts +2 -2
- package/dist/templates/sse.d.ts +20 -113
- package/dist/templates/standardSchema.d.ts +1 -1
- package/docs/guides/authentication.md +9 -1
- package/docs/guides/migrate-to-v6.md +139 -0
- package/docs/guides/validate-requests-and-responses.md +8 -3
- package/docs/guides/write-your-client.md +117 -11
- package/docs/options.md +2 -2
- package/docs/overview.md +1 -1
- package/kubb-extension.json +1 -1
- package/package.json +6 -5
- package/src/builders/callConfig.ts +80 -0
- package/src/builders/contentType.ts +35 -0
- package/src/builders/generics.ts +29 -0
- package/src/builders/operationBody.ts +43 -0
- package/src/builders/operationData.ts +64 -0
- package/src/builders/returnStatement.ts +103 -0
- package/src/builders/security.ts +125 -0
- package/src/builders/signature.ts +71 -0
- package/src/builders/styles.ts +94 -0
- package/src/builders/validator.ts +84 -0
- package/src/builders/validatorOptions.ts +53 -0
- package/src/clientTypes.ts +195 -0
- package/src/components/ClientRuntime.tsx +97 -0
- package/src/components/Operation.tsx +72 -0
- package/src/components/SdkClient.tsx +100 -0
- package/src/components/SdkFacade.tsx +35 -0
- package/src/generators/clientGenerator.tsx +91 -6
- package/src/generators/sdkGenerator.tsx +209 -0
- package/src/index.ts +3 -1
- package/src/macros.ts +9 -0
- package/src/plugin.ts +7 -8
- package/src/resolver.ts +28 -0
- package/src/setup.ts +90 -0
- package/src/types.ts +4 -2
- package/templates/client.ts +2 -1
- package/templates/core.ts +735 -286
- package/templates/mediaTypes/formData.ts +59 -0
- package/templates/mediaTypes/urlencoded.ts +33 -0
- package/templates/mediaTypes.ts +147 -0
- package/templates/serializers/cookies.ts +58 -0
- package/templates/serializers/headers.ts +35 -0
- package/templates/serializers/path.ts +19 -0
- package/templates/serializers/pathStyles.ts +27 -0
- package/templates/serializers/query.ts +21 -0
- package/templates/serializers/queryStyles.ts +107 -0
- package/templates/serializers.ts +48 -364
- package/templates/sse.ts +87 -178
- package/templates/standardSchema.ts +16 -37
- package/dist/client-BxO6Dygu.d.ts +0 -355
- package/dist/serializers-Ds8zZwOO.d.ts +0 -190
package/templates/serializers.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
61
|
+
export type PathStyle = 'simple' | 'label' | 'matrix'
|
|
62
|
+
|
|
63
|
+
export type PathParamStyle = SerializationStyle<PathStyle>
|
|
85
64
|
|
|
86
65
|
/**
|
|
87
|
-
*
|
|
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
|
|
68
|
+
export type CookieSerializer = (cookies: Record<string, unknown>, styles?: Record<string, CookieParamStyle>) => string
|
|
91
69
|
|
|
92
70
|
/**
|
|
93
|
-
*
|
|
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
|
|
73
|
+
export type HeaderSerializer = (headers: HeadersInit | undefined, styles?: Record<string, HeaderParamStyle>) => HeadersInit | undefined
|
|
97
74
|
|
|
98
75
|
/**
|
|
99
|
-
*
|
|
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
|
-
*
|
|
107
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
256
|
-
const
|
|
257
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
}
|