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