@uniflowed/validator 0.0.0-alpha.4 → 0.0.0-alpha.6
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 +245 -763
- 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 +19 -4
- 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/namespace.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/namespace`: every builder under one name.
|
|
4
|
+
//
|
|
5
|
+
// import { v } from "@uniflowed/validator";
|
|
6
|
+
// const Account = v.object({ email: v.pipe(v.string(), v.email()) });
|
|
7
|
+
//
|
|
8
|
+
// The named exports are the primary surface. This is the convenience alias,
|
|
9
|
+
// for the many schemas that mention a dozen builders and would otherwise open
|
|
10
|
+
// with a dozen-line import, and for readers coming from Valibot who already
|
|
11
|
+
// write `v.` in front of everything.
|
|
12
|
+
//
|
|
13
|
+
// # Why it is a module of its own
|
|
14
|
+
//
|
|
15
|
+
// Because it is the one module that has to import all of them, and a package
|
|
16
|
+
// whose entry point did that would make every application carry every check.
|
|
17
|
+
// Here it is reached through a single named re-export from `index.js`, which a
|
|
18
|
+
// bundler drops when nothing reads `v`; a project that imports
|
|
19
|
+
// `@uniflowed/validator/primitive` directly never resolves this file at all.
|
|
20
|
+
//
|
|
21
|
+
// # Why it is typed with `typeof`
|
|
22
|
+
//
|
|
23
|
+
// So the two surfaces cannot drift. Each field's type is the exported
|
|
24
|
+
// function's own, which means adding a builder and forgetting to list it here
|
|
25
|
+
// is a missing property rather than a silently different signature, and
|
|
26
|
+
// renaming one is an error at this file rather than a surprise at a call site.
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
email,
|
|
30
|
+
endsWith,
|
|
31
|
+
includes,
|
|
32
|
+
integer,
|
|
33
|
+
isoDate,
|
|
34
|
+
length,
|
|
35
|
+
max,
|
|
36
|
+
maxItems,
|
|
37
|
+
maxLength,
|
|
38
|
+
min,
|
|
39
|
+
minItems,
|
|
40
|
+
minLength,
|
|
41
|
+
multipleOf,
|
|
42
|
+
nonEmpty,
|
|
43
|
+
regex,
|
|
44
|
+
startsWith,
|
|
45
|
+
toLowerCase,
|
|
46
|
+
toUpperCase,
|
|
47
|
+
trim,
|
|
48
|
+
url,
|
|
49
|
+
uuid,
|
|
50
|
+
} from "./action.js";
|
|
51
|
+
import { array, map, record, set, tuple } from "./collection.js";
|
|
52
|
+
import { flatten } from "./issue.js";
|
|
53
|
+
import { toJsonSchema } from "./json-schema.js";
|
|
54
|
+
import { lazy, lazyAsync } from "./lazy.js";
|
|
55
|
+
import { looseObject, object, partial, strictObject } from "./object.js";
|
|
56
|
+
import { fallback, nullable, nullish, optional, withDefault } from "./optional.js";
|
|
57
|
+
import {
|
|
58
|
+
is,
|
|
59
|
+
parse,
|
|
60
|
+
parseAsync,
|
|
61
|
+
parser,
|
|
62
|
+
safeParse,
|
|
63
|
+
safeParseAsync,
|
|
64
|
+
useValidation,
|
|
65
|
+
} from "./parse.js";
|
|
66
|
+
import { brand, check, checkAsync, pipe, refine, transform, transformAsync } from "./pipe.js";
|
|
67
|
+
import {
|
|
68
|
+
bigint,
|
|
69
|
+
boolean,
|
|
70
|
+
custom,
|
|
71
|
+
date,
|
|
72
|
+
enum_,
|
|
73
|
+
instance,
|
|
74
|
+
literal,
|
|
75
|
+
never,
|
|
76
|
+
null_,
|
|
77
|
+
number,
|
|
78
|
+
string,
|
|
79
|
+
undefined_,
|
|
80
|
+
unknown,
|
|
81
|
+
} from "./primitive.js";
|
|
82
|
+
import { describe, isAsync } from "./schema.js";
|
|
83
|
+
import { intersect, union, variant } from "./union.js";
|
|
84
|
+
|
|
85
|
+
export const v: {
|
|
86
|
+
readonly string: typeof string,
|
|
87
|
+
readonly number: typeof number,
|
|
88
|
+
readonly bigint: typeof bigint,
|
|
89
|
+
readonly boolean: typeof boolean,
|
|
90
|
+
readonly unknown: typeof unknown,
|
|
91
|
+
readonly never: typeof never,
|
|
92
|
+
readonly null: typeof null_,
|
|
93
|
+
readonly undefined: typeof undefined_,
|
|
94
|
+
readonly literal: typeof literal,
|
|
95
|
+
readonly enum: typeof enum_,
|
|
96
|
+
readonly date: typeof date,
|
|
97
|
+
readonly instance: typeof instance,
|
|
98
|
+
readonly custom: typeof custom,
|
|
99
|
+
readonly object: typeof object,
|
|
100
|
+
readonly strictObject: typeof strictObject,
|
|
101
|
+
readonly looseObject: typeof looseObject,
|
|
102
|
+
readonly partial: typeof partial,
|
|
103
|
+
readonly array: typeof array,
|
|
104
|
+
readonly tuple: typeof tuple,
|
|
105
|
+
readonly record: typeof record,
|
|
106
|
+
readonly map: typeof map,
|
|
107
|
+
readonly set: typeof set,
|
|
108
|
+
readonly union: typeof union,
|
|
109
|
+
readonly variant: typeof variant,
|
|
110
|
+
readonly intersect: typeof intersect,
|
|
111
|
+
readonly optional: typeof optional,
|
|
112
|
+
readonly nullable: typeof nullable,
|
|
113
|
+
readonly nullish: typeof nullish,
|
|
114
|
+
readonly withDefault: typeof withDefault,
|
|
115
|
+
readonly fallback: typeof fallback,
|
|
116
|
+
readonly lazy: typeof lazy,
|
|
117
|
+
readonly lazyAsync: typeof lazyAsync,
|
|
118
|
+
readonly pipe: typeof pipe,
|
|
119
|
+
readonly check: typeof check,
|
|
120
|
+
readonly checkAsync: typeof checkAsync,
|
|
121
|
+
readonly transform: typeof transform,
|
|
122
|
+
readonly transformAsync: typeof transformAsync,
|
|
123
|
+
readonly brand: typeof brand,
|
|
124
|
+
readonly refine: typeof refine,
|
|
125
|
+
readonly minLength: typeof minLength,
|
|
126
|
+
readonly maxLength: typeof maxLength,
|
|
127
|
+
readonly length: typeof length,
|
|
128
|
+
readonly nonEmpty: typeof nonEmpty,
|
|
129
|
+
readonly startsWith: typeof startsWith,
|
|
130
|
+
readonly endsWith: typeof endsWith,
|
|
131
|
+
readonly includes: typeof includes,
|
|
132
|
+
readonly regex: typeof regex,
|
|
133
|
+
readonly email: typeof email,
|
|
134
|
+
readonly url: typeof url,
|
|
135
|
+
readonly uuid: typeof uuid,
|
|
136
|
+
readonly isoDate: typeof isoDate,
|
|
137
|
+
readonly trim: typeof trim,
|
|
138
|
+
readonly toLowerCase: typeof toLowerCase,
|
|
139
|
+
readonly toUpperCase: typeof toUpperCase,
|
|
140
|
+
readonly min: typeof min,
|
|
141
|
+
readonly max: typeof max,
|
|
142
|
+
readonly integer: typeof integer,
|
|
143
|
+
readonly multipleOf: typeof multipleOf,
|
|
144
|
+
readonly minItems: typeof minItems,
|
|
145
|
+
readonly maxItems: typeof maxItems,
|
|
146
|
+
readonly parse: typeof parse,
|
|
147
|
+
readonly safeParse: typeof safeParse,
|
|
148
|
+
readonly parseAsync: typeof parseAsync,
|
|
149
|
+
readonly safeParseAsync: typeof safeParseAsync,
|
|
150
|
+
readonly is: typeof is,
|
|
151
|
+
readonly parser: typeof parser,
|
|
152
|
+
readonly useValidation: typeof useValidation,
|
|
153
|
+
readonly describe: typeof describe,
|
|
154
|
+
readonly isAsync: typeof isAsync,
|
|
155
|
+
readonly flatten: typeof flatten,
|
|
156
|
+
readonly toJsonSchema: typeof toJsonSchema,
|
|
157
|
+
} = {
|
|
158
|
+
string,
|
|
159
|
+
number,
|
|
160
|
+
bigint,
|
|
161
|
+
boolean,
|
|
162
|
+
unknown,
|
|
163
|
+
never,
|
|
164
|
+
null: null_,
|
|
165
|
+
undefined: undefined_,
|
|
166
|
+
literal,
|
|
167
|
+
enum: enum_,
|
|
168
|
+
date,
|
|
169
|
+
instance,
|
|
170
|
+
custom,
|
|
171
|
+
object,
|
|
172
|
+
strictObject,
|
|
173
|
+
looseObject,
|
|
174
|
+
partial,
|
|
175
|
+
array,
|
|
176
|
+
tuple,
|
|
177
|
+
record,
|
|
178
|
+
map,
|
|
179
|
+
set,
|
|
180
|
+
union,
|
|
181
|
+
variant,
|
|
182
|
+
intersect,
|
|
183
|
+
optional,
|
|
184
|
+
nullable,
|
|
185
|
+
nullish,
|
|
186
|
+
withDefault,
|
|
187
|
+
fallback,
|
|
188
|
+
lazy,
|
|
189
|
+
lazyAsync,
|
|
190
|
+
pipe,
|
|
191
|
+
check,
|
|
192
|
+
checkAsync,
|
|
193
|
+
transform,
|
|
194
|
+
transformAsync,
|
|
195
|
+
brand,
|
|
196
|
+
refine,
|
|
197
|
+
minLength,
|
|
198
|
+
maxLength,
|
|
199
|
+
length,
|
|
200
|
+
nonEmpty,
|
|
201
|
+
startsWith,
|
|
202
|
+
endsWith,
|
|
203
|
+
includes,
|
|
204
|
+
regex,
|
|
205
|
+
email,
|
|
206
|
+
url,
|
|
207
|
+
uuid,
|
|
208
|
+
isoDate,
|
|
209
|
+
trim,
|
|
210
|
+
toLowerCase,
|
|
211
|
+
toUpperCase,
|
|
212
|
+
min,
|
|
213
|
+
max,
|
|
214
|
+
integer,
|
|
215
|
+
multipleOf,
|
|
216
|
+
minItems,
|
|
217
|
+
maxItems,
|
|
218
|
+
parse,
|
|
219
|
+
safeParse,
|
|
220
|
+
parseAsync,
|
|
221
|
+
safeParseAsync,
|
|
222
|
+
is,
|
|
223
|
+
parser,
|
|
224
|
+
useValidation,
|
|
225
|
+
describe,
|
|
226
|
+
isAsync,
|
|
227
|
+
flatten,
|
|
228
|
+
toJsonSchema,
|
|
229
|
+
};
|
package/object.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/object`: an object whose keys are known when the
|
|
4
|
+
// schema is written.
|
|
5
|
+
//
|
|
6
|
+
// Three of them, and the only difference is what happens to a key the shape
|
|
7
|
+
// does not name:
|
|
8
|
+
//
|
|
9
|
+
// - [`object`] drops it. The right default for reading somebody else's
|
|
10
|
+
// payload, where a new field appearing upstream is not your emergency.
|
|
11
|
+
// - [`strictObject`] rejects it. The right answer for a configuration file or
|
|
12
|
+
// an internal API, where an unrecognised key is almost always a typo the
|
|
13
|
+
// user would rather hear about than have ignored.
|
|
14
|
+
// - [`looseObject`] keeps it. For a boundary that has to forward what it did
|
|
15
|
+
// not understand — a webhook that is re-signed, a document that round-trips
|
|
16
|
+
// through a form and must come back whole.
|
|
17
|
+
//
|
|
18
|
+
// One walk serves all three, because "which keys does the shape name" is the
|
|
19
|
+
// same question in each and only the fourth line of the answer differs.
|
|
20
|
+
//
|
|
21
|
+
// # Why these take a shape and not a schema
|
|
22
|
+
//
|
|
23
|
+
// `partial({ name: string(), age: number() })`, not `partial(User)`. A schema
|
|
24
|
+
// here is a closure and a description, not a reified list of fields, so there
|
|
25
|
+
// is nothing on a built schema for `partial` to take apart — and adding one
|
|
26
|
+
// would mean every object schema carried its shape for the benefit of the
|
|
27
|
+
// callers that reshape it. Naming the shape once and passing it to both is the
|
|
28
|
+
// same amount of typing and keeps the built schema the size of its job:
|
|
29
|
+
//
|
|
30
|
+
// const account = { name: string(), age: number() };
|
|
31
|
+
// const Account = object(account);
|
|
32
|
+
// const Draft = partial(account);
|
|
33
|
+
//
|
|
34
|
+
// Valibot's `pick`, `omit` and `required` are absent for the same reason, and
|
|
35
|
+
// because over a shape they are object literal manipulation the language
|
|
36
|
+
// already has: `object({ name: account.name })` is `pick`.
|
|
37
|
+
//
|
|
38
|
+
// # Reading and writing
|
|
39
|
+
//
|
|
40
|
+
// Every read of an untrusted key goes through `plain-object.js`, and so does
|
|
41
|
+
// every write. `object.js` never touches `source[key]` or `out[key] = …`
|
|
42
|
+
// directly, and the reason is written down there.
|
|
43
|
+
|
|
44
|
+
import type { Shape, ShapeInput, ShapeOutput } from "./infer.js";
|
|
45
|
+
import type { Issue, PathBuffer } from "./issue.js";
|
|
46
|
+
import { issue } from "./issue.js";
|
|
47
|
+
import { isPlainObject, ownKeys, ownValue, plainRecord, put } from "./plain-object.js";
|
|
48
|
+
import type { Description, Result, Schema } from "./schema.js";
|
|
49
|
+
import {
|
|
50
|
+
collectAsync,
|
|
51
|
+
describe,
|
|
52
|
+
fail,
|
|
53
|
+
isAsync,
|
|
54
|
+
makeAsyncSchema,
|
|
55
|
+
makeSchema,
|
|
56
|
+
mergeIssues,
|
|
57
|
+
ok,
|
|
58
|
+
runAt,
|
|
59
|
+
} from "./schema.js";
|
|
60
|
+
import { optional } from "./optional.js";
|
|
61
|
+
|
|
62
|
+
/** What an object schema does with a key its shape does not name. */
|
|
63
|
+
type UnknownKeys = "strip" | "reject" | "keep";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pairs of `[key, schema]`, resolved once when the schema is built.
|
|
67
|
+
*
|
|
68
|
+
* `Object.keys(shape)` on every parse re-reads the same descriptors for the
|
|
69
|
+
* life of the process. The shape cannot change after construction, so the walk
|
|
70
|
+
* belongs at construction.
|
|
71
|
+
*/
|
|
72
|
+
function shapeEntries(shape: Shape): $ReadOnlyArray<[string, Schema<mixed, mixed>]> {
|
|
73
|
+
return Object.keys(shape).map((key) => [key, shape[key]]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Issues for every own key the shape does not name. Empty unless rejecting. */
|
|
77
|
+
function unknownKeyIssues(
|
|
78
|
+
source: { readonly [string]: mixed, ... },
|
|
79
|
+
named: Set<string>,
|
|
80
|
+
path: PathBuffer,
|
|
81
|
+
): Array<Issue> {
|
|
82
|
+
const issues: Array<Issue> = [];
|
|
83
|
+
for (const key of ownKeys(source)) {
|
|
84
|
+
if (!named.has(key)) {
|
|
85
|
+
path.push(key);
|
|
86
|
+
issues.push(issue("unknown_key", `unexpected key ${key}`, path));
|
|
87
|
+
path.pop();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return issues;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Copy every own key the shape does not name into the output, unchecked. */
|
|
94
|
+
function keepUnknownKeys(
|
|
95
|
+
source: { readonly [string]: mixed, ... },
|
|
96
|
+
named: Set<string>,
|
|
97
|
+
out: { [string]: mixed, ... },
|
|
98
|
+
): void {
|
|
99
|
+
for (const key of ownKeys(source)) {
|
|
100
|
+
if (!named.has(key)) {
|
|
101
|
+
put(out, key, ownValue(source, key));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The one object parser.
|
|
108
|
+
*
|
|
109
|
+
* `TOutput` and `TInput` are supplied by the three exported spellings; nothing
|
|
110
|
+
* at run time depends on them, which is why one implementation can serve an
|
|
111
|
+
* exact result, a `Partial` one and an inexact one alike.
|
|
112
|
+
*/
|
|
113
|
+
function buildObject<TOutput, TInput>(
|
|
114
|
+
shape: Shape,
|
|
115
|
+
unknownKeys: UnknownKeys,
|
|
116
|
+
): Schema<TOutput, TInput> {
|
|
117
|
+
const entries = shapeEntries(shape);
|
|
118
|
+
const named = new Set(entries.map(([key]) => key));
|
|
119
|
+
const description = (): Description => ({
|
|
120
|
+
kind: "object",
|
|
121
|
+
entries: entries.map(([key, schema]) => [key, describe(schema)]),
|
|
122
|
+
unknownKeys,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function assemble(
|
|
126
|
+
source: { readonly [string]: mixed, ... },
|
|
127
|
+
out: { [string]: mixed, ... },
|
|
128
|
+
found: Array<Issue>,
|
|
129
|
+
path: PathBuffer,
|
|
130
|
+
): Result<TOutput> {
|
|
131
|
+
const issues =
|
|
132
|
+
unknownKeys === "reject" ? found.concat(unknownKeyIssues(source, named, path)) : found;
|
|
133
|
+
if (issues.length > 0) {
|
|
134
|
+
return { ok: false, issues };
|
|
135
|
+
}
|
|
136
|
+
if (unknownKeys === "keep") {
|
|
137
|
+
keepUnknownKeys(source, named, out);
|
|
138
|
+
}
|
|
139
|
+
// $FlowFixMe[incompatible-type] every retained key was produced by the shape.
|
|
140
|
+
return ok(out as TOutput);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (entries.some(([, schema]) => isAsync(schema))) {
|
|
144
|
+
return makeAsyncSchema(async (value, path) => {
|
|
145
|
+
if (!isPlainObject(value)) {
|
|
146
|
+
return fail("type", "expected object", path);
|
|
147
|
+
}
|
|
148
|
+
const source = plainRecord(value);
|
|
149
|
+
const collected = await collectAsync(
|
|
150
|
+
entries.map(([key, schema]) => ({ keys: [key], schema, value: ownValue(source, key) })),
|
|
151
|
+
path,
|
|
152
|
+
);
|
|
153
|
+
const out: { [string]: mixed, ... } = {};
|
|
154
|
+
const found: Array<Issue> = [];
|
|
155
|
+
match (collected) {
|
|
156
|
+
{ok: true, values: const values} => {
|
|
157
|
+
entries.forEach(([key], index) => {
|
|
158
|
+
put(out, key, values[index]);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
{ok: false, issues: const collectedIssues} => {
|
|
162
|
+
for (const entry of collectedIssues) {
|
|
163
|
+
found.push(entry);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return assemble(source, out, found, path);
|
|
168
|
+
}, description);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return makeSchema((value, path) => {
|
|
172
|
+
if (!isPlainObject(value)) {
|
|
173
|
+
return fail("type", "expected object", path);
|
|
174
|
+
}
|
|
175
|
+
const source = plainRecord(value);
|
|
176
|
+
const out: { [string]: mixed, ... } = {};
|
|
177
|
+
const found: Array<Issue> = [];
|
|
178
|
+
for (const [key, schema] of entries) {
|
|
179
|
+
const result = runAt(schema, ownValue(source, key), path, key);
|
|
180
|
+
if (result.ok) {
|
|
181
|
+
put(out, key, result.value);
|
|
182
|
+
} else {
|
|
183
|
+
mergeIssues(found, result);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return assemble(source, out, found, path);
|
|
187
|
+
}, description);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** An object with exactly the shape's keys; anything else is dropped. */
|
|
191
|
+
export function object<TShape extends Shape>(
|
|
192
|
+
shape: TShape,
|
|
193
|
+
): Schema<ShapeOutput<TShape>, ShapeInput<TShape>> {
|
|
194
|
+
return buildObject(shape, "strip");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* An object that rejects keys the shape does not name.
|
|
199
|
+
*
|
|
200
|
+
* The unknown-key scan runs whether or not the fields parsed. Returning early
|
|
201
|
+
* on a field failure meant `{ name: 1, extra: true }` reported the wrong type
|
|
202
|
+
* of `name` and said nothing about `extra`, so fixing the first error revealed
|
|
203
|
+
* the second — which is the whole reason this validator collects issues
|
|
204
|
+
* instead of stopping at one.
|
|
205
|
+
*/
|
|
206
|
+
export function strictObject<TShape extends Shape>(
|
|
207
|
+
shape: TShape,
|
|
208
|
+
): Schema<ShapeOutput<TShape>, ShapeInput<TShape>> {
|
|
209
|
+
return buildObject(shape, "reject");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* An object that keeps the keys the shape does not name.
|
|
214
|
+
*
|
|
215
|
+
* The extra keys are in the parsed value and not in its type: they are
|
|
216
|
+
* `mixed`, because nothing validated them. The result type is inexact, which
|
|
217
|
+
* is Flow saying exactly that.
|
|
218
|
+
*/
|
|
219
|
+
export function looseObject<TShape extends Shape>(
|
|
220
|
+
shape: TShape,
|
|
221
|
+
): Schema<{ ...ShapeOutput<TShape>, ... }, { ...ShapeInput<TShape>, ... }> {
|
|
222
|
+
return buildObject(shape, "keep");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Every field of `shape`, each allowed to be missing.
|
|
227
|
+
*
|
|
228
|
+
* A key that was absent is present in the result holding `undefined`, rather
|
|
229
|
+
* than absent from it. One shape for the parsed value means a consumer reads
|
|
230
|
+
* `draft.name` without asking whether the key exists, and `Partial` says the
|
|
231
|
+
* same thing to the checker.
|
|
232
|
+
*/
|
|
233
|
+
export function partial<TShape extends Shape>(
|
|
234
|
+
shape: TShape,
|
|
235
|
+
): Schema<Partial<ShapeOutput<TShape>>, Partial<ShapeInput<TShape>>> {
|
|
236
|
+
const partialShape: { [string]: Schema<mixed, mixed>, ... } = {};
|
|
237
|
+
for (const key of Object.keys(shape)) {
|
|
238
|
+
put(partialShape, key, optional(shape[key]));
|
|
239
|
+
}
|
|
240
|
+
return buildObject(partialShape, "strip");
|
|
241
|
+
}
|
package/optional.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/optional`: a value that might not be there.
|
|
4
|
+
//
|
|
5
|
+
// Four wrappers and one rule each, and they are together because the question
|
|
6
|
+
// they answer is one question — what counts as "not there", and what should
|
|
7
|
+
// the parse produce when it happens.
|
|
8
|
+
//
|
|
9
|
+
// optional(string()) // undefined is fine, and stays undefined
|
|
10
|
+
// nullable(string()) // null is fine, and stays null
|
|
11
|
+
// nullish(string()) // either is fine
|
|
12
|
+
// withDefault(string(), "") // undefined becomes ""
|
|
13
|
+
// fallback(number(), 8080) // *anything* that fails becomes 8080
|
|
14
|
+
//
|
|
15
|
+
// `undefined` and `null` are kept apart on purpose. A missing key and a key
|
|
16
|
+
// explicitly set to null mean different things in every wire format uf reads —
|
|
17
|
+
// a PATCH body, a GraphQL response, a form that cleared a field — and a
|
|
18
|
+
// validator that folded them together would hand the application a value it
|
|
19
|
+
// could no longer tell apart. `nullish` is there for the boundary that really
|
|
20
|
+
// does not care.
|
|
21
|
+
//
|
|
22
|
+
// # Where the input type stops matching the output type
|
|
23
|
+
//
|
|
24
|
+
// [`withDefault`] and [`fallback`] are the two schemas whose `InferInput` is
|
|
25
|
+
// genuinely wider than their `InferOutput`: a default's input may be missing
|
|
26
|
+
// and its output never is, and a fallback accepts literally anything. That is
|
|
27
|
+
// not a quirk of the typing, it is what those two are for, and it is the case
|
|
28
|
+
// `InferInput` exists to describe.
|
|
29
|
+
|
|
30
|
+
import type { Description, Result, Schema } from "./schema.js";
|
|
31
|
+
import { describe, isAsync, makeAsyncSchema, makeSchema, ok, run, runAsync } from "./schema.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a wrapper that answers for some inputs itself and delegates the rest.
|
|
35
|
+
*
|
|
36
|
+
* `shortcut` returns a result to use as-is, or null to mean "ask the inner
|
|
37
|
+
* schema". Every wrapper in this module is that shape, and writing it once is
|
|
38
|
+
* also what keeps the asynchronous variants from being four more copies of the
|
|
39
|
+
* same three lines.
|
|
40
|
+
*/
|
|
41
|
+
function wrap<TOutput, TInput>(
|
|
42
|
+
inner: Schema<TOutput, mixed>,
|
|
43
|
+
shortcut: (value: mixed) => null | Result<TOutput>,
|
|
44
|
+
description: () => Description,
|
|
45
|
+
): Schema<TOutput, TInput> {
|
|
46
|
+
if (isAsync(inner)) {
|
|
47
|
+
return makeAsyncSchema((value, path) => {
|
|
48
|
+
const answered = shortcut(value);
|
|
49
|
+
return answered == null ? runAsync(inner, value, path) : Promise.resolve(answered);
|
|
50
|
+
}, description);
|
|
51
|
+
}
|
|
52
|
+
return makeSchema((value, path) => {
|
|
53
|
+
const answered = shortcut(value);
|
|
54
|
+
return answered == null ? run(inner, value, path) : answered;
|
|
55
|
+
}, description);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `undefined` passes through; anything else goes to `schema`. */
|
|
59
|
+
export function optional<TOutput, TInput>(
|
|
60
|
+
schema: Schema<TOutput, TInput>,
|
|
61
|
+
): Schema<void | TOutput, void | TInput> {
|
|
62
|
+
return wrap(
|
|
63
|
+
schema,
|
|
64
|
+
(value) => (value === undefined ? ok(undefined) : null),
|
|
65
|
+
() => ({ kind: "optional", inner: describe(schema) }),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `null` passes through; anything else goes to `schema`. */
|
|
70
|
+
export function nullable<TOutput, TInput>(
|
|
71
|
+
schema: Schema<TOutput, TInput>,
|
|
72
|
+
): Schema<null | TOutput, null | TInput> {
|
|
73
|
+
return wrap(
|
|
74
|
+
schema,
|
|
75
|
+
(value) => (value === null ? ok(null) : null),
|
|
76
|
+
() => ({ kind: "nullable", inner: describe(schema) }),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Either `null` or `undefined` passes through, unchanged. */
|
|
81
|
+
export function nullish<TOutput, TInput>(
|
|
82
|
+
schema: Schema<TOutput, TInput>,
|
|
83
|
+
): Schema<null | void | TOutput, null | void | TInput> {
|
|
84
|
+
return wrap(
|
|
85
|
+
schema,
|
|
86
|
+
(value) => (value == null ? ok(value === null ? null : undefined) : null),
|
|
87
|
+
() => ({ kind: "nullish", inner: describe(schema) }),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `undefined` becomes `value`; anything else goes to `schema`.
|
|
93
|
+
*
|
|
94
|
+
* The default is not validated. It is a value the program wrote, in the
|
|
95
|
+
* program's own types, and running it back through the parser would only be a
|
|
96
|
+
* chance for the two to disagree.
|
|
97
|
+
*/
|
|
98
|
+
export function withDefault<TOutput, TInput>(
|
|
99
|
+
schema: Schema<TOutput, TInput>,
|
|
100
|
+
value: TOutput,
|
|
101
|
+
): Schema<TOutput, void | TInput> {
|
|
102
|
+
return wrap(
|
|
103
|
+
schema,
|
|
104
|
+
(input) => (input === undefined ? ok(value) : null),
|
|
105
|
+
() => ({ kind: "default", inner: describe(schema) }),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A schema that never fails, substituting `value` when the inner one does.
|
|
111
|
+
*
|
|
112
|
+
* For the boundary where a bad field should not sink the whole payload — a
|
|
113
|
+
* cached response, a user preference — and where the alternative is a
|
|
114
|
+
* `safeParse` and a hand-written `if` at every call site. Its input type is
|
|
115
|
+
* `mixed`, because that is the truth: it accepts everything.
|
|
116
|
+
*/
|
|
117
|
+
export function fallback<TOutput>(
|
|
118
|
+
schema: Schema<TOutput, mixed>,
|
|
119
|
+
value: TOutput,
|
|
120
|
+
): Schema<TOutput, mixed> {
|
|
121
|
+
const description = (): Description => ({ kind: "fallback", inner: describe(schema) });
|
|
122
|
+
if (isAsync(schema)) {
|
|
123
|
+
return makeAsyncSchema(async (input, path) => {
|
|
124
|
+
const result = await runAsync(schema, input, path);
|
|
125
|
+
return result.ok ? result : ok(value);
|
|
126
|
+
}, description);
|
|
127
|
+
}
|
|
128
|
+
return makeSchema((input, path) => {
|
|
129
|
+
const result = run(schema, input, path);
|
|
130
|
+
return result.ok ? result : ok(value);
|
|
131
|
+
}, description);
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/validator",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.0-alpha.6",
|
|
4
|
+
"description": "Schemas that parse rather than assert: typed inference, issue paths and JSON Schema export, part of the Unified Toolchain for Flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -11,9 +11,24 @@
|
|
|
11
11
|
"directory": "packages/validator"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".": "./index.js"
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./action": "./action.js",
|
|
16
|
+
"./collection": "./collection.js",
|
|
17
|
+
"./infer": "./infer.js",
|
|
18
|
+
"./issue": "./issue.js",
|
|
19
|
+
"./json-schema": "./json-schema.js",
|
|
20
|
+
"./lazy": "./lazy.js",
|
|
21
|
+
"./namespace": "./namespace.js",
|
|
22
|
+
"./object": "./object.js",
|
|
23
|
+
"./optional": "./optional.js",
|
|
24
|
+
"./parse": "./parse.js",
|
|
25
|
+
"./pipe": "./pipe.js",
|
|
26
|
+
"./plain-object": "./plain-object.js",
|
|
27
|
+
"./primitive": "./primitive.js",
|
|
28
|
+
"./schema": "./schema.js",
|
|
29
|
+
"./union": "./union.js"
|
|
15
30
|
},
|
|
16
31
|
"files": [
|
|
17
|
-
"
|
|
32
|
+
"*.js"
|
|
18
33
|
]
|
|
19
34
|
}
|