@structuralists/scaffolding 0.4.1 → 0.5.0
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/bun.lock +192 -76
- package/package.json +15 -15
- package/src/forms/CLAUDE.md +41 -18
- package/src/forms/plan.md +270 -0
- package/src/forms/useFormState/types.ts +10 -0
- package/src/forms/useFormState/useFormState.stories.tsx +196 -0
- package/src/forms/useFormState/useFormState.test-d.ts +209 -0
- package/src/forms/useFormState/useFormState.test.tsx +134 -0
- package/src/forms/useFormState/useFormState.ts +41 -5
- package/src/forms/validations/perField.ts +11 -0
- package/src/forms/validations/types.test-d.ts +74 -1
- package/src/forms/validations/types.ts +23 -0
- package/src/forms/validators/validators.test.ts +99 -0
- package/src/forms/validators/validators.ts +95 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Refinement, Validator } from '../validations/types';
|
|
2
|
+
|
|
3
|
+
// Standard-library validators. Each one declares its refinement explicitly —
|
|
4
|
+
// `Validator<Input, Excluded>` — even when it narrows nothing (`never`).
|
|
5
|
+
// The marker is attached with `Object.assign(fn, {} as Refinement<X>)`: a
|
|
6
|
+
// pure type-level tag, never read at runtime.
|
|
7
|
+
|
|
8
|
+
// Rules out null, undefined, and the empty string. The only refining
|
|
9
|
+
// validator in the baseline set: submit-time type becomes
|
|
10
|
+
// `Exclude<FieldType, null | undefined | ''>`.
|
|
11
|
+
export const notEmpty = (
|
|
12
|
+
fieldName: string,
|
|
13
|
+
): Validator<unknown, null | undefined | ''> =>
|
|
14
|
+
Object.assign(
|
|
15
|
+
(val: unknown) =>
|
|
16
|
+
val == null || val === '' ? `'${fieldName}' cannot be empty` : null,
|
|
17
|
+
{} as Refinement<null | undefined | ''>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Minimum string length. Nullish and empty values pass — requiredness is
|
|
21
|
+
// notEmpty's job; compose via allOf. Narrows nothing: TypeScript has no type
|
|
22
|
+
// for "string of at least n characters".
|
|
23
|
+
export const minLength = (
|
|
24
|
+
fieldName: string,
|
|
25
|
+
min: number,
|
|
26
|
+
): Validator<string | null | undefined> =>
|
|
27
|
+
Object.assign(
|
|
28
|
+
(val: string | null | undefined) =>
|
|
29
|
+
val != null && val !== '' && val.length < min
|
|
30
|
+
? `'${fieldName}' must be at least ${min} characters`
|
|
31
|
+
: null,
|
|
32
|
+
{} as Refinement,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Regex test. Nullish and empty values pass (compose with notEmpty for
|
|
36
|
+
// requiredness). Narrows nothing.
|
|
37
|
+
export const matches = (
|
|
38
|
+
fieldName: string,
|
|
39
|
+
pattern: RegExp,
|
|
40
|
+
description: string,
|
|
41
|
+
): Validator<string | null | undefined> =>
|
|
42
|
+
Object.assign(
|
|
43
|
+
(val: string | null | undefined) =>
|
|
44
|
+
val != null && val !== '' && !pattern.test(val)
|
|
45
|
+
? `'${fieldName}' must be ${description}`
|
|
46
|
+
: null,
|
|
47
|
+
{} as Refinement,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Minimum numeric value. Nullish passes. Narrows nothing.
|
|
51
|
+
export const min = (
|
|
52
|
+
fieldName: string,
|
|
53
|
+
minimum: number,
|
|
54
|
+
): Validator<number | null | undefined> =>
|
|
55
|
+
Object.assign(
|
|
56
|
+
(val: number | null | undefined) =>
|
|
57
|
+
val != null && val < minimum
|
|
58
|
+
? `'${fieldName}' must be at least ${minimum}`
|
|
59
|
+
: null,
|
|
60
|
+
{} as Refinement,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
type ExcludedOf<V> = V extends Refinement<infer Excluded> ? Excluded : never;
|
|
64
|
+
|
|
65
|
+
// The composed input is the *intersection* of the parts' inputs: a value the
|
|
66
|
+
// composite accepts must be acceptable to every part. Inferring `I` from the
|
|
67
|
+
// union of function types puts it in contravariant position, which is what
|
|
68
|
+
// makes TS produce the intersection. Computing it this way — rather than as
|
|
69
|
+
// a standalone `Input` type parameter inferred from the arguments — matters:
|
|
70
|
+
// a separate parameter resolves from whichever argument TS visits first
|
|
71
|
+
// (e.g. `unknown` from notEmpty) and then rejects narrower siblings when the
|
|
72
|
+
// call has no outer contextual type, as inside an inline constraints object.
|
|
73
|
+
type InputOf<Validators extends readonly ((val: never) => string | null)[]> =
|
|
74
|
+
Validators[number] extends (val: infer I) => string | null ? I : never;
|
|
75
|
+
|
|
76
|
+
// Composes validators on one field: first error wins. The refinement is the
|
|
77
|
+
// union of the parts' refinements — running all of them earns all of their
|
|
78
|
+
// narrowings. `const Validators` keeps each member's precise type so
|
|
79
|
+
// `ExcludedOf` can extract markers; bare functions contribute `never`.
|
|
80
|
+
export const allOf = <
|
|
81
|
+
const Validators extends readonly ((val: never) => string | null)[],
|
|
82
|
+
>(
|
|
83
|
+
...validators: Validators
|
|
84
|
+
): Validator<InputOf<Validators>, ExcludedOf<Validators[number]>> =>
|
|
85
|
+
Object.assign(
|
|
86
|
+
(val: InputOf<Validators>) => {
|
|
87
|
+
for (const validator of validators) {
|
|
88
|
+
// Safe: each part's declared input is a supertype of the intersection.
|
|
89
|
+
const error = (validator as (v: unknown) => string | null)(val);
|
|
90
|
+
if (error != null) return error;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
{} as Refinement<ExcludedOf<Validators[number]>>,
|
|
95
|
+
);
|