@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isEncodedBody } from '../mediaTypes.ts'
|
|
2
|
+
import type { MediaType } from '../mediaTypes.ts'
|
|
3
|
+
import { isJsonMediaType, jsonReplacer, notNullish, toValue } from '../serializers.ts'
|
|
4
|
+
import type { BodyEncoding } from '../serializers.ts'
|
|
5
|
+
|
|
6
|
+
function appendFormDataValue({
|
|
7
|
+
formData,
|
|
8
|
+
key,
|
|
9
|
+
value,
|
|
10
|
+
contentType,
|
|
11
|
+
}: {
|
|
12
|
+
formData: FormData
|
|
13
|
+
key: string
|
|
14
|
+
value: unknown
|
|
15
|
+
contentType?: string | undefined
|
|
16
|
+
}): void {
|
|
17
|
+
if (value === undefined || value === null) return
|
|
18
|
+
if (value instanceof Blob) {
|
|
19
|
+
formData.append(key, value)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
// A JSON part sends a scalar as JSON too, so a string arrives quoted.
|
|
23
|
+
if ((typeof value === 'object' && !(value instanceof Date)) || isJsonMediaType(contentType)) {
|
|
24
|
+
const encoded = JSON.stringify(value, jsonReplacer)
|
|
25
|
+
// A part's media type can only be set by wrapping the value in a typed Blob.
|
|
26
|
+
formData.append(key, contentType ? new Blob([encoded], { type: contentType }) : encoded)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
formData.append(key, toValue(value))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const formDelimiters: Partial<Record<string, string>> = { spaceDelimited: ' ', pipeDelimited: '|' }
|
|
33
|
+
|
|
34
|
+
function toFormData(body: Record<string, unknown>, encoding: Record<string, BodyEncoding> | undefined): FormData {
|
|
35
|
+
const formData = new FormData()
|
|
36
|
+
for (const [key, value] of Object.entries(body)) {
|
|
37
|
+
const { contentType, style, explode } = encoding?.[key] ?? {}
|
|
38
|
+
// `explode: false`, the default for the delimited styles, sends an array of scalars as one part, its items joined by the style's delimiter.
|
|
39
|
+
if (
|
|
40
|
+
Array.isArray(value) &&
|
|
41
|
+
!(explode ?? (style !== 'spaceDelimited' && style !== 'pipeDelimited')) &&
|
|
42
|
+
value.every((item) => typeof item !== 'object' || item === null || item instanceof Date)
|
|
43
|
+
) {
|
|
44
|
+
const delimiter = formDelimiters[style ?? 'form'] ?? ','
|
|
45
|
+
formData.append(key, value.filter(notNullish).map(toValue).join(delimiter))
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
for (const item of Array.isArray(value) ? value : [value]) appendFormDataValue({ formData, key, value: item, contentType })
|
|
49
|
+
}
|
|
50
|
+
return formData
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The `multipart/form-data` handler, which follows `config.styles.body`.
|
|
55
|
+
*/
|
|
56
|
+
export const multipartFormData: MediaType = {
|
|
57
|
+
encode: (body, { config }) => (isEncodedBody(body) ? body : toFormData(body as Record<string, unknown>, config.styles?.body)),
|
|
58
|
+
decode: (response) => response.formData(),
|
|
59
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isEncodedBody } from '../mediaTypes.ts'
|
|
2
|
+
import type { MediaType } from '../mediaTypes.ts'
|
|
3
|
+
import { isJsonMediaType, jsonReplacer, serializeDefaultQueryParam } from '../serializers.ts'
|
|
4
|
+
import type { BodyEncoding } from '../serializers.ts'
|
|
5
|
+
import { serializeStyledQueryParam } from '../serializers/queryStyles.ts'
|
|
6
|
+
|
|
7
|
+
function serializeUrlencodedBody(body: Record<string, unknown>, encoding: Record<string, BodyEncoding>): string {
|
|
8
|
+
const parts: Array<string> = []
|
|
9
|
+
for (const [key, value] of Object.entries(body)) {
|
|
10
|
+
const propertyEncoding = encoding[key]
|
|
11
|
+
// OpenAPI ignores `contentType` once a style field is set.
|
|
12
|
+
const styled = propertyEncoding?.style !== undefined || propertyEncoding?.explode !== undefined || propertyEncoding?.allowReserved !== undefined
|
|
13
|
+
if (value !== undefined && value !== null && !styled && isJsonMediaType(propertyEncoding?.contentType)) {
|
|
14
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value, jsonReplacer))}`)
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
parts.push(...(propertyEncoding ? serializeStyledQueryParam({ key, value, options: propertyEncoding }) : serializeDefaultQueryParam(key, value)))
|
|
18
|
+
}
|
|
19
|
+
return parts.join('&')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The `application/x-www-form-urlencoded` handler, which follows `config.styles.body`.
|
|
24
|
+
*/
|
|
25
|
+
export const urlencoded: MediaType = {
|
|
26
|
+
encode: (body, { config }) => {
|
|
27
|
+
if (isEncodedBody(body)) return body
|
|
28
|
+
const encoding = config.styles?.body
|
|
29
|
+
const encoded = serializeUrlencodedBody(body as Record<string, unknown>, encoding ?? {})
|
|
30
|
+
return encoding ? encoded : new URLSearchParams(encoded)
|
|
31
|
+
},
|
|
32
|
+
decode: (response) => response.formData(),
|
|
33
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { EventStreamOptions, RequestConfig } from './core.ts'
|
|
2
|
+
import { baseContentType, jsonReplacer } from './serializers.ts'
|
|
3
|
+
import type { RequestBody } from './serializers.ts'
|
|
4
|
+
import { ParseError } from './standardSchema.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a media type handler gets next to the body.
|
|
8
|
+
*/
|
|
9
|
+
export type MediaTypeContext = {
|
|
10
|
+
contentType: string
|
|
11
|
+
/**
|
|
12
|
+
* The response status, or `undefined` while a request body is encoded.
|
|
13
|
+
*/
|
|
14
|
+
status?: number | undefined
|
|
15
|
+
signal?: AbortSignal | undefined
|
|
16
|
+
config: Omit<RequestConfig, 'client' | 'transport'> & EventStreamOptions
|
|
17
|
+
/**
|
|
18
|
+
* Where a stream records the last event ID it read, for `StreamResult.lastEventId`.
|
|
19
|
+
*/
|
|
20
|
+
cursor?: { lastEventId: string } | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Encodes and decodes bodies of one media type. Every method is optional.
|
|
25
|
+
*/
|
|
26
|
+
export type MediaType = {
|
|
27
|
+
encode?: (body: unknown, context: MediaTypeContext) => RequestBody | undefined
|
|
28
|
+
decode?: (response: Response, context: MediaTypeContext) => unknown
|
|
29
|
+
/**
|
|
30
|
+
* Reads a 2xx body item by item. Each item is validated with `validator.response`.
|
|
31
|
+
*/
|
|
32
|
+
stream?: (body: ReadableStream<Uint8Array>, context: MediaTypeContext) => AsyncIterable<unknown>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handlers keyed by media type. A key may be a pattern such as `application/*+json` or `text/*`.
|
|
37
|
+
*/
|
|
38
|
+
export type MediaTypes = Record<string, MediaType>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Picks the handler for a `Content-Type`: the exact type, then its `+suffix`, then `type/*`, then the catch-all. With `use`, a
|
|
42
|
+
* handler without that method is skipped.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveMediaType({
|
|
45
|
+
mediaTypes,
|
|
46
|
+
contentType,
|
|
47
|
+
use,
|
|
48
|
+
}: {
|
|
49
|
+
mediaTypes: MediaTypes
|
|
50
|
+
contentType: string | null | undefined
|
|
51
|
+
use?: keyof MediaType
|
|
52
|
+
}): MediaType | undefined {
|
|
53
|
+
const base = baseContentType(contentType)
|
|
54
|
+
const [type, subtype = ''] = base?.split('/') ?? []
|
|
55
|
+
const suffix = subtype.includes('+') ? subtype.slice(subtype.lastIndexOf('+')) : undefined
|
|
56
|
+
const keys = base ? [base, suffix ? `${type}/*${suffix}` : undefined, `${type}/*`, '*/*'] : ['*/*']
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
const mediaType = key ? mediaTypes[key] : undefined
|
|
59
|
+
if (mediaType && (!use || mediaType[use])) return mediaType
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether a body is already encoded, so every built-in media type sends it as is.
|
|
66
|
+
*/
|
|
67
|
+
export function isEncodedBody(body: unknown): body is RequestBody {
|
|
68
|
+
return (
|
|
69
|
+
typeof body === 'string' ||
|
|
70
|
+
body instanceof Blob ||
|
|
71
|
+
body instanceof ArrayBuffer ||
|
|
72
|
+
ArrayBuffer.isView(body) ||
|
|
73
|
+
body instanceof FormData ||
|
|
74
|
+
body instanceof URLSearchParams ||
|
|
75
|
+
body instanceof ReadableStream
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function encodeOrJson(body: unknown, { contentType }: MediaTypeContext): RequestBody {
|
|
80
|
+
if (isEncodedBody(body)) return body
|
|
81
|
+
// XML, YAML, and CSV are never JSON, so a value for one must not go out as JSON under its label.
|
|
82
|
+
const mediaType = baseContentType(contentType)
|
|
83
|
+
if (mediaType && /[/+](xml|yaml|x-yaml|csv)$/.test(mediaType)) {
|
|
84
|
+
throw new TypeError(`Cannot encode the body as ${mediaType}, register mediaTypes['${mediaType}'].encode or pass the encoded body as a string.`)
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(body, jsonReplacer)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses JSON text, throwing a `ParseError` that names the content type when it is not valid JSON.
|
|
91
|
+
*/
|
|
92
|
+
export function parseJson(text: string, contentType: string | undefined): unknown {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new ParseError({ message: `Invalid JSON in a ${baseContentType(contentType)} body`, issues: [{ message: String(error) }], cause: error })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const json: MediaType = {
|
|
101
|
+
// A string is JSON-encoded too, so it arrives as a JSON string.
|
|
102
|
+
encode: (body) => (isEncodedBody(body) && typeof body !== 'string' ? body : JSON.stringify(body, jsonReplacer)),
|
|
103
|
+
decode: async (response, { contentType }) => {
|
|
104
|
+
const text = await response.text()
|
|
105
|
+
return text ? parseJson(text, contentType) : undefined
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const text: MediaType = {
|
|
110
|
+
encode: encodeOrJson,
|
|
111
|
+
decode: async (response) => (await response.text()) || undefined,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const binary: MediaType = {
|
|
115
|
+
encode: encodeOrJson,
|
|
116
|
+
decode: (response) => response.blob(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const catchAll: MediaType = {
|
|
120
|
+
encode: encodeOrJson,
|
|
121
|
+
// A server may label a plain message such as `Pet deleted` with any type, so non-JSON stays text.
|
|
122
|
+
decode: async (response) => {
|
|
123
|
+
const body = await response.text()
|
|
124
|
+
if (!body) return undefined
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(body)
|
|
127
|
+
} catch {
|
|
128
|
+
return body
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The media types every client knows: JSON, text, binary, and a catch-all that sends JSON and reads JSON or text.
|
|
135
|
+
*/
|
|
136
|
+
export const defaultMediaTypes: MediaTypes = {
|
|
137
|
+
'application/json': json,
|
|
138
|
+
'application/*+json': json,
|
|
139
|
+
'text/json': json,
|
|
140
|
+
'text/*': text,
|
|
141
|
+
'application/octet-stream': binary,
|
|
142
|
+
'application/pdf': binary,
|
|
143
|
+
'image/*': binary,
|
|
144
|
+
'audio/*': binary,
|
|
145
|
+
'video/*': binary,
|
|
146
|
+
'*/*': catchAll,
|
|
147
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isJsonMediaType, isRecord, jsonReplacer, notNullish, toValue } from '../serializers.ts'
|
|
2
|
+
import type { CookieSerializer } from '../serializers.ts'
|
|
3
|
+
|
|
4
|
+
// RFC 6265 cookie-octets, which the `cookie` style sends unescaped.
|
|
5
|
+
const cookieOctets = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*$/
|
|
6
|
+
// RFC 6265 cookie-name, an RFC 7230 token: an `=` would move the name/value split.
|
|
7
|
+
const cookieToken = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
|
|
8
|
+
|
|
9
|
+
function rawCookieValue(name: string, item: unknown): string {
|
|
10
|
+
const value = toValue(item)
|
|
11
|
+
// The message leaves the value out, since cookies often carry session tokens.
|
|
12
|
+
if (!cookieOctets.test(value)) throw new TypeError(`Cookie ${name} has characters the cookie style cannot send, escape it first.`)
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rawCookieName(name: string, key: string): string {
|
|
17
|
+
if (!cookieToken.test(key)) throw new TypeError(`Cookie ${name} has a key the cookie style cannot send as a cookie name, escape it first.`)
|
|
18
|
+
return key
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function serializeCookie({ name, value, explode, raw }: { name: string; value: unknown; explode: boolean; raw: boolean }): string {
|
|
22
|
+
const encode = (item: unknown) => (raw ? rawCookieValue(name, item) : encodeURIComponent(toValue(item)))
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
const items = value.filter(notNullish).map(encode)
|
|
25
|
+
return explode ? items.map((item) => `${name}=${item}`).join('; ') : `${name}=${items.join(',')}`
|
|
26
|
+
}
|
|
27
|
+
if (isRecord(value)) {
|
|
28
|
+
const entries = Object.entries(value).filter(([, item]) => notNullish(item))
|
|
29
|
+
// An exploded key becomes a cookie name, so the cookie style checks it as a token.
|
|
30
|
+
if (explode) return entries.map(([key, item]) => `${raw ? rawCookieName(name, key) : encodeURIComponent(key)}=${encode(item)}`).join('; ')
|
|
31
|
+
return `${name}=${entries
|
|
32
|
+
.flatMap(([key, item]) => [key, item])
|
|
33
|
+
.map(encode)
|
|
34
|
+
.join(',')}`
|
|
35
|
+
}
|
|
36
|
+
return `${name}=${encode(value)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Joins cookie parameters into a `Cookie` header value with the OpenAPI `form` style, or the `cookie` style when a parameter sets it.
|
|
41
|
+
*/
|
|
42
|
+
export const serializeCookies: CookieSerializer = (cookies, styles) => {
|
|
43
|
+
const parts: Array<string> = []
|
|
44
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
45
|
+
if (value === undefined || value === null) continue
|
|
46
|
+
const style = styles?.[name]
|
|
47
|
+
if (isJsonMediaType(style?.contentType)) {
|
|
48
|
+
parts.push(`${name}=${encodeURIComponent(JSON.stringify(value, jsonReplacer))}`)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
// OpenAPI 3.2 defaults `explode` to true for the `cookie` style.
|
|
52
|
+
const raw = style?.style === 'cookie'
|
|
53
|
+
const cookie = serializeCookie({ name, value, explode: style?.explode ?? raw, raw })
|
|
54
|
+
// An empty exploded array or object has no pairs, so it adds no `; ` segment.
|
|
55
|
+
if (cookie) parts.push(cookie)
|
|
56
|
+
}
|
|
57
|
+
return parts.join('; ')
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isJsonMediaType, isRecord, jsonReplacer, notNullish, toValue } from '../serializers.ts'
|
|
2
|
+
import type { HeaderSerializer, HeaderValue } from '../serializers.ts'
|
|
3
|
+
|
|
4
|
+
function serializeHeaderValue(value: unknown, explode: boolean): string {
|
|
5
|
+
if (Array.isArray(value)) return value.filter(notNullish).map(toValue).join(',')
|
|
6
|
+
if (!isRecord(value)) return toValue(value)
|
|
7
|
+
const entries = Object.entries(value).filter(([, item]) => notNullish(item))
|
|
8
|
+
if (explode) return entries.map(([key, item]) => `${key}=${toValue(item)}`).join(',')
|
|
9
|
+
return entries
|
|
10
|
+
.flatMap(([key, item]) => [key, item])
|
|
11
|
+
.map(toValue)
|
|
12
|
+
.join(',')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Header values must be Latin-1, so non-ASCII characters become JSON escapes.
|
|
16
|
+
function toAsciiJson(value: unknown): string {
|
|
17
|
+
return JSON.stringify(value, jsonReplacer).replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serializes array and object headers with the OpenAPI `simple` style.
|
|
22
|
+
*/
|
|
23
|
+
export const serializeHeaders: HeaderSerializer = (headers, styles) => {
|
|
24
|
+
// A `Headers` instance only holds strings, so there is nothing to style.
|
|
25
|
+
if (!headers || headers instanceof Headers) return headers
|
|
26
|
+
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
27
|
+
return entries.map(([key, value]) => {
|
|
28
|
+
// Without an entry the parameter uses the OpenAPI default: `simple`, no explode.
|
|
29
|
+
const style = styles?.[key] ?? {}
|
|
30
|
+
if (value === undefined || value === null) return [key, value] as [string, HeaderValue]
|
|
31
|
+
if (isJsonMediaType(style.contentType)) return [key, toAsciiJson(value)] as [string, HeaderValue]
|
|
32
|
+
if (typeof value !== 'object') return [key, value] as [string, HeaderValue]
|
|
33
|
+
return [key, serializeHeaderValue(value, style.explode ?? false)] as [string, HeaderValue]
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { encodeComponent, isRecord } from '../serializers.ts'
|
|
2
|
+
import type { PathSerializer } from '../serializers.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The path serializer for the `simple` style, which `serializeStyledPath` also uses.
|
|
6
|
+
*/
|
|
7
|
+
export const serializePath: PathSerializer = ({ name, value, options }) => {
|
|
8
|
+
if (value === undefined || value === null) return ''
|
|
9
|
+
if (options?.style && options.style !== 'simple') {
|
|
10
|
+
throw new TypeError(`Cannot send path parameter ${name} with the ${options.style} style, register serializer.path on the client.`)
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value)) return value.map(encodeComponent).join(',')
|
|
13
|
+
if (isRecord(value)) {
|
|
14
|
+
return Object.entries(value)
|
|
15
|
+
.map(([key, item]) => `${encodeComponent(key)}${options?.explode ? '=' : ','}${encodeComponent(item)}`)
|
|
16
|
+
.join(',')
|
|
17
|
+
}
|
|
18
|
+
return encodeComponent(value)
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { encodeComponent as encode, isRecord } from '../serializers.ts'
|
|
2
|
+
import type { PathSerializer } from '../serializers.ts'
|
|
3
|
+
import { serializePath } from './path.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The path serializer for the OpenAPI `label` and `matrix` styles. A `simple` parameter uses `serializePath`.
|
|
7
|
+
*/
|
|
8
|
+
export const serializeStyledPath: PathSerializer = ({ name, value, options }) => {
|
|
9
|
+
const style = options?.style ?? 'simple'
|
|
10
|
+
if (style === 'simple' || value === undefined || value === null) return serializePath({ name, value, options })
|
|
11
|
+
const explode = options?.explode ?? false
|
|
12
|
+
const label = style === 'label'
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
const items = value.map(encode)
|
|
15
|
+
if (label) return `.${items.join(explode ? '.' : ',')}`
|
|
16
|
+
return explode ? items.map((item) => `;${name}=${item}`).join('') : `;${name}=${items.join(',')}`
|
|
17
|
+
}
|
|
18
|
+
if (isRecord(value)) {
|
|
19
|
+
const members = Object.entries(value).map(([key, item]) => `${encode(key)}${explode ? '=' : ','}${encode(item)}`)
|
|
20
|
+
if (label) return `.${members.join(explode ? '.' : ',')}`
|
|
21
|
+
return explode ? members.map((member) => `;${member}`).join('') : `;${name}=${members.join(',')}`
|
|
22
|
+
}
|
|
23
|
+
const encoded = encode(value)
|
|
24
|
+
// RFC 6570 drops the `=` of an empty matrix value: `;color`.
|
|
25
|
+
if (label) return `.${encoded}`
|
|
26
|
+
return encoded ? `;${name}=${encoded}` : `;${name}`
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { encodeComponent, isRecord, notNullish, serializeDefaultQueryParam } from '../serializers.ts'
|
|
2
|
+
import type { QuerySerializer } from '../serializers.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The query serializer for a spec without styled query parameters: arrays repeat their key, objects use `form` with explode.
|
|
6
|
+
*/
|
|
7
|
+
export const serializeQuery: QuerySerializer = (params, options) => {
|
|
8
|
+
const parts: Array<string> = []
|
|
9
|
+
for (const [key, value] of Object.entries(params)) {
|
|
10
|
+
if (options?.[key]) throw new TypeError(`Cannot send query parameter ${key} with a style, register serializer.query on the client.`)
|
|
11
|
+
// `URLSearchParams` writes a space as `+`, form encoding for bodies. A query follows RFC 6570: `%20`.
|
|
12
|
+
parts.push(
|
|
13
|
+
...(isRecord(value)
|
|
14
|
+
? Object.entries(value)
|
|
15
|
+
.filter(([, item]) => notNullish(item))
|
|
16
|
+
.map(([prop, item]) => `${encodeComponent(prop)}=${encodeComponent(item)}`)
|
|
17
|
+
: serializeDefaultQueryParam(key, value).map((part) => part.replaceAll('+', '%20'))),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return parts.join('&')
|
|
21
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { baseContentType, encodeComponent, isJsonMediaType, isRecord, jsonReplacer, notNullish, serializeDefaultQueryParam, toValue } from '../serializers.ts'
|
|
2
|
+
import type { QueryParamStyle, QuerySerializer, QueryStyle } from '../serializers.ts'
|
|
3
|
+
|
|
4
|
+
const queryDelimiters: Record<QueryStyle, string> = { form: ',', spaceDelimited: '%20', pipeDelimited: '%7C', deepObject: ',' }
|
|
5
|
+
|
|
6
|
+
// `allowReserved` keeps reserved characters and percent-encoded triples, but `#`, `&`, and `+` would end the query or change a form value.
|
|
7
|
+
function encodeReserved(value: unknown): string {
|
|
8
|
+
return encodeURI(toValue(value))
|
|
9
|
+
.replace(/%25([0-9A-Fa-f]{2})/g, '%$1')
|
|
10
|
+
.replace(/[#&+]/g, encodeURIComponent)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// OpenAPI defaults `explode` to false for the delimited styles. A deepObject array of scalars, which OpenAPI leaves undefined, keeps repeating its key.
|
|
14
|
+
function defaultExplode(options: QueryParamStyle): boolean {
|
|
15
|
+
return options.style !== 'spaceDelimited' && options.style !== 'pipeDelimited'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function serializeDeepObject({ key, value, encode }: { key: string; value: unknown; encode: (value: unknown) => string }): Array<string> {
|
|
19
|
+
if (value === undefined || value === null) return []
|
|
20
|
+
if (Array.isArray(value)) return value.flatMap((item, index) => serializeDeepObject({ key: `${key}[${index}]`, value: item, encode }))
|
|
21
|
+
if (isRecord(value)) {
|
|
22
|
+
return Object.entries(value)
|
|
23
|
+
.filter(([, item]) => notNullish(item))
|
|
24
|
+
.flatMap(([prop, item]) => serializeDeepObject({ key: `${key}[${prop}]`, value: item, encode }))
|
|
25
|
+
}
|
|
26
|
+
return [`${encode(key)}=${encode(value)}`]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function serializeStyledQueryArray({
|
|
30
|
+
key,
|
|
31
|
+
value,
|
|
32
|
+
options,
|
|
33
|
+
encode,
|
|
34
|
+
}: {
|
|
35
|
+
key: string
|
|
36
|
+
value: Array<unknown>
|
|
37
|
+
options: QueryParamStyle
|
|
38
|
+
encode: (value: unknown) => string
|
|
39
|
+
}): Array<string> {
|
|
40
|
+
const items = value.filter(notNullish)
|
|
41
|
+
if (options.explode ?? defaultExplode(options)) return items.map((item) => `${encode(key)}=${encode(item)}`)
|
|
42
|
+
return [`${encode(key)}=${items.map(encode).join(queryDelimiters[options.style ?? 'form'])}`]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function serializeStyledQueryObject({
|
|
46
|
+
key,
|
|
47
|
+
value,
|
|
48
|
+
options,
|
|
49
|
+
encode,
|
|
50
|
+
}: {
|
|
51
|
+
key: string
|
|
52
|
+
value: Record<string, unknown>
|
|
53
|
+
options: QueryParamStyle
|
|
54
|
+
encode: (value: unknown) => string
|
|
55
|
+
}): Array<string> {
|
|
56
|
+
if ((options.style ?? 'form') === 'deepObject') return serializeDeepObject({ key, value, encode })
|
|
57
|
+
const entries = Object.entries(value).filter(([, item]) => notNullish(item))
|
|
58
|
+
if (options.explode ?? defaultExplode(options)) return entries.map(([prop, item]) => `${encode(prop)}=${encode(item)}`)
|
|
59
|
+
return [
|
|
60
|
+
`${encode(key)}=${entries
|
|
61
|
+
.flatMap(([prop, item]) => [prop, item])
|
|
62
|
+
.map(encode)
|
|
63
|
+
.join(queryDelimiters[options.style ?? 'form'])}`,
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One query parameter or form field with its OpenAPI style, as encoded pairs.
|
|
69
|
+
*/
|
|
70
|
+
export function serializeStyledQueryParam({ key, value, options }: { key: string; value: unknown; options: QueryParamStyle }): Array<string> {
|
|
71
|
+
if (value === undefined || value === null) return []
|
|
72
|
+
const encode = options.allowReserved ? encodeReserved : encodeComponent
|
|
73
|
+
// OpenAPI leaves arrays under deepObject undefined. Objects in one are indexed, `items[0][price]=p`, as nested arrays are.
|
|
74
|
+
if (Array.isArray(value) && options.style === 'deepObject' && value.some(isRecord)) return serializeDeepObject({ key, value, encode })
|
|
75
|
+
if (Array.isArray(value)) return serializeStyledQueryArray({ key, value, options, encode })
|
|
76
|
+
if (isRecord(value)) return serializeStyledQueryObject({ key, value, options, encode })
|
|
77
|
+
return [`${encode(key)}=${encode(value)}`]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The query serializer for a spec with styled query parameters: an OpenAPI style, `allowReserved` or a JSON content
|
|
82
|
+
* type. Without a style entry, arrays repeat their key and objects use `form` with explode.
|
|
83
|
+
*/
|
|
84
|
+
export const serializeStyledQuery: QuerySerializer = (params, options) => {
|
|
85
|
+
const parts: Array<string> = []
|
|
86
|
+
for (const [key, value] of Object.entries(params)) {
|
|
87
|
+
const paramOptions = options?.[key]
|
|
88
|
+
if (notNullish(value) && isJsonMediaType(paramOptions?.contentType)) {
|
|
89
|
+
const json = encodeComponent(JSON.stringify(value, jsonReplacer))
|
|
90
|
+
parts.push(paramOptions?.querystring ? json : `${encodeComponent(key)}=${json}`)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
// A string querystring is already in its media type, so it only needs percent-encoding, which form-urlencoded has.
|
|
94
|
+
if (paramOptions?.querystring && typeof value === 'string') {
|
|
95
|
+
parts.push(baseContentType(paramOptions.contentType) === 'application/x-www-form-urlencoded' ? value : encodeComponent(value))
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const styled = paramOptions ?? (isRecord(value) ? {} : undefined)
|
|
99
|
+
// `URLSearchParams` writes a space as `+`, form encoding for bodies. A query follows RFC 6570, as a styled one does: `%20`.
|
|
100
|
+
parts.push(
|
|
101
|
+
...(styled
|
|
102
|
+
? serializeStyledQueryParam({ key, value, options: styled })
|
|
103
|
+
: serializeDefaultQueryParam(key, value).map((part) => part.replaceAll('+', '%20'))),
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return parts.join('&')
|
|
107
|
+
}
|