@kubb/plugin-axios 5.9.0-canary.20261024T172532 → 5.9.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/dist/index.cjs +103 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +103 -58
- package/dist/index.js.map +1 -1
- package/dist/templates/axios.d.ts +12 -4
- package/dist/templates/standardSchema.d.ts +14 -0
- package/docs/options.md +16 -10
- package/package.json +5 -5
- package/templates/axios.ts +54 -15
- package/templates/standardSchema.ts +55 -0
|
@@ -162,16 +162,19 @@ export type ContentType = string | {
|
|
|
162
162
|
* A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
|
|
163
163
|
* it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
|
|
164
164
|
* `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
|
|
165
|
-
* non-2xx call does not throw).
|
|
165
|
+
* non-2xx call does not throw), and `validator.path` / `validator.query` / `validator.headers` for the
|
|
166
|
+
* request params.
|
|
166
167
|
*/
|
|
167
168
|
export type Validator<T = unknown> = StandardSchemaValidator<T>;
|
|
168
169
|
/**
|
|
169
|
-
* The failing
|
|
170
|
-
* `direction` says which
|
|
170
|
+
* The failing value and the call it came from, handed to `onValidationError` alongside the `ParseError`.
|
|
171
|
+
* `direction` says which side rejected it: the request, the success body, or the error body. `source`
|
|
172
|
+
* says which part of it: the body, or one of the request param groups.
|
|
171
173
|
*/
|
|
172
174
|
export type ValidationErrorContext = {
|
|
173
175
|
value: unknown;
|
|
174
176
|
direction: 'request' | 'response' | 'error';
|
|
177
|
+
source: 'body' | 'path' | 'query' | 'headers';
|
|
175
178
|
method?: string;
|
|
176
179
|
url?: string;
|
|
177
180
|
status?: number;
|
|
@@ -194,6 +197,7 @@ export type Auth = {
|
|
|
194
197
|
scheme?: 'bearer' | 'basic';
|
|
195
198
|
name?: string;
|
|
196
199
|
in?: 'header' | 'query' | 'cookie';
|
|
200
|
+
scopes?: Array<string>;
|
|
197
201
|
};
|
|
198
202
|
/**
|
|
199
203
|
* The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
|
|
@@ -233,6 +237,9 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
|
|
|
233
237
|
codecs?: Record<string, Codec>;
|
|
234
238
|
validator?: {
|
|
235
239
|
request?: Validator;
|
|
240
|
+
path?: Validator;
|
|
241
|
+
query?: Validator;
|
|
242
|
+
headers?: Validator;
|
|
236
243
|
response?: Validator;
|
|
237
244
|
error?: Validator;
|
|
238
245
|
};
|
|
@@ -291,6 +298,7 @@ export type Interceptors<TRequest = AxiosRequestConfig, TResponse = AxiosRespons
|
|
|
291
298
|
response: InterceptorChannel<AxiosResponse, TRequest, TResponse>;
|
|
292
299
|
error: InterceptorChannel<AxiosError, TRequest, TResponse>;
|
|
293
300
|
};
|
|
301
|
+
export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>;
|
|
294
302
|
/**
|
|
295
303
|
* A client instance: the callable send plus configuration, interceptors, and an isolated
|
|
296
304
|
* `createClient` factory.
|
|
@@ -299,7 +307,7 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
|
|
|
299
307
|
<TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>;
|
|
300
308
|
getConfig: () => ClientConfig;
|
|
301
309
|
setConfig: (config: ClientConfig) => ClientConfig;
|
|
302
|
-
getUrl:
|
|
310
|
+
getUrl: (config: UrlConfig) => string;
|
|
303
311
|
interceptors: Interceptors<TRequest, TResponse>;
|
|
304
312
|
createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>;
|
|
305
313
|
};
|
|
@@ -48,4 +48,18 @@ export declare class ParseError extends Error {
|
|
|
48
48
|
* const pet = await validateStandardSchema(PetSchema, rawData)
|
|
49
49
|
*/
|
|
50
50
|
export declare function validateStandardSchema<TOutput>(schema: StandardSchemaValidator<TOutput>, value: unknown): Promise<TOutput>;
|
|
51
|
+
/**
|
|
52
|
+
* Wraps an object schema so its keys match case-insensitively.
|
|
53
|
+
* Keys the schema does not declare pass through untouched.
|
|
54
|
+
* The declared keys are validated with `schema` under their declared names.
|
|
55
|
+
* Every key, and every issue path, keeps the casing the caller used.
|
|
56
|
+
*
|
|
57
|
+
* @param schema Standard Schema-compatible object schema.
|
|
58
|
+
* @param entries The record of keys the passed object schema validates.
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* validator: { headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape) }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function toHeadersStandardSchema(schema: StandardSchemaValidator, entries: Record<string, unknown>): StandardSchemaValidator;
|
|
51
65
|
//#endregion
|
package/docs/options.md
CHANGED
|
@@ -10,7 +10,7 @@ Pass these options to `pluginAxios()`. Shared options link to [Shared plugin opt
|
|
|
10
10
|
| [`group`](https://kubb.dev/docs/5.x/reference/plugin-options#group) | Split output by tag or path: `type`, `name`. | None |
|
|
11
11
|
| [`baseURL`](#baseurl) | Base URL prepended to every request. | None |
|
|
12
12
|
| [`throwOnErrorDefault`](#throwonerrordefault) | Default error behavior and return type for generated operations. | `true` |
|
|
13
|
-
| [`validator`](#validator) | Validate request and
|
|
13
|
+
| [`validator`](#validator) | Validate request bodies, params, and responses with Zod. | `false` |
|
|
14
14
|
| [`sdk`](#sdk) | Emit a class-based SDK instead of standalone functions. | None |
|
|
15
15
|
| [`returnType`](#returntype) | Shape of the value a generated call resolves to. | `'full'` |
|
|
16
16
|
| [`include`](https://kubb.dev/docs/5.x/reference/plugin-options#include) | Keep only operations that match. | None |
|
|
@@ -48,13 +48,13 @@ if (result.status !== 200) console.error(result.status, result.error)
|
|
|
48
48
|
|
|
49
49
|
### validator
|
|
50
50
|
|
|
51
|
-
Validates request and response bodies with schemas from `@kubb/plugin-zod`, so add `pluginZod()` too. An invalid
|
|
51
|
+
Validates request bodies, path, query, and header params, and response bodies with schemas from `@kubb/plugin-zod`, so add `pluginZod()` too. An invalid value makes the generated function throw a `ParseError`.
|
|
52
52
|
|
|
53
|
-
| |
|
|
54
|
-
| -------- |
|
|
55
|
-
| Type | `false \| 'zod' \| { request?: 'zod'; response?: 'zod' }` |
|
|
56
|
-
| Required | `false`
|
|
57
|
-
| Default | `false`
|
|
53
|
+
| | |
|
|
54
|
+
| -------- | ------------------------------------------------------------------------- |
|
|
55
|
+
| Type | `false \| 'zod' \| { request?: 'zod'; params?: 'zod'; response?: 'zod' }` |
|
|
56
|
+
| Required | `false` |
|
|
57
|
+
| Default | `false` |
|
|
58
58
|
|
|
59
59
|
::field-group
|
|
60
60
|
|
|
@@ -66,8 +66,8 @@ Default value. Skips validation and casts the response to the generated type.
|
|
|
66
66
|
Validates the success response body and, when a non-2xx call does not throw, the error body.
|
|
67
67
|
:::
|
|
68
68
|
|
|
69
|
-
:::field{name="{ request?: 'zod', response?: 'zod' }"}
|
|
70
|
-
Enables validation separately for
|
|
69
|
+
:::field{name="{ request?: 'zod', params?: 'zod', response?: 'zod' }"}
|
|
70
|
+
Enables validation separately for request bodies, params, and responses.
|
|
71
71
|
:::
|
|
72
72
|
|
|
73
73
|
::
|
|
@@ -84,11 +84,17 @@ export default defineConfig({
|
|
|
84
84
|
plugins: [
|
|
85
85
|
pluginTs(),
|
|
86
86
|
pluginZod(),
|
|
87
|
-
pluginAxios({ validator: { request: 'zod', response: 'zod' } }),
|
|
87
|
+
pluginAxios({ validator: { request: 'zod', params: 'zod', response: 'zod' } }),
|
|
88
88
|
],
|
|
89
89
|
})
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
`params` runs each group through its generated `<operation>PathSchema`, `QuerySchema`, or `HeadersSchema`, and sends what the schema returns. A codec param such as a `Date` goes out in its wire form, and an invalid param fails before the request. Values added by `auth` are not validated.
|
|
93
|
+
|
|
94
|
+
Only the params your OpenAPI spec declares are sent. Path and query keys the spec does not declare are stripped, so a key such as an undocumented filter or a cache-buster never reaches the URL. Declare it in the spec, or leave `params` off. Headers are the exception: client-level `headers` (such as `Authorization` or tracing headers) are merged with the call's headers before validation, so headers the spec does not declare pass through unchanged, and declared header params match in any casing.
|
|
95
|
+
|
|
96
|
+
Pair `request` and `params` with `pluginZod({ inferred: 'direction' })` when the schemas use codecs, so the request types accept the domain values (such as a `Date`) that the validators encode. See [`inferred`](https://kubb.dev/extensions/kubb-labs/plugin-zod/options#inferred).
|
|
97
|
+
|
|
92
98
|
### sdk
|
|
93
99
|
|
|
94
100
|
Generates a class-based SDK. Each instance takes its own client config, so environments can use separate clients. Leave `sdk` unset to keep the standalone functions that query plugins call.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-axios",
|
|
3
|
-
"version": "5.9.0
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Generate a slim, type-safe HTTP client with Kubb based on axios.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -80,12 +80,12 @@
|
|
|
80
80
|
"registry": "https://registry.npmjs.org/"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@kubb/plugin-ts": "^5.
|
|
84
|
-
"@kubb/plugin-zod": "^5.7.0
|
|
83
|
+
"@kubb/plugin-ts": "^5.1.0",
|
|
84
|
+
"@kubb/plugin-zod": "^5.7.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@internals/client": "0.
|
|
88
|
-
"@internals/shared": "0.
|
|
87
|
+
"@internals/client": "0.0.0",
|
|
88
|
+
"@internals/shared": "0.0.0",
|
|
89
89
|
"axios": "^1.20.0",
|
|
90
90
|
"kubb": "^5.13.0",
|
|
91
91
|
"typescript": "^7.0.2"
|
package/templates/axios.ts
CHANGED
|
@@ -165,17 +165,20 @@ export type ContentType = string | { request?: string; response?: string }
|
|
|
165
165
|
* A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
|
|
166
166
|
* it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
|
|
167
167
|
* `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
|
|
168
|
-
* non-2xx call does not throw).
|
|
168
|
+
* non-2xx call does not throw), and `validator.path` / `validator.query` / `validator.headers` for the
|
|
169
|
+
* request params.
|
|
169
170
|
*/
|
|
170
171
|
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
|
-
* The failing
|
|
174
|
-
* `direction` says which
|
|
174
|
+
* The failing value and the call it came from, handed to `onValidationError` alongside the `ParseError`.
|
|
175
|
+
* `direction` says which side rejected it: the request, the success body, or the error body. `source`
|
|
176
|
+
* says which part of it: the body, or one of the request param groups.
|
|
175
177
|
*/
|
|
176
178
|
export type ValidationErrorContext = {
|
|
177
179
|
value: unknown
|
|
178
180
|
direction: 'request' | 'response' | 'error'
|
|
181
|
+
source: 'body' | 'path' | 'query' | 'headers'
|
|
179
182
|
method?: string
|
|
180
183
|
url?: string
|
|
181
184
|
status?: number
|
|
@@ -196,6 +199,7 @@ export type Auth = {
|
|
|
196
199
|
scheme?: 'bearer' | 'basic'
|
|
197
200
|
name?: string
|
|
198
201
|
in?: 'header' | 'query' | 'cookie'
|
|
202
|
+
scopes?: Array<string>
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
/**
|
|
@@ -237,7 +241,7 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
|
|
|
237
241
|
transport?: AxiosInstance
|
|
238
242
|
serializer?: Serializers
|
|
239
243
|
codecs?: Record<string, Codec>
|
|
240
|
-
validator?: { request?: Validator; response?: Validator; error?: Validator }
|
|
244
|
+
validator?: { request?: Validator; path?: Validator; query?: Validator; headers?: Validator; response?: Validator; error?: Validator }
|
|
241
245
|
onValidationError?: ValidationErrorHandler
|
|
242
246
|
security?: Array<Auth>
|
|
243
247
|
auth?: AuthResolver
|
|
@@ -307,6 +311,8 @@ export type Interceptors<TRequest = AxiosRequestConfig, TResponse = AxiosRespons
|
|
|
307
311
|
error: InterceptorChannel<AxiosError, TRequest, TResponse>
|
|
308
312
|
}
|
|
309
313
|
|
|
314
|
+
export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>
|
|
315
|
+
|
|
310
316
|
/**
|
|
311
317
|
* A client instance: the callable send plus configuration, interceptors, and an isolated
|
|
312
318
|
* `createClient` factory.
|
|
@@ -315,7 +321,7 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
|
|
|
315
321
|
<TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
|
|
316
322
|
getConfig: () => ClientConfig
|
|
317
323
|
setConfig: (config: ClientConfig) => ClientConfig
|
|
318
|
-
getUrl:
|
|
324
|
+
getUrl: (config: UrlConfig) => string
|
|
319
325
|
interceptors: Interceptors<TRequest, TResponse>
|
|
320
326
|
createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>
|
|
321
327
|
}
|
|
@@ -365,9 +371,14 @@ export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TRes
|
|
|
365
371
|
|
|
366
372
|
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
367
373
|
|
|
374
|
+
function headerEntries(headers: object): Array<[string, unknown]> {
|
|
375
|
+
if (Array.isArray(headers)) return headers
|
|
376
|
+
return headers instanceof Headers ? [...headers] : Object.entries(headers)
|
|
377
|
+
}
|
|
378
|
+
|
|
368
379
|
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
369
380
|
if (!headers) return {}
|
|
370
|
-
const entries =
|
|
381
|
+
const entries = headerEntries(headers)
|
|
371
382
|
const result: Record<string, string> = {}
|
|
372
383
|
for (const [key, value] of entries) {
|
|
373
384
|
if (value === undefined || value === null) continue
|
|
@@ -376,8 +387,22 @@ function serializeHeaders(headers: HeadersInit | undefined): Record<string, stri
|
|
|
376
387
|
return result
|
|
377
388
|
}
|
|
378
389
|
|
|
379
|
-
function
|
|
380
|
-
|
|
390
|
+
function collectHeadersCaseInsensitive(...sources: Array<unknown>): Record<string, unknown> {
|
|
391
|
+
const keys = new Map<string, string>()
|
|
392
|
+
const result: Record<string, unknown> = {}
|
|
393
|
+
for (const source of sources) {
|
|
394
|
+
if (!source) continue
|
|
395
|
+
const entries = headerEntries(source)
|
|
396
|
+
for (const [key, value] of entries) {
|
|
397
|
+
if (value === undefined || value === null) continue
|
|
398
|
+
const lowerCase = key.toLowerCase()
|
|
399
|
+
const previous = keys.get(lowerCase)
|
|
400
|
+
if (previous !== undefined) delete result[previous]
|
|
401
|
+
keys.set(lowerCase, key)
|
|
402
|
+
result[key] = value
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return result
|
|
381
406
|
}
|
|
382
407
|
|
|
383
408
|
function getHeader(headers: Record<string, string>, name: string): string | undefined {
|
|
@@ -585,15 +610,29 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
585
610
|
}): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record<string, Codec>; throwOnError: boolean }> {
|
|
586
611
|
const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
587
612
|
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
613
|
+
const onValidationError = requestConfig.onValidationError ?? config.onValidationError
|
|
614
|
+
const validationContext = { direction: 'request', method: requestConfig.method, url: requestConfig.url } as const
|
|
615
|
+
|
|
616
|
+
// Params are encoded before anything reads them: headers before styling, query before auth adds its values.
|
|
617
|
+
const paramValues = {
|
|
618
|
+
path: { ...(requestConfig.path as Record<string, unknown> | undefined) },
|
|
619
|
+
query: { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) },
|
|
620
|
+
headers: collectHeadersCaseInsensitive(config.headers, requestConfig.headers),
|
|
621
|
+
}
|
|
622
|
+
const [validatedPath, validatedQuery, validatedHeaders] = await Promise.all(
|
|
623
|
+
(['path', 'query', 'headers'] as const).map((source) =>
|
|
624
|
+
runValidator({ validator: requestConfig.validator?.[source], value: paramValues[source], context: { ...validationContext, source }, onValidationError }),
|
|
625
|
+
),
|
|
626
|
+
)
|
|
588
627
|
|
|
589
|
-
const headers =
|
|
628
|
+
const headers = serializeHeaders(applyHeaderStyles(validatedHeaders as HeadersInit | undefined, requestConfig.styles?.header))
|
|
590
629
|
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
591
630
|
const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
|
|
592
631
|
if (responseContentType && !hasHeader(headers, 'accept')) {
|
|
593
632
|
headers['Accept'] = responseContentType
|
|
594
633
|
}
|
|
595
634
|
|
|
596
|
-
const query: Record<string, unknown> = { ...(
|
|
635
|
+
const query: Record<string, unknown> = { ...(validatedQuery as Record<string, unknown> | undefined) }
|
|
597
636
|
|
|
598
637
|
await resolveAuth({
|
|
599
638
|
security: requestConfig.security,
|
|
@@ -610,8 +649,8 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
610
649
|
const validatedBody = await runValidator({
|
|
611
650
|
validator: requestConfig.validator?.request,
|
|
612
651
|
value: requestConfig.body,
|
|
613
|
-
context: {
|
|
614
|
-
onValidationError
|
|
652
|
+
context: { ...validationContext, source: 'body' },
|
|
653
|
+
onValidationError,
|
|
615
654
|
})
|
|
616
655
|
const requestContentTypeBase = baseContentType(requestContentType)
|
|
617
656
|
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
@@ -632,7 +671,7 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
632
671
|
headers['Content-Type'] = 'application/json'
|
|
633
672
|
}
|
|
634
673
|
|
|
635
|
-
const pathParams = (
|
|
674
|
+
const pathParams = (validatedPath ?? {}) as Record<string, unknown>
|
|
636
675
|
const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
637
676
|
pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
|
|
638
677
|
)
|
|
@@ -700,7 +739,7 @@ async function settleResponse<TRequest, TResponse>({
|
|
|
700
739
|
? await runValidator({
|
|
701
740
|
validator: validator?.response,
|
|
702
741
|
value: decoded,
|
|
703
|
-
context: { direction: 'response', ...validationContext },
|
|
742
|
+
context: { direction: 'response', source: 'body', ...validationContext },
|
|
704
743
|
onValidationError,
|
|
705
744
|
})
|
|
706
745
|
: undefined
|
|
@@ -709,7 +748,7 @@ async function settleResponse<TRequest, TResponse>({
|
|
|
709
748
|
: await runValidator({
|
|
710
749
|
validator: validator?.error,
|
|
711
750
|
value: decoded,
|
|
712
|
-
context: { direction: 'error', ...validationContext },
|
|
751
|
+
context: { direction: 'error', source: 'body', ...validationContext },
|
|
713
752
|
onValidationError,
|
|
714
753
|
})
|
|
715
754
|
return {
|
|
@@ -52,3 +52,58 @@ export async function validateStandardSchema<TOutput>(schema: StandardSchemaVali
|
|
|
52
52
|
}
|
|
53
53
|
return (result as { value: TOutput }).value
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Wraps an object schema so its keys match case-insensitively.
|
|
58
|
+
* Keys the schema does not declare pass through untouched.
|
|
59
|
+
* The declared keys are validated with `schema` under their declared names.
|
|
60
|
+
* Every key, and every issue path, keeps the casing the caller used.
|
|
61
|
+
*
|
|
62
|
+
* @param schema Standard Schema-compatible object schema.
|
|
63
|
+
* @param entries The record of keys the passed object schema validates.
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* validator: { headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape) }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function toHeadersStandardSchema(schema: StandardSchemaValidator, entries: Record<string, unknown>): StandardSchemaValidator {
|
|
70
|
+
const declared = new Map(Object.keys(entries).map((name) => [name.toLowerCase(), name]))
|
|
71
|
+
|
|
72
|
+
function validate(value: unknown): StandardSchemaResult<unknown> | Promise<StandardSchemaResult<unknown>> {
|
|
73
|
+
if (typeof value !== 'object' || value === null) {
|
|
74
|
+
return { issues: [{ message: 'Expected an object' }] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const originalKeys = new Map<string, string>()
|
|
78
|
+
const declaredValues: Record<string, unknown> = {}
|
|
79
|
+
const undeclaredValues: Record<string, unknown> = {}
|
|
80
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
81
|
+
const name = declared.get(key.toLowerCase())
|
|
82
|
+
if (name === undefined) {
|
|
83
|
+
undeclaredValues[key] = entry
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
originalKeys.set(name, key)
|
|
87
|
+
declaredValues[name] = entry
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const toOriginal = (key: string) => originalKeys.get(key) ?? key
|
|
91
|
+
const settle = (result: StandardSchemaResult<unknown>): StandardSchemaResult<unknown> => {
|
|
92
|
+
if (result.issues) {
|
|
93
|
+
return {
|
|
94
|
+
issues: result.issues.map((issue): StandardSchemaIssue => ({
|
|
95
|
+
...issue,
|
|
96
|
+
path: issue.path?.map((segment, index) => (index === 0 && typeof segment === 'string' ? toOriginal(segment) : segment)),
|
|
97
|
+
})),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const validated = Object.entries((result.value ?? {}) as Record<string, unknown>).map(([key, entry]) => [toOriginal(key), entry])
|
|
101
|
+
return { value: { ...undeclaredValues, ...Object.fromEntries(validated) } }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = schema['~standard'].validate(declaredValues)
|
|
105
|
+
return result instanceof Promise ? result.then(settle) : settle(result)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { '~standard': { validate } }
|
|
109
|
+
}
|