@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/json-schema.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/json-schema`: a schema as a document somebody else can
|
|
4
|
+
// read.
|
|
5
|
+
//
|
|
6
|
+
// const { schema, unrepresentable } = toJsonSchema(Account);
|
|
7
|
+
// // { $schema: "…/2020-12/schema", type: "object", properties: { … } }
|
|
8
|
+
//
|
|
9
|
+
// The roadmap calls this "schema exports", and it is the reason `schema.js`
|
|
10
|
+
// makes every schema carry a [`Description`] beside its parse function. A
|
|
11
|
+
// closure cannot be read: a schema that was *only* the four lines that check a
|
|
12
|
+
// value can be run and can never be published, documented, handed to an
|
|
13
|
+
// OpenAPI generator, or turned into types for a consumer written in another
|
|
14
|
+
// language. The description is the readable half, and this module is the first
|
|
15
|
+
// consumer of it.
|
|
16
|
+
//
|
|
17
|
+
// # What "unrepresentable" means, and why it is a return value
|
|
18
|
+
//
|
|
19
|
+
// JSON Schema describes JSON. `map`, `set`, `bigint`, `date`, `instance` and
|
|
20
|
+
// `custom` describe values that JSON does not have, and `check` describes a
|
|
21
|
+
// predicate no declarative format can express. A converter has three options
|
|
22
|
+
// for those: throw, lie, or say so. Throwing makes one `Date` field fatal for a
|
|
23
|
+
// document that is otherwise fine. Lying — emitting `{}` and moving on — is
|
|
24
|
+
// what makes an exported schema quietly weaker than the one the application
|
|
25
|
+
// runs.
|
|
26
|
+
//
|
|
27
|
+
// So this says so. The node becomes `{}`, which accepts anything and is
|
|
28
|
+
// therefore *never wrong*, only imprecise, and the place it happened is
|
|
29
|
+
// reported in `unrepresentable` with the path to it. A caller that wants the
|
|
30
|
+
// strict behaviour asserts the list is empty; a caller documenting an API that
|
|
31
|
+
// happens to have one `Date` in it gets its document.
|
|
32
|
+
//
|
|
33
|
+
// # The exported document describes the input, not the output
|
|
34
|
+
//
|
|
35
|
+
// `pipe(string(), transform(Number))` exports as `{ "type": "string" }`. That
|
|
36
|
+
// is the whole point of a JSON Schema: it validates what arrives on the wire,
|
|
37
|
+
// and what arrives is the input. `InferOutput` is for the code on this side of
|
|
38
|
+
// the boundary; the document is for the code on the other side, which has
|
|
39
|
+
// never heard of the transform.
|
|
40
|
+
//
|
|
41
|
+
// The consequence is easy to get wrong, and this module got it wrong first: a
|
|
42
|
+
// step *after* a transform constrains the output, not the input.
|
|
43
|
+
// `pipe(string(), transform(Number), min(18))` must not export as `{ "type":
|
|
44
|
+
// "string", "minimum": 18 }`, which would ask a consumer to compare a string
|
|
45
|
+
// against a number. It exports as `{ "type": "string" }`, and `min after a
|
|
46
|
+
// transform` is reported as unrepresentable — because the schema does reject
|
|
47
|
+
// `"5"` and the document does not, and that is exactly what the list is for.
|
|
48
|
+
//
|
|
49
|
+
// `fallback` follows the same rule to its conclusion: a fallback accepts every
|
|
50
|
+
// value, so it exports as `{}`. Nothing is lost that JSON Schema could have
|
|
51
|
+
// held.
|
|
52
|
+
//
|
|
53
|
+
// # Recursion
|
|
54
|
+
//
|
|
55
|
+
// A `lazy` schema is a cycle, and the description of one is a thunk rather
|
|
56
|
+
// than a value for exactly that reason. Each `lazy` carries an identity, and
|
|
57
|
+
// the walk keeps a table from that identity to a name under `$defs`: the first
|
|
58
|
+
// visit converts the body, and every visit after it emits a `$ref`. A comment
|
|
59
|
+
// tree comes out as one definition and a reference to it, which is what JSON
|
|
60
|
+
// Schema is for.
|
|
61
|
+
//
|
|
62
|
+
// # What this is not, yet
|
|
63
|
+
//
|
|
64
|
+
// `uf prepare` lists a `GenerateValidatorTypes` step, and there is no code
|
|
65
|
+
// behind it — no crate reads a schema and writes a `.js.flow`. This module is
|
|
66
|
+
// the half that can exist without one: a description a generator would read,
|
|
67
|
+
// and a converter proving the description is complete enough to build
|
|
68
|
+
// something from. When that step is implemented it should consume
|
|
69
|
+
// [`describe`], not re-derive a shape from the source.
|
|
70
|
+
|
|
71
|
+
import type { Path } from "./issue.js";
|
|
72
|
+
import { put } from "./plain-object.js";
|
|
73
|
+
import type { Constraint, Description, Schema } from "./schema.js";
|
|
74
|
+
import { describe } from "./schema.js";
|
|
75
|
+
|
|
76
|
+
/** One node of a JSON Schema document. */
|
|
77
|
+
export type JsonSchemaNode = { readonly [string]: mixed, ... };
|
|
78
|
+
|
|
79
|
+
/** Somewhere the document is less precise than the schema it came from. */
|
|
80
|
+
export type Unrepresentable = {|
|
|
81
|
+
readonly path: Path,
|
|
82
|
+
readonly kind: string,
|
|
83
|
+
|};
|
|
84
|
+
|
|
85
|
+
/** A JSON Schema document, and everything JSON Schema could not say. */
|
|
86
|
+
export type JsonSchemaExport = {|
|
|
87
|
+
readonly schema: JsonSchemaNode,
|
|
88
|
+
readonly unrepresentable: $ReadOnlyArray<Unrepresentable>,
|
|
89
|
+
|};
|
|
90
|
+
|
|
91
|
+
const DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
92
|
+
|
|
93
|
+
/** `left` with `right`'s keywords on top, written prototype-safely. */
|
|
94
|
+
function merge(left: JsonSchemaNode, right: JsonSchemaNode): JsonSchemaNode {
|
|
95
|
+
const out: { [string]: mixed, ... } = {};
|
|
96
|
+
for (const key of Object.keys(left)) {
|
|
97
|
+
put(out, key, left[key]);
|
|
98
|
+
}
|
|
99
|
+
for (const key of Object.keys(right)) {
|
|
100
|
+
put(out, key, right[key]);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether a field may be absent.
|
|
107
|
+
*
|
|
108
|
+
* `optional`, `nullish` and a default all mean the key can be missing, and any
|
|
109
|
+
* of them may be wrapped in the steps of a pipeline — `pipe(optional(string()),
|
|
110
|
+
* …)` is an `optional` under a `constrained` — so the wrappers are unwrapped
|
|
111
|
+
* before the question is asked. `nullable` is not here: `null` is a value, and
|
|
112
|
+
* a key holding it is present.
|
|
113
|
+
*/
|
|
114
|
+
function mayBeAbsent(description: Description): boolean {
|
|
115
|
+
return match (description) {
|
|
116
|
+
{kind: "optional", inner: _} => true,
|
|
117
|
+
{kind: "nullish", inner: _} => true,
|
|
118
|
+
{kind: "default", inner: _} => true,
|
|
119
|
+
{kind: "fallback", inner: _} => true,
|
|
120
|
+
{kind: "constrained", inner: const inner, constraint: _} => mayBeAbsent(inner),
|
|
121
|
+
{kind: "transformed", inner: const inner} => mayBeAbsent(inner),
|
|
122
|
+
_ => false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether a description is of a value some step already changed.
|
|
128
|
+
*
|
|
129
|
+
* Only the spine of pipeline wrappers is followed. A transform *inside* an
|
|
130
|
+
* object's field does not make the object transformed: the field's own node is
|
|
131
|
+
* where that is handled.
|
|
132
|
+
*/
|
|
133
|
+
function describesTransformedValue(description: Description): boolean {
|
|
134
|
+
return match (description) {
|
|
135
|
+
{kind: "transformed", inner: _} => true,
|
|
136
|
+
{kind: "constrained", inner: const inner, constraint: _} => describesTransformedValue(inner),
|
|
137
|
+
_ => false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** What to call a constraint in the `unrepresentable` list. */
|
|
142
|
+
function labelFor(constraint: Constraint): string {
|
|
143
|
+
return constraint.kind === "opaque" ? `check ${constraint.label}` : constraint.kind;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The JSON Schema keywords one `pipe` step contributes, if any. */
|
|
147
|
+
function keywordsFor(constraint: Constraint): JsonSchemaNode {
|
|
148
|
+
return match (constraint) {
|
|
149
|
+
{kind: "minLength", value: const value} => { minLength: value },
|
|
150
|
+
{kind: "maxLength", value: const value} => { maxLength: value },
|
|
151
|
+
{kind: "length", value: const value} => { minLength: value, maxLength: value },
|
|
152
|
+
{kind: "minItems", value: const value} => { minItems: value },
|
|
153
|
+
{kind: "maxItems", value: const value} => { maxItems: value },
|
|
154
|
+
{kind: "min", value: const value} => { minimum: value },
|
|
155
|
+
{kind: "max", value: const value} => { maximum: value },
|
|
156
|
+
{kind: "integer"} => { type: "integer" },
|
|
157
|
+
{kind: "multipleOf", value: const value} => { multipleOf: value },
|
|
158
|
+
{kind: "pattern", source: const source} => { pattern: source },
|
|
159
|
+
{kind: "format", name: const name} => { format: name },
|
|
160
|
+
{kind: "brand", name: const name} => { title: name },
|
|
161
|
+
{kind: "opaque", label: _} => {},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Convert `schema` to a JSON Schema document.
|
|
167
|
+
*
|
|
168
|
+
* The walk keeps three pieces of state — the definitions a recursive schema
|
|
169
|
+
* needs, the names already given out, and the places JSON Schema could not
|
|
170
|
+
* say what the schema meant — which is why it is a closure over a converter
|
|
171
|
+
* rather than a free function.
|
|
172
|
+
*/
|
|
173
|
+
export function toJsonSchema(schema: Schema<mixed, mixed>): JsonSchemaExport {
|
|
174
|
+
const definitions: { [string]: JsonSchemaNode, ... } = {};
|
|
175
|
+
const names = new Map<symbol, string>();
|
|
176
|
+
const unrepresentable: Array<Unrepresentable> = [];
|
|
177
|
+
|
|
178
|
+
function unsupported(kind: string, path: Path): JsonSchemaNode {
|
|
179
|
+
unrepresentable.push({ path: path.slice(), kind });
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function object(
|
|
184
|
+
entries: $ReadOnlyArray<[string, Description]>,
|
|
185
|
+
unknownKeys: "strip" | "reject" | "keep",
|
|
186
|
+
path: Path,
|
|
187
|
+
): JsonSchemaNode {
|
|
188
|
+
const properties: { [string]: JsonSchemaNode, ... } = {};
|
|
189
|
+
const required: Array<string> = [];
|
|
190
|
+
for (const [key, inner] of entries) {
|
|
191
|
+
put(properties, key, convert(inner, path.concat(key)));
|
|
192
|
+
if (!mayBeAbsent(inner)) {
|
|
193
|
+
required.push(key);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const base = { type: "object", properties, required };
|
|
197
|
+
return unknownKeys === "reject" ? merge(base, { additionalProperties: false }) : base;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function recursive(id: symbol, inner: () => Description, path: Path): JsonSchemaNode {
|
|
201
|
+
const already = names.get(id);
|
|
202
|
+
if (already != null) {
|
|
203
|
+
return { $ref: `#/$defs/${already}` };
|
|
204
|
+
}
|
|
205
|
+
const name = `definition${String(names.size)}`;
|
|
206
|
+
names.set(id, name);
|
|
207
|
+
put(definitions, name, convert(inner(), path));
|
|
208
|
+
return { $ref: `#/$defs/${name}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function convert(description: Description, path: Path): JsonSchemaNode {
|
|
212
|
+
return match (description) {
|
|
213
|
+
{kind: "unknown"} => {},
|
|
214
|
+
{kind: "never"} => { not: {} },
|
|
215
|
+
{kind: "string"} => { type: "string" },
|
|
216
|
+
{kind: "number"} => { type: "number" },
|
|
217
|
+
{kind: "boolean"} => { type: "boolean" },
|
|
218
|
+
{kind: "null"} => { type: "null" },
|
|
219
|
+
{kind: "bigint"} => unsupported("bigint", path),
|
|
220
|
+
{kind: "undefined"} => unsupported("undefined", path),
|
|
221
|
+
{kind: "date"} => unsupported("date", path),
|
|
222
|
+
{kind: "instance", name: const name} => unsupported(`instance ${name}`, path),
|
|
223
|
+
{kind: "custom", name: const name} => unsupported(`custom ${name}`, path),
|
|
224
|
+
{kind: "map", key: _, value: _} => unsupported("map", path),
|
|
225
|
+
{kind: "set", item: _} => unsupported("set", path),
|
|
226
|
+
{kind: "literal", value: const value} => { const: value },
|
|
227
|
+
{kind: "enum", values: const values} => { enum: values.slice() },
|
|
228
|
+
{kind: "array", item: const item} =>
|
|
229
|
+
{
|
|
230
|
+
type: "array",
|
|
231
|
+
items: convert(item, path.concat("*")),
|
|
232
|
+
},
|
|
233
|
+
{kind: "tuple", items: const items} =>
|
|
234
|
+
{
|
|
235
|
+
type: "array",
|
|
236
|
+
prefixItems: items.map((item, index) => convert(item, path.concat(String(index)))),
|
|
237
|
+
minItems: items.length,
|
|
238
|
+
maxItems: items.length,
|
|
239
|
+
},
|
|
240
|
+
{kind: "record", value: const value} =>
|
|
241
|
+
{
|
|
242
|
+
type: "object",
|
|
243
|
+
additionalProperties: convert(value, path.concat("*")),
|
|
244
|
+
},
|
|
245
|
+
{kind: "object", entries: const entries, unknownKeys: const unknownKeys} =>
|
|
246
|
+
object(entries, unknownKeys, path),
|
|
247
|
+
{kind: "union", options: const options} =>
|
|
248
|
+
{
|
|
249
|
+
anyOf: options.map((option) => convert(option, path)),
|
|
250
|
+
},
|
|
251
|
+
{kind: "variant", key: _, branches: const branches} =>
|
|
252
|
+
{
|
|
253
|
+
oneOf: branches.map(([, branch]) => convert(branch, path)),
|
|
254
|
+
},
|
|
255
|
+
{kind: "intersect", parts: const parts} =>
|
|
256
|
+
{
|
|
257
|
+
allOf: parts.map((part) => convert(part, path)),
|
|
258
|
+
},
|
|
259
|
+
{kind: "optional", inner: const inner} => convert(inner, path),
|
|
260
|
+
{kind: "default", inner: const inner} => convert(inner, path),
|
|
261
|
+
{kind: "nullable", inner: const inner} =>
|
|
262
|
+
{
|
|
263
|
+
anyOf: [convert(inner, path), { type: "null" }],
|
|
264
|
+
},
|
|
265
|
+
{kind: "nullish", inner: const inner} =>
|
|
266
|
+
{
|
|
267
|
+
anyOf: [convert(inner, path), { type: "null" }],
|
|
268
|
+
},
|
|
269
|
+
{kind: "fallback", inner: _} => {},
|
|
270
|
+
{kind: "transformed", inner: const inner} => convert(inner, path),
|
|
271
|
+
{kind: "lazy", id: const id, inner: const inner} => recursive(id, inner, path),
|
|
272
|
+
{kind: "constrained", inner: const inner, constraint: const constraint} =>
|
|
273
|
+
constrained(inner, constraint, path),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A pipeline step's keywords on top of what it refined.
|
|
279
|
+
*
|
|
280
|
+
* Two of them contribute nothing. A `check`'s predicate is a closure, and a
|
|
281
|
+
* step that sits above a `transform` is about the output rather than the
|
|
282
|
+
* input this document describes. Both leave the node alone and add a line
|
|
283
|
+
* saying the document is looser than the schema at that path.
|
|
284
|
+
*/
|
|
285
|
+
function constrained(inner: Description, constraint: Constraint, path: Path): JsonSchemaNode {
|
|
286
|
+
const node = convert(inner, path);
|
|
287
|
+
if (describesTransformedValue(inner)) {
|
|
288
|
+
unrepresentable.push({
|
|
289
|
+
path: path.slice(),
|
|
290
|
+
kind: `${labelFor(constraint)} after a transform`,
|
|
291
|
+
});
|
|
292
|
+
return node;
|
|
293
|
+
}
|
|
294
|
+
if (constraint.kind === "opaque") {
|
|
295
|
+
unrepresentable.push({ path: path.slice(), kind: labelFor(constraint) });
|
|
296
|
+
return node;
|
|
297
|
+
}
|
|
298
|
+
return merge(node, keywordsFor(constraint));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const root = convert(describe(schema), []);
|
|
302
|
+
const document =
|
|
303
|
+
Object.keys(definitions).length === 0
|
|
304
|
+
? merge({ $schema: DIALECT }, root)
|
|
305
|
+
: merge(merge({ $schema: DIALECT }, root), { $defs: definitions });
|
|
306
|
+
return { schema: document, unrepresentable };
|
|
307
|
+
}
|
package/lazy.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/validator/lazy`: a schema that does not exist yet.
|
|
4
|
+
//
|
|
5
|
+
// A comment has replies, and a reply is a comment. There is no order in which
|
|
6
|
+
// to write that down, because the initialiser of `Comment` would have to name
|
|
7
|
+
// `Comment`, and at that moment it is still `undefined`. So the schema is
|
|
8
|
+
// wrapped in a function, and the function is not called until the first parse:
|
|
9
|
+
//
|
|
10
|
+
// type Comment = {| text: string, replies: $ReadOnlyArray<Comment> |};
|
|
11
|
+
//
|
|
12
|
+
// const Comment: Schema<Comment> = lazy(() =>
|
|
13
|
+
// object({ text: string(), replies: array(Comment) }),
|
|
14
|
+
// );
|
|
15
|
+
//
|
|
16
|
+
// The annotation is the other half, and it is not optional. Flow will infer
|
|
17
|
+
// the type of almost every schema in this package, but it will not solve a
|
|
18
|
+
// type that mentions itself; the `Schema<Comment>` on the binding is where the
|
|
19
|
+
// cycle is cut, and `infer.js` says so among the other limits.
|
|
20
|
+
//
|
|
21
|
+
// # Why the result is memoised
|
|
22
|
+
//
|
|
23
|
+
// Without it, every node of the tree calls `build()` and gets a fresh object
|
|
24
|
+
// graph — a thousand-comment thread would construct a thousand schemas, none
|
|
25
|
+
// of which is reused, on a parse that should have allocated nothing. With it,
|
|
26
|
+
// recursion costs one closure for the whole schema.
|
|
27
|
+
//
|
|
28
|
+
// # Why there are two of these
|
|
29
|
+
//
|
|
30
|
+
// Everything else in the package works out whether it is asynchronous when it
|
|
31
|
+
// is built: `object` asks its fields, `array` asks its item. A lazy schema
|
|
32
|
+
// cannot ask, because the thing it would ask does not exist yet, and building
|
|
33
|
+
// it to find out is the infinite loop the laziness was there to avoid.
|
|
34
|
+
//
|
|
35
|
+
// So the answer is declared instead of discovered. [`lazy`] is synchronous and
|
|
36
|
+
// [`lazyAsync`] is not, and a recursive schema with a `checkAsync` anywhere
|
|
37
|
+
// inside it must use the second — otherwise the objects and arrays above it
|
|
38
|
+
// build their synchronous halves, and the first `await` that never happens
|
|
39
|
+
// surfaces as a thrown "use safeParseAsync" from the middle of a parse rather
|
|
40
|
+
// than as a type the caller could have seen.
|
|
41
|
+
|
|
42
|
+
import type { Description, Schema } from "./schema.js";
|
|
43
|
+
import { describe, makeAsyncSchema, makeSchema, run, runAsync } from "./schema.js";
|
|
44
|
+
|
|
45
|
+
/** Build once, then hand back the same schema for the life of the process. */
|
|
46
|
+
function memoise<TOutput, TInput>(
|
|
47
|
+
build: () => Schema<TOutput, TInput>,
|
|
48
|
+
): () => Schema<TOutput, TInput> {
|
|
49
|
+
let built: null | Schema<TOutput, TInput> = null;
|
|
50
|
+
return () => {
|
|
51
|
+
const already = built;
|
|
52
|
+
if (already != null) {
|
|
53
|
+
return already;
|
|
54
|
+
}
|
|
55
|
+
const made = build();
|
|
56
|
+
built = made;
|
|
57
|
+
return made;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A synchronous schema built on first use.
|
|
63
|
+
*
|
|
64
|
+
* `id` is what a converter keys its definitions on: `json-schema.js` sees the
|
|
65
|
+
* same symbol every time it walks back around the cycle, which is how a
|
|
66
|
+
* recursive schema becomes a `$ref` rather than a stack overflow.
|
|
67
|
+
*/
|
|
68
|
+
export function lazy<TOutput, TInput = mixed>(
|
|
69
|
+
build: () => Schema<TOutput, TInput>,
|
|
70
|
+
): Schema<TOutput, TInput> {
|
|
71
|
+
const resolve = memoise(build);
|
|
72
|
+
const id = Symbol("lazy");
|
|
73
|
+
const description = (): Description => ({
|
|
74
|
+
kind: "lazy",
|
|
75
|
+
id,
|
|
76
|
+
inner: () => describe(resolve()),
|
|
77
|
+
});
|
|
78
|
+
return makeSchema((value, path) => run(resolve(), value, path), description);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A recursive schema with an asynchronous step somewhere inside it. */
|
|
82
|
+
export function lazyAsync<TOutput, TInput = mixed>(
|
|
83
|
+
build: () => Schema<TOutput, TInput>,
|
|
84
|
+
): Schema<TOutput, TInput> {
|
|
85
|
+
const resolve = memoise(build);
|
|
86
|
+
const id = Symbol("lazyAsync");
|
|
87
|
+
const description = (): Description => ({
|
|
88
|
+
kind: "lazy",
|
|
89
|
+
id,
|
|
90
|
+
inner: () => describe(resolve()),
|
|
91
|
+
});
|
|
92
|
+
return makeAsyncSchema((value, path) => runAsync(resolve(), value, path), description);
|
|
93
|
+
}
|
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
|
+
};
|