@uniflowed/validator 0.0.0-alpha.10
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/action.js +309 -0
- package/collection.js +338 -0
- package/index.js +265 -0
- package/infer.js +107 -0
- package/issue.js +129 -0
- package/json-schema.js +307 -0
- package/lazy.js +93 -0
- package/namespace.js +229 -0
- package/object.js +241 -0
- package/optional.js +132 -0
- package/package.json +34 -0
- package/parse.js +116 -0
- package/pipe.js +300 -0
- package/plain-object.js +105 -0
- package/primitive.js +183 -0
- package/schema.js +388 -0
- package/union.js +229 -0
package/pipe.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/pipe`: a schema with steps after it.
|
|
4
|
+
//
|
|
5
|
+
// const Handle = pipe(string(), trim(), minLength(3), startsWith("@"));
|
|
6
|
+
// const Age = pipe(string(), transform(Number), integer(), min(18));
|
|
7
|
+
//
|
|
8
|
+
// A step takes a schema and returns a schema, so a pipeline is a fold and
|
|
9
|
+
// nothing more. `pipe` is variadic because refinement is cumulative in
|
|
10
|
+
// practice — a handle is a string that is trimmed *and* long enough *and*
|
|
11
|
+
// starts with an at-sign — and making that `pipe(pipe(pipe(…)))` is a tax on
|
|
12
|
+
// the only case anybody has.
|
|
13
|
+
//
|
|
14
|
+
// # A pipeline may change the output type
|
|
15
|
+
//
|
|
16
|
+
// `transform` is the reason the type has two parameters. `pipe(string(),
|
|
17
|
+
// transform(Number))` accepts a `string` and produces a `number`, and both
|
|
18
|
+
// halves survive: `InferInput` is `string`, `InferOutput` is `number`. That is
|
|
19
|
+
// what lets `@uniflowed/form` type `defaultValues` as what the controls hold
|
|
20
|
+
// and `onValid` as what the application wanted, from one schema.
|
|
21
|
+
//
|
|
22
|
+
// A step is polymorphic in the input type — `<TInput>(Schema<TFrom, TInput>)
|
|
23
|
+
// => Schema<TTo, TInput>` — which is how the pipeline's input survives every
|
|
24
|
+
// step after the first. It is also why a step is written as a generic arrow
|
|
25
|
+
// rather than a plain one.
|
|
26
|
+
//
|
|
27
|
+
// # Why the overload table
|
|
28
|
+
//
|
|
29
|
+
// Flow cannot fold a type over a variadic argument list, so the relationship
|
|
30
|
+
// "the output of step *n* is the input of step *n+1*" has to be spelled out
|
|
31
|
+
// once per arity. Eight of them, which covers every pipeline this repository
|
|
32
|
+
// or Valibot's own examples contain; a ninth step is a type error, and the fix
|
|
33
|
+
// is to name the first eight and pipe the result. The implementation under the
|
|
34
|
+
// table is arity-agnostic and there is exactly one cast in this package to
|
|
35
|
+
// join the two, marked where it happens.
|
|
36
|
+
//
|
|
37
|
+
// The alternative was the previous signature, `pipe(schema, ...steps:
|
|
38
|
+
// Step<any, any>): Schema<any>`, which type-checked everything and knew
|
|
39
|
+
// nothing: it silently erased the output type of every pipeline in every
|
|
40
|
+
// application, and `uf lint` was right to reject it.
|
|
41
|
+
//
|
|
42
|
+
// # Where a cross-field rule reports
|
|
43
|
+
//
|
|
44
|
+
// [`check`] takes an optional path. A rule that compares two fields lives on
|
|
45
|
+
// the object — it needs both of them — but the message belongs under the
|
|
46
|
+
// control the user has to change:
|
|
47
|
+
//
|
|
48
|
+
// pipe(
|
|
49
|
+
// object({ password: string(), confirm: string() }),
|
|
50
|
+
// check((form) => form.password === form.confirm, "Passwords must match", ["confirm"]),
|
|
51
|
+
// );
|
|
52
|
+
//
|
|
53
|
+
// Without it the issue arrives with the object's own path and a form has
|
|
54
|
+
// nowhere to put it.
|
|
55
|
+
|
|
56
|
+
import type { Path } from "./issue.js";
|
|
57
|
+
import { issueUnder } from "./issue.js";
|
|
58
|
+
import type { Constraint, Description, Schema } from "./schema.js";
|
|
59
|
+
import { describe, isAsync, makeAsyncSchema, makeSchema, ok, run, runAsync } from "./schema.js";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One stage of a pipeline.
|
|
63
|
+
*
|
|
64
|
+
* Polymorphic in `TInput` so that the type of the pipeline's *input* is
|
|
65
|
+
* carried through every step rather than being flattened to `mixed` at the
|
|
66
|
+
* first one.
|
|
67
|
+
*/
|
|
68
|
+
export type Step<TFrom, TTo> = <TInput>(Schema<TFrom, TInput>) => Schema<TTo, TInput>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `pipe`'s type: one call signature per number of steps.
|
|
72
|
+
*
|
|
73
|
+
* Inexact, because an exact object type with call properties cannot be
|
|
74
|
+
* inhabited by a function.
|
|
75
|
+
*/
|
|
76
|
+
export type Pipe = {
|
|
77
|
+
<A, AIn>(schema: Schema<A, AIn>): Schema<A, AIn>,
|
|
78
|
+
<A, AIn, B>(schema: Schema<A, AIn>, a: Step<A, B>): Schema<B, AIn>,
|
|
79
|
+
<A, AIn, B, C>(schema: Schema<A, AIn>, a: Step<A, B>, b: Step<B, C>): Schema<C, AIn>,
|
|
80
|
+
<A, AIn, B, C, D>(
|
|
81
|
+
schema: Schema<A, AIn>,
|
|
82
|
+
a: Step<A, B>,
|
|
83
|
+
b: Step<B, C>,
|
|
84
|
+
c: Step<C, D>,
|
|
85
|
+
): Schema<D, AIn>,
|
|
86
|
+
<A, AIn, B, C, D, E>(
|
|
87
|
+
schema: Schema<A, AIn>,
|
|
88
|
+
a: Step<A, B>,
|
|
89
|
+
b: Step<B, C>,
|
|
90
|
+
c: Step<C, D>,
|
|
91
|
+
d: Step<D, E>,
|
|
92
|
+
): Schema<E, AIn>,
|
|
93
|
+
<A, AIn, B, C, D, E, F>(
|
|
94
|
+
schema: Schema<A, AIn>,
|
|
95
|
+
a: Step<A, B>,
|
|
96
|
+
b: Step<B, C>,
|
|
97
|
+
c: Step<C, D>,
|
|
98
|
+
d: Step<D, E>,
|
|
99
|
+
e: Step<E, F>,
|
|
100
|
+
): Schema<F, AIn>,
|
|
101
|
+
<A, AIn, B, C, D, E, F, G>(
|
|
102
|
+
schema: Schema<A, AIn>,
|
|
103
|
+
a: Step<A, B>,
|
|
104
|
+
b: Step<B, C>,
|
|
105
|
+
c: Step<C, D>,
|
|
106
|
+
d: Step<D, E>,
|
|
107
|
+
e: Step<E, F>,
|
|
108
|
+
f: Step<F, G>,
|
|
109
|
+
): Schema<G, AIn>,
|
|
110
|
+
<A, AIn, B, C, D, E, F, G, H>(
|
|
111
|
+
schema: Schema<A, AIn>,
|
|
112
|
+
a: Step<A, B>,
|
|
113
|
+
b: Step<B, C>,
|
|
114
|
+
c: Step<C, D>,
|
|
115
|
+
d: Step<D, E>,
|
|
116
|
+
e: Step<E, F>,
|
|
117
|
+
f: Step<F, G>,
|
|
118
|
+
g: Step<G, H>,
|
|
119
|
+
): Schema<H, AIn>,
|
|
120
|
+
<A, AIn, B, C, D, E, F, G, H, I>(
|
|
121
|
+
schema: Schema<A, AIn>,
|
|
122
|
+
a: Step<A, B>,
|
|
123
|
+
b: Step<B, C>,
|
|
124
|
+
c: Step<C, D>,
|
|
125
|
+
d: Step<D, E>,
|
|
126
|
+
e: Step<E, F>,
|
|
127
|
+
f: Step<F, G>,
|
|
128
|
+
g: Step<G, H>,
|
|
129
|
+
h: Step<H, I>,
|
|
130
|
+
): Schema<I, AIn>,
|
|
131
|
+
...
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function applySteps<TInput>(
|
|
135
|
+
schema: Schema<mixed, TInput>,
|
|
136
|
+
...steps: $ReadOnlyArray<Step<mixed, mixed>>
|
|
137
|
+
): Schema<mixed, TInput> {
|
|
138
|
+
let piped = schema;
|
|
139
|
+
for (const step of steps) {
|
|
140
|
+
piped = step<TInput>(piped);
|
|
141
|
+
}
|
|
142
|
+
return piped;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Apply steps to a schema, left to right. */
|
|
146
|
+
// $FlowFixMe[incompatible-type] the overload table above is the checked surface.
|
|
147
|
+
export const pipe: Pipe = applySteps as Pipe;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A schema with one more thing that must be true of its output.
|
|
151
|
+
*
|
|
152
|
+
* Every named step in `action.js` is a call to this. The `constraint` is what
|
|
153
|
+
* makes the step visible to `json-schema.js`: a refinement that cannot say
|
|
154
|
+
* what it refined is invisible to every exporter.
|
|
155
|
+
*/
|
|
156
|
+
export function refine<TOutput, TInput>(
|
|
157
|
+
schema: Schema<TOutput, TInput>,
|
|
158
|
+
accepts: (value: TOutput) => boolean,
|
|
159
|
+
code: string,
|
|
160
|
+
message: string,
|
|
161
|
+
constraint: Constraint,
|
|
162
|
+
at: Path = [],
|
|
163
|
+
): Schema<TOutput, TInput> {
|
|
164
|
+
const description = (): Description => ({
|
|
165
|
+
kind: "constrained",
|
|
166
|
+
inner: describe(schema),
|
|
167
|
+
constraint,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (isAsync(schema)) {
|
|
171
|
+
return makeAsyncSchema(async (value, path) => {
|
|
172
|
+
const result = await runAsync(schema, value, path);
|
|
173
|
+
if (!result.ok || accepts(result.value)) {
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
return { ok: false, issues: [issueUnder(code, message, path, at)] };
|
|
177
|
+
}, description);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return makeSchema((value, path) => {
|
|
181
|
+
const result = run(schema, value, path);
|
|
182
|
+
if (!result.ok || accepts(result.value)) {
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
return { ok: false, issues: [issueUnder(code, message, path, at)] };
|
|
186
|
+
}, description);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* An arbitrary predicate, with the message it should report.
|
|
191
|
+
*
|
|
192
|
+
* Every other step in the package is a special case of this one. It exists so
|
|
193
|
+
* that a rule the library did not anticipate — a checksum, a business rule,
|
|
194
|
+
* one field agreeing with another — is a one-liner rather than a reason to
|
|
195
|
+
* abandon the schema and hand-roll validation.
|
|
196
|
+
*
|
|
197
|
+
* `at` is where the issue lands, relative to the value being checked. See the
|
|
198
|
+
* module docs for the cross-field case it is there for.
|
|
199
|
+
*/
|
|
200
|
+
export function check<TValue>(
|
|
201
|
+
accepts: (value: TValue) => boolean,
|
|
202
|
+
message: string,
|
|
203
|
+
at: Path = [],
|
|
204
|
+
): Step<TValue, TValue> {
|
|
205
|
+
return <TInput>(schema: Schema<TValue, TInput>): Schema<TValue, TInput> =>
|
|
206
|
+
refine(schema, accepts, "check", message, { kind: "opaque", label: message }, at);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A predicate that has to ask something.
|
|
211
|
+
*
|
|
212
|
+
* "Is this username taken" is a question with a network on the other end, and
|
|
213
|
+
* a schema containing one is asynchronous from here up: the object around it,
|
|
214
|
+
* the array around that, and [`safeParse`] will refuse it and say to use
|
|
215
|
+
* [`safeParseAsync`].
|
|
216
|
+
*/
|
|
217
|
+
export function checkAsync<TValue>(
|
|
218
|
+
accepts: (value: TValue) => Promise<boolean>,
|
|
219
|
+
message: string,
|
|
220
|
+
at: Path = [],
|
|
221
|
+
): Step<TValue, TValue> {
|
|
222
|
+
return <TInput>(schema: Schema<TValue, TInput>): Schema<TValue, TInput> =>
|
|
223
|
+
makeAsyncSchema(
|
|
224
|
+
async (value, path) => {
|
|
225
|
+
const result = await runAsync(schema, value, path);
|
|
226
|
+
if (!result.ok) {
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
return (await accepts(result.value))
|
|
230
|
+
? result
|
|
231
|
+
: { ok: false, issues: [issueUnder("check", message, path, at)] };
|
|
232
|
+
},
|
|
233
|
+
() => ({
|
|
234
|
+
kind: "constrained",
|
|
235
|
+
inner: describe(schema),
|
|
236
|
+
constraint: { kind: "opaque", label: message },
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Change the value, and with it the schema's output type.
|
|
243
|
+
*
|
|
244
|
+
* Runs after everything before it in the pipeline has accepted, so a transform
|
|
245
|
+
* never sees a value the steps above rejected — which is why
|
|
246
|
+
* `pipe(string(), minLength(2), transform((text) => text.length))` is safe to
|
|
247
|
+
* write in that order and means something different in the other.
|
|
248
|
+
*/
|
|
249
|
+
export function transform<TFrom, TTo>(change: (value: TFrom) => TTo): Step<TFrom, TTo> {
|
|
250
|
+
return <TInput>(schema: Schema<TFrom, TInput>): Schema<TTo, TInput> => {
|
|
251
|
+
const description = (): Description => ({ kind: "transformed", inner: describe(schema) });
|
|
252
|
+
if (isAsync(schema)) {
|
|
253
|
+
return makeAsyncSchema(async (value, path) => {
|
|
254
|
+
const result = await runAsync(schema, value, path);
|
|
255
|
+
return result.ok ? ok(change(result.value)) : result;
|
|
256
|
+
}, description);
|
|
257
|
+
}
|
|
258
|
+
return makeSchema((value, path) => {
|
|
259
|
+
const result = run(schema, value, path);
|
|
260
|
+
return result.ok ? ok(change(result.value)) : result;
|
|
261
|
+
}, description);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A transform that has to wait: a lookup, a hash, a decode off the main path. */
|
|
266
|
+
export function transformAsync<TFrom, TTo>(
|
|
267
|
+
change: (value: TFrom) => Promise<TTo>,
|
|
268
|
+
): Step<TFrom, TTo> {
|
|
269
|
+
return <TInput>(schema: Schema<TFrom, TInput>): Schema<TTo, TInput> =>
|
|
270
|
+
makeAsyncSchema(
|
|
271
|
+
async (value, path) => {
|
|
272
|
+
const result = await runAsync(schema, value, path);
|
|
273
|
+
return result.ok ? ok(await change(result.value)) : result;
|
|
274
|
+
},
|
|
275
|
+
() => ({ kind: "transformed", inner: describe(schema) }),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* A name on an otherwise ordinary value.
|
|
281
|
+
*
|
|
282
|
+
* It checks nothing at run time and it is not pretending to. The name reaches
|
|
283
|
+
* the description, so an exported schema can say "this string is a `UserId`";
|
|
284
|
+
* it does not reach the Flow type, because Flow's opaque types are declared in
|
|
285
|
+
* a module and cannot be produced by a call. `infer.js` says what to do
|
|
286
|
+
* instead when the distinction has to be enforced.
|
|
287
|
+
*/
|
|
288
|
+
export function brand<TValue>(name: string): Step<TValue, TValue> {
|
|
289
|
+
return <TInput>(schema: Schema<TValue, TInput>): Schema<TValue, TInput> => {
|
|
290
|
+
const description = (): Description => ({
|
|
291
|
+
kind: "constrained",
|
|
292
|
+
inner: describe(schema),
|
|
293
|
+
constraint: { kind: "brand", name },
|
|
294
|
+
});
|
|
295
|
+
if (isAsync(schema)) {
|
|
296
|
+
return makeAsyncSchema((value, path) => runAsync(schema, value, path), description);
|
|
297
|
+
}
|
|
298
|
+
return makeSchema((value, path) => run(schema, value, path), description);
|
|
299
|
+
};
|
|
300
|
+
}
|
package/plain-object.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/plain-object`: reading and writing an object whose
|
|
4
|
+
// keys came from outside.
|
|
5
|
+
//
|
|
6
|
+
// Three functions, and they are separate from every schema that uses them
|
|
7
|
+
// because they are the package's whole answer to one question: what happens
|
|
8
|
+
// when a payload contains a key that means something to JavaScript.
|
|
9
|
+
//
|
|
10
|
+
// `{"__proto__": {"isAdmin": true}}` is valid JSON, and it is what an attacker
|
|
11
|
+
// sends. `out[key] = value` runs the legacy `__proto__` setter rather than
|
|
12
|
+
// adding a property, so the parsed object would come back with a prototype the
|
|
13
|
+
// attacker chose and every `record.isAdmin` downstream would read `true` from
|
|
14
|
+
// a property nobody ever validated. Reading is the mirror image: `input[key]`
|
|
15
|
+
// finds inherited properties, so a shape asking for `constructor` would be
|
|
16
|
+
// handed `Object`.
|
|
17
|
+
//
|
|
18
|
+
// `object.js`, `collection.js`, `union.js` and `issue.js` all build or read an
|
|
19
|
+
// object out of untrusted input, and every one of them goes through here
|
|
20
|
+
// rather than keeping its own copy of the rule. That is the reason this is a
|
|
21
|
+
// module and not three helpers at the top of the object parser: the defence is
|
|
22
|
+
// only a defence if there is exactly one of it.
|
|
23
|
+
|
|
24
|
+
/** Whether `value` is an object a shape or a record could be read from. */
|
|
25
|
+
export function isPlainObject(value: mixed): boolean {
|
|
26
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `value` as something with string keys.
|
|
31
|
+
*
|
|
32
|
+
* The single object boundary in the package. Every caller has already asked
|
|
33
|
+
* [`isPlainObject`], and every field that leaves a schema went through that
|
|
34
|
+
* schema first, so the `mixed` values this exposes are narrowed before they
|
|
35
|
+
* reach an output type.
|
|
36
|
+
*/
|
|
37
|
+
export function plainRecord(value: mixed): { readonly [string]: mixed, ... } {
|
|
38
|
+
// $FlowFixMe[incompatible-type] guarded by `isPlainObject` at every call.
|
|
39
|
+
return value as { readonly [string]: mixed, ... };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The own keys of `value`, and nothing inherited.
|
|
44
|
+
*
|
|
45
|
+
* `Object.keys` already skips the prototype chain, which is why it is here
|
|
46
|
+
* rather than `for (const key in value)`.
|
|
47
|
+
*/
|
|
48
|
+
export function ownKeys(value: { readonly [string]: mixed, ... }): $ReadOnlyArray<string> {
|
|
49
|
+
return Object.keys(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read one key, without falling through to the prototype.
|
|
54
|
+
*
|
|
55
|
+
* `record.constructor` is `Object` on every object in the language; a shape
|
|
56
|
+
* with a `constructor` field would otherwise be handed a function and report
|
|
57
|
+
* that the payload was fine. It also makes `object(shape)` answer the same way
|
|
58
|
+
* for `{}` and for a class instance with getters on its prototype, which is
|
|
59
|
+
* the sort of difference that is discovered in production.
|
|
60
|
+
*
|
|
61
|
+
* `Object.hasOwn` on every field read costs about 13% of a field-dense parse,
|
|
62
|
+
* measured on the workload in `index.js`. That is the price of the paragraph
|
|
63
|
+
* above and it is being paid deliberately: a payload out of `JSON.parse` has
|
|
64
|
+
* only own keys, so the check earns nothing there, and it earns everything the
|
|
65
|
+
* first time somebody hands a schema an object they built themselves.
|
|
66
|
+
*/
|
|
67
|
+
export function ownValue(source: { readonly [string]: mixed, ... }, key: string): mixed {
|
|
68
|
+
return Object.hasOwn(source, key) ? source[key] : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Write one parsed field into the object being built.
|
|
73
|
+
*
|
|
74
|
+
* `__proto__` is the only key that needs `defineProperty`, and it is worth
|
|
75
|
+
* knowing why rather than reaching for it on every field. `Object.prototype`
|
|
76
|
+
* has exactly one accessor on it — `__proto__` — and assignment to a key an
|
|
77
|
+
* accessor owns runs the setter instead of adding a property. Every other
|
|
78
|
+
* inherited name (`constructor`, `toString`, `valueOf`) is a *data* property,
|
|
79
|
+
* and assigning to one of those shadows it with an own property on the
|
|
80
|
+
* receiver, which is what a parsed field is supposed to be.
|
|
81
|
+
*
|
|
82
|
+
* So one string comparison is the whole defence against a hostile payload, and
|
|
83
|
+
* `defineProperty` is the slow path for the one key that needs it. That is not
|
|
84
|
+
* a micro-optimisation: `defineProperty` on every field made a thousand-record
|
|
85
|
+
* parse three times slower than the same parse with an assignment in it — more
|
|
86
|
+
* than the entire rest of the walk cost — and the measurement is in
|
|
87
|
+
* `index.js`.
|
|
88
|
+
*
|
|
89
|
+
* What this does not defend against is an `Object.prototype` that some other
|
|
90
|
+
* code has already given a setter to. Nothing in a parser can: a process whose
|
|
91
|
+
* `Object.prototype` is writable by an attacker has lost, and every read the
|
|
92
|
+
* consumer makes afterwards goes through the same polluted object.
|
|
93
|
+
*/
|
|
94
|
+
export function put<Value>(out: { [string]: Value, ... }, key: string, value: Value): void {
|
|
95
|
+
if (key !== "__proto__") {
|
|
96
|
+
out[key] = value;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
Object.defineProperty(out, key, {
|
|
100
|
+
value,
|
|
101
|
+
writable: true,
|
|
102
|
+
enumerable: true,
|
|
103
|
+
configurable: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
package/primitive.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/primitive`: the leaves.
|
|
4
|
+
//
|
|
5
|
+
// A schema that does not contain another schema. Every one of them is the same
|
|
6
|
+
// four lines — test the value, hand it back or say what was wanted — and they
|
|
7
|
+
// are together because that sameness is the whole subject: this is the list of
|
|
8
|
+
// things uf will recognise without being told how.
|
|
9
|
+
//
|
|
10
|
+
// Each is a function rather than a constant so that a project ships only the
|
|
11
|
+
// checks it called. `string` is not a value in a table somewhere that a
|
|
12
|
+
// bundler has to keep because the table is reachable.
|
|
13
|
+
|
|
14
|
+
import { plainRecord } from "./plain-object.js";
|
|
15
|
+
import type { Description, Schema } from "./schema.js";
|
|
16
|
+
import { fail, makeSchema, ok } from "./schema.js";
|
|
17
|
+
|
|
18
|
+
const stringly = (): Description => ({ kind: "string" });
|
|
19
|
+
|
|
20
|
+
export function string(): Schema<string, string> {
|
|
21
|
+
return makeSchema(
|
|
22
|
+
(value, path) =>
|
|
23
|
+
typeof value === "string" ? ok(value) : fail("type", "expected string", path),
|
|
24
|
+
stringly,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A finite number.
|
|
30
|
+
*
|
|
31
|
+
* `NaN` and the infinities are rejected. They are numbers to `typeof` and
|
|
32
|
+
* disasters to arithmetic, and a validator that lets `NaN` through has not
|
|
33
|
+
* validated anything — every comparison downstream silently answers `false`.
|
|
34
|
+
*/
|
|
35
|
+
export function number(): Schema<number, number> {
|
|
36
|
+
return makeSchema(
|
|
37
|
+
(value, path) =>
|
|
38
|
+
typeof value === "number" && Number.isFinite(value)
|
|
39
|
+
? ok(value)
|
|
40
|
+
: fail("type", "expected number", path),
|
|
41
|
+
() => ({ kind: "number" }),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A `bigint`.
|
|
47
|
+
*
|
|
48
|
+
* Separate from [`number`] because the two do not mix: `1n === 1` is false,
|
|
49
|
+
* `1n + 1` throws, and `JSON.stringify` refuses. A schema that accepted either
|
|
50
|
+
* would hand its caller a value whose arithmetic depends on the payload.
|
|
51
|
+
*/
|
|
52
|
+
export function bigint(): Schema<bigint, bigint> {
|
|
53
|
+
return makeSchema(
|
|
54
|
+
(value, path) =>
|
|
55
|
+
typeof value === "bigint" ? ok(value) : fail("type", "expected bigint", path),
|
|
56
|
+
() => ({ kind: "bigint" }),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function boolean(): Schema<boolean, boolean> {
|
|
61
|
+
return makeSchema(
|
|
62
|
+
(value, path) =>
|
|
63
|
+
typeof value === "boolean" ? ok(value) : fail("type", "expected boolean", path),
|
|
64
|
+
() => ({ kind: "boolean" }),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Anything at all, unexamined. The identity of this package. */
|
|
69
|
+
export function unknown(): Schema<mixed, mixed> {
|
|
70
|
+
return makeSchema(
|
|
71
|
+
(value) => ok(value),
|
|
72
|
+
() => ({ kind: "unknown" }),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Nothing at all.
|
|
78
|
+
*
|
|
79
|
+
* For the branch of a union that must not be reachable, and for a shape whose
|
|
80
|
+
* field is being removed: `never()` says so at the boundary instead of leaving
|
|
81
|
+
* a field that quietly still works.
|
|
82
|
+
*/
|
|
83
|
+
export function never(): Schema<empty, empty> {
|
|
84
|
+
return makeSchema(
|
|
85
|
+
(value, path) => fail("never", "expected nothing here", path),
|
|
86
|
+
() => ({ kind: "never" }),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Exactly `null`. Distinct from a missing key, which is [`optional`]. */
|
|
91
|
+
export function null_(): Schema<null, null> {
|
|
92
|
+
return makeSchema(
|
|
93
|
+
(value, path) => (value === null ? ok(null) : fail("type", "expected null", path)),
|
|
94
|
+
() => ({ kind: "null" }),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Exactly `undefined`. */
|
|
99
|
+
export function undefined_(): Schema<void, void> {
|
|
100
|
+
return makeSchema(
|
|
101
|
+
(value, path) =>
|
|
102
|
+
value === undefined ? ok(undefined) : fail("type", "expected undefined", path),
|
|
103
|
+
() => ({ kind: "undefined" }),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function literal<TValue extends string | number | boolean | null>(
|
|
108
|
+
expected: TValue,
|
|
109
|
+
): Schema<TValue, TValue> {
|
|
110
|
+
return makeSchema(
|
|
111
|
+
(value, path) =>
|
|
112
|
+
value === expected ? ok(expected) : fail("literal", `expected ${String(expected)}`, path),
|
|
113
|
+
() => ({ kind: "literal", value: expected }),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One of a fixed list of strings.
|
|
119
|
+
*
|
|
120
|
+
* The message names every option, because a rejected enum is almost always a
|
|
121
|
+
* typo and the fix is in the list the caller could not see.
|
|
122
|
+
*/
|
|
123
|
+
export function enum_<TValue extends string>(
|
|
124
|
+
values: $ReadOnlyArray<TValue>,
|
|
125
|
+
): Schema<TValue, TValue> {
|
|
126
|
+
const message = `expected one of ${values.join(", ")}`;
|
|
127
|
+
return makeSchema(
|
|
128
|
+
(value, path) => {
|
|
129
|
+
for (const option of values) {
|
|
130
|
+
if (value === option) {
|
|
131
|
+
return ok(option);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return fail("enum", message, path);
|
|
135
|
+
},
|
|
136
|
+
() => ({ kind: "enum", values: values.slice() }),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A `Date` that is a date, rather than the `Invalid Date` a bad string makes. */
|
|
141
|
+
export function date(): Schema<Date, Date> {
|
|
142
|
+
return makeSchema(
|
|
143
|
+
(value, path) =>
|
|
144
|
+
value instanceof Date && Number.isFinite(value.getTime())
|
|
145
|
+
? ok(value)
|
|
146
|
+
: fail("type", "expected Date", path),
|
|
147
|
+
() => ({ kind: "date" }),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** An instance of `ClassValue`, by `instanceof`. */
|
|
152
|
+
export function instance<TValue>(ClassValue: Class<TValue>): Schema<TValue, TValue> {
|
|
153
|
+
const named = plainRecord(ClassValue).name;
|
|
154
|
+
const name = typeof named === "string" ? named : "instance";
|
|
155
|
+
return makeSchema(
|
|
156
|
+
(value, path) =>
|
|
157
|
+
value instanceof ClassValue ? ok(value as TValue) : fail("type", `expected ${name}`, path),
|
|
158
|
+
() => ({ kind: "instance", name }),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A leaf this package has no name for.
|
|
164
|
+
*
|
|
165
|
+
* The escape hatch, and the one place a caller's word is taken for a type:
|
|
166
|
+
* `accepts` returning true is what makes the value a `TValue`, and nothing
|
|
167
|
+
* checks that claim. Use it for a value with a shape of its own — a
|
|
168
|
+
* `Uint8Array`, a branded id from another library — and reach for [`check`]
|
|
169
|
+
* instead when the value is an ordinary type with a rule attached, because
|
|
170
|
+
* that keeps the type honest.
|
|
171
|
+
*/
|
|
172
|
+
export function custom<TValue>(
|
|
173
|
+
accepts: (value: mixed) => boolean,
|
|
174
|
+
message: string,
|
|
175
|
+
name: string = "custom",
|
|
176
|
+
): Schema<TValue, TValue> {
|
|
177
|
+
return makeSchema(
|
|
178
|
+
(value, path) =>
|
|
179
|
+
// $FlowFixMe[incompatible-type] `accepts` is the caller's promise, not a proof.
|
|
180
|
+
accepts(value) ? ok(value as TValue) : fail("custom", message, path),
|
|
181
|
+
() => ({ kind: "custom", name }),
|
|
182
|
+
);
|
|
183
|
+
}
|