@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.
@@ -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
+ );