-
Notifications
You must be signed in to change notification settings - Fork 1
/
schema.ts
381 lines (365 loc) · 10.4 KB
/
schema.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
import { isFailure, isSuccess, ValidationResult } from "./validation";
export interface ValidationOptions {
/**
* Stop validation on the first issue.
* default: false
*/
earlyExit: boolean;
/**
* Use coercion to validate this value. This option has no effect during parsing.
* default: false
*/
withCoercion: boolean;
}
export interface ParsingOptions {
/**
* Parse in strip mode:
* - Objects will not contain additional properties
* default: true
*/
strip: boolean;
/**
* Do not validate the object before parsing.
* Note that some validation may still be needed to parse the value.
* default: false
*/
skipValidation: boolean;
}
export type Options = ParsingOptions & ValidationOptions;
export const defaultOptions: Options = {
withCoercion: false,
earlyExit: false,
strip: true,
skipValidation: false,
};
export const getOption = (
o: Partial<Options> | undefined,
key: keyof Options
) => o?.[key] ?? defaultOptions[key];
/**
* Result of a parse
*/
export type ParseResult<I, O> = {
parsedValue?: O;
validation?: ValidationResult<I>;
};
/**
* A Schema that can be used to typeguard and validate type I
* and parse values into type O.
* It also has a metadata type M that allows introspection.
*
* @param I the type this schema validates against
* @param O the type this schema parses into
* @param M the type containing meta information
*/
export interface Schema<I, O = I, M = { type: string }> {
/**
* a typeguard of type I
* this method is constructed by the makeSchema function
* @param v the value to be checked
* @returns boolean indicating if v is I
*/
accepts: (v: unknown, options?: Partial<ValidationOptions>) => v is I;
/**
* builds a validation object containing all validation errors of the object
* @param v the value to be checked
* @param options the validation options
* @returns the validation result
*/
validate: (
v: unknown,
options?: Partial<ValidationOptions>
) => ValidationResult<I>;
/**
* builds a validation object asynchronously containing all validation errors of the object
* @param v the value to be checked
* @param options the validation options
* @returns a promise containing the validation result
*/
validateAsync: (
v: unknown,
options?: Partial<ValidationOptions>
) => Promise<ValidationResult<I>>;
/**
* parses a value as type O after validating it as type I
* @param v the value to be parsed
* @param options the parsing and validation options
* @returns the parse result
*/
parse: (v: unknown, options?: Partial<Options>) => ParseResult<I, O>;
/**
* parses a value as type O after validating it asynchronously as type I
* @param v the value to be parsed
* @param options the parsing and validation options
* @returns a promise containing the parse result
*/
parseAsync: (
v: unknown,
options?: Partial<Options>
) => Promise<ParseResult<I, O>>;
/**
* returns the Meta Object
*/
meta: () => M;
}
/**
* A helper function to create a schema from a validation function.
* @param validate the validation method
* @returns the schema
*/
export function makeSchema<I, O, M>(
validate: Schema<I, O, M>["validate"],
validateAsync: Schema<I, O, M>["validateAsync"],
meta: () => M,
parseAfterValidation: (v: I, options?: Partial<Options>) => O = (v) =>
v as unknown as O
): Schema<I, O, M> {
const parse: Schema<I, O, M>["parse"] = (v, o) => {
if (!getOption(o, "skipValidation")) {
const validation = validate(v, { ...o, withCoercion: true });
if (isFailure(validation)) {
return { validation };
}
}
return {
parsedValue: parseAfterValidation(v as I, {
...o,
skipValidation: true,
withCoercion: true,
}),
};
};
const parseAsync: Schema<I, O, M>["parseAsync"] = async (v, o) => {
if (!getOption(o, "skipValidation")) {
const validation = await validateAsync(v, { ...o, withCoercion: true });
if (isFailure(validation)) {
return { validation };
}
}
return {
parsedValue: parseAfterValidation(v as I, {
...o,
skipValidation: true,
withCoercion: true,
}),
};
};
return {
accepts: (v, o): v is I =>
isSuccess(validate(v, { ...o, earlyExit: true })),
parse,
parseAsync,
validate,
validateAsync,
meta,
};
}
/**
* A helper function to create a schema from a validation function.
* It builds the asynchronous validation canonically from the validation function.
* @param validate the validation method
* @returns the schema
*/
export function makeSimpleSchema<I, O, M>(
validate: Schema<I, O, M>["validate"],
meta: () => M,
parseAfterValidation: (v: I, options?: Partial<Options>) => O = (v) =>
v as unknown as O
): Schema<I, O, M> {
const parse: Schema<I, O, M>["parse"] = (v, o) => {
if (!getOption(o, "skipValidation")) {
const validation = validate(v, { ...o, withCoercion: true });
if (isFailure(validation)) {
return { validation };
}
}
return {
parsedValue: parseAfterValidation(v as I, {
...o,
skipValidation: true,
withCoercion: true,
}),
};
};
return {
accepts: (v, o): v is I =>
isSuccess(validate(v, { ...o, earlyExit: true })),
parse,
parseAsync: (v, o) => Promise.resolve(parse(v, o)),
validate,
validateAsync: (v, o) => Promise.resolve(validate(v, o)),
meta,
};
}
/**
* Modify a schema by extending its meta informations.
*
* @param schema the base schema
* @param metaExtension the additional meta fields
* @returns the modified schema
*/
export function withMetaInformation<T, O, M, N>(
schema: Schema<T, O, M>,
metaExtension: N
) {
return makeSchema(schema.validate, schema.validateAsync, () => ({
...schema.meta(),
...metaExtension,
}));
}
/**
* Preprocesses data before parsing or validation (aka coercion).
*
* Note that coercion is only done during `accepts` and `validate` if `options.withCoercion` is true.
*
* For example
* ```
* coerce(number(), Number)
* ```
* will result in a schema that will validate numbers,
* string, boolean etc... through native js coercion. It will
* still act as a typeguard for numbers only.
*
* This method is especially useful for parsing Timestamps
* into date, number or string schemas. @see coercedDate.
* Using coercion, you can avoid DTO objects and manual
* transformations in simple use-cases.
*
* @param schema the base schema to coerce values into
* @param coercion the coercion function
* @returns the coerced schema
*/
export function coerce<I, O, M>(
schema: Schema<I, O, M>,
coercion: (v: unknown) => unknown
): Schema<I, O, M> {
const parse: Schema<I, O, M>["parse"] = (v, o) =>
schema.parse(coercion(v), o);
const validate: Schema<I, O, M>["validate"] = (v, o) =>
schema.validate(getOption(o, "withCoercion") ? coercion(v) : v, o);
return {
accepts: (v, o): v is I =>
schema.accepts(getOption(o, "withCoercion") ? coercion(v) : v, o),
parse,
parseAsync: (v, o) => Promise.resolve(parse(v, o)),
validate,
validateAsync: (v, o) => Promise.resolve(validate(v, o)),
meta: () => schema.meta(),
};
}
/**
* Transforms the parsed value using a general function.
*
* @param schema the source schema
* @param transform the transform function
* @returns the transformed schema
*/
export function transform<I, O, P, M>(
schema: Schema<I, O, M>,
transform: (v: O) => P
): Schema<I, P, M> {
const parse: Schema<I, P, M>["parse"] = (v, o) => {
const result = schema.parse(v, o);
return {
...result,
parsedValue: isSuccess(result.validation)
? transform(result.parsedValue as O)
: undefined,
};
};
const validate: Schema<I, P, M>["validate"] = (v, o) => schema.validate(v, o);
return {
accepts: (v): v is I => schema.accepts(v),
parse,
parseAsync: (v, o) => Promise.resolve(parse(v, o)),
validate,
validateAsync: (v, o) => Promise.resolve(validate(v, o)),
meta: () => schema.meta(),
};
}
/**
* Narrows the type of the parsed value using a projection function.
*
* This is a general case of @see defaultValue() and a special case of @see transform
*
* @param schema the source schema
* @param projection the projection function
* @returns the narrowed schema
*/
export function narrow<I, O, P extends O, M>(
schema: Schema<I, O, M>,
projection: (v: O) => P
): Schema<I, P, M> {
return transform(schema, projection);
}
/**
* Set option overrides for a schema.
*
* @param schema the nested schema
* @param options the options
* @returns the modified schema
*/
export function options<I, O, M>(
schema: Schema<I, O, M>,
options: Partial<Options>
): Schema<I, O, M> {
const parse: Schema<I, O, M>["parse"] = (v, o) =>
schema.parse(v, { ...o, ...options });
const validate: Schema<I, O, M>["validate"] = (v, o) =>
schema.validate(v, { ...o, ...options });
return {
accepts: (v): v is I => schema.accepts(v),
parse,
parseAsync: (v, o) => Promise.resolve(parse(v, o)),
validate,
validateAsync: (v, o) => Promise.resolve(validate(v, o)),
meta: () => schema.meta(),
};
}
/**
* Tries to parse a source value as JSON into the schema or
* fail otherwhise (resulting in undefined)
* @param schema the base schema to coerce values into
* @returns the coerced schema
*/
export function json<I, O, M>(schema: Schema<I, O, M>): Schema<I, O, M> {
return coerce(schema, (v) =>
typeof v === "string" ? JSON.parse(v) : undefined
);
}
/**
* Infers the result type of a Schema<T, M> to T
*/
export type InferType<T> = T extends Schema<infer U, unknown, unknown>
? U
: never;
export type InferOutputType<T> = T extends Schema<unknown, infer U, unknown>
? U
: never;
export type InferMetaTypes<T extends readonly unknown[]> = T extends [
infer Head,
...infer Tail
]
? [InferMetaType<Head>, ...InferMetaTypes<Tail>]
: [];
/**
* Infers the meta type of a Schema<T, M> to M
*/
export type InferMetaType<T> = T extends Schema<unknown, unknown, infer U>
? U
: never;
/**
* Infers the result type of a tuple of Schemas. E.g. [Schema<A, M>, Schema<B, N>] to [A, B]
*/
export type InferTypes<T extends readonly unknown[]> = T extends [
infer Head,
...infer Tail
]
? [InferType<Head>, ...InferTypes<Tail>]
: [];
export type InferOutputTypes<T extends readonly unknown[]> = T extends [
infer Head,
...infer Tail
]
? [InferOutputType<Head>, ...InferOutputTypes<Tail>]
: [];