@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/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
+ }
@@ -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
+ }