Skip to content

Commit df49f0e

Browse files
committed
feat(validation): enhance the validation rule types and custom validators
1 parent 62bb209 commit df49f0e

File tree

1 file changed

+79
-3
lines changed

1 file changed

+79
-3
lines changed

primitives/filed-form /src/form-core/validation.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ import { isEqual, isNil } from 'skyroc-utils';
55

66
import type { StoreValue } from '../types/formStore';
77

8-
/* ---------- Types ---------- */
8+
/**
9+
* Discriminated union describing the expected value type for validation.
10+
* Determines which built-in type checks will run for a rule.
11+
*
12+
* - 'string': validates string length and pattern
13+
* - 'number': validates numeric range and equality
14+
* - 'integer': validates integers only with optional min/max
15+
* - 'float': validates finite non-integer numbers
16+
* - 'boolean': validates a boolean primitive
17+
* - 'date': validates a Date or date-like value with optional min/max
18+
* - 'enum': validates membership in a provided enum array (deep-equals)
19+
* - 'email': validates a simple email pattern
20+
* - 'hex': validates hex color strings (#RGB[A], #RRGGBB[AA])
21+
* - 'regexp': validates a RegExp instance or a compilable pattern string
22+
* - 'url': validates a URL parsable by the URL constructor
23+
*/
924
export type RuleType =
1025
| 'boolean'
1126
| 'date'
@@ -19,53 +34,91 @@ export type RuleType =
1934
| 'string'
2035
| 'url';
2136

37+
/** Custom validator signature. Return a message string to fail, or a falsy value to pass. May be async. */
2238
type Validator = (
2339
rule: Rule,
2440
value: StoreValue,
2541
values: StoreValue
2642
) => Promise<string | any> | string | undefined | null;
2743

44+
/** Validation rule configuration combining base, type-specific, and custom validations. */
2845
export interface Rule {
46+
/** Optional debounce time in milliseconds for async validators. */
2947
debounceMs?: number;
48+
/** Allowed values for type 'enum'. Compared by deep equality. */
3049
enum?: StoreValue[];
50+
/** Exact numeric value or exact string length, depending on type. */
3151
len?: number;
52+
/** Maximum numeric value or latest date/time allowed. */
3253
max?: number | Date | string;
54+
/** Maximum string length allowed (type 'string'). */
3355
maxLength?: number;
56+
/** Override default error/warning message. */
3457
message?: string;
58+
/** Minimum numeric value or earliest date/time allowed. */
3559
min?: number | Date | string;
60+
/** Minimum string length allowed (type 'string'). */
3661
minLength?: number;
62+
/** Regular expression to test a string value against. */
3763
pattern?: RegExp;
64+
/** Whether the field is required (non-empty). */
3865
required?: boolean;
66+
/** If true (default), skip checks when empty and not required. */
3967
skipIfEmpty?: boolean;
68+
/** Transform the raw value before validation. */
4069
transform?: (value: StoreValue) => StoreValue;
70+
/** Declares which built-in type validators should run. Defaults to 'string'. */
4171
type?: RuleType;
72+
/** Which trigger(s) cause this rule to run (e.g., 'change', 'blur'). */
4273
validateTrigger?: string | string[];
74+
/** Custom validator executed after base and type-specific checks. */
4375
validator?: Validator;
76+
/** Treat failures as warnings instead of errors when true. */
4477
warningOnly?: boolean;
78+
/** Disallow strings that contain only whitespace when true. */
4579
whitespace?: boolean;
4680
}
4781

82+
/** Execution strategy for running multiple rules. */
4883
export type RunMode = 'parallelAll' | 'parallelFirst' | 'serial';
4984

85+
/** Options controlling validation execution, including mode and triggers. */
5086
export type ValidateOptions = {
87+
/** Rule execution mode; see RunMode. */
5188
mode?: RunMode;
89+
/** Which trigger(s) initiated validation; used to filter rules. */
5290
trigger?: string | string[];
5391
};
5492

93+
/** Result of a single rule execution: error or warning message. */
5594
type Res = { err?: string; warn?: string };
95+
/** Context passed to a check function, including the rule and all form values. */
5696
type Ctx = { rule: Rule; value: any; values: StoreValue };
97+
/** Function type for an individual check strategy. */
5798
type Check = (ctx: Ctx) => Promise<Res> | Res;
5899

59-
/* ---------- Utils ---------- */
100+
/**
101+
* Utility functions for validation
102+
* Contains helper functions for checking empty values, whitespace, dates, and other common validations
103+
*/
104+
/** Successful validation result (no error and no warning). */
60105
const ok = (): Res => ({});
106+
/** Build a failure result, honoring rule.warningOnly and rule.message overrides. */
61107
const fail = (r: Rule, dft: string): Res => (r.warningOnly ? { warn: r.message || dft } : { err: r.message || dft });
62108

109+
/** True when value is null/undefined, empty string, or empty array. */
63110
const isEmpty = (v: any) =>
64111
isNil(v) || (typeof v === 'string' && v?.length === 0) || (Array.isArray(v) && v?.length === 0);
65112

113+
/** True when a string contains only whitespace characters. */
66114
const onlyWhitespace = (s: string) => s?.length > 0 && s?.trim().length === 0;
115+
/** True for finite numbers (not NaN/Infinity). */
67116
const isFiniteNumber = (v: any) => Number.isFinite(v);
68117

118+
/**
119+
* Normalize a date-like value to a millisecond timestamp.
120+
* Returns null when the value cannot be interpreted as a valid date/time.
121+
*/
69122
const toDateMs = (d: number | string | Date): number | null => {
70123
if (d instanceof Date) return Number.isNaN(d.getTime()) ? null : d.getTime();
71124
if (typeof d === 'number') return Number.isFinite(d) ? d : null;
@@ -76,8 +129,11 @@ const toDateMs = (d: number | string | Date): number | null => {
76129
return null;
77130
};
78131

132+
/** Simple email validation based on a non-whitespace/local@domain.tld pattern. */
79133
const isEmail = (s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
134+
/** Hex color validator supporting 3/4/6/8 digit forms with optional leading '#'. */
80135
const isHexColor = (s: string) => /^#?(?:[A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(s);
136+
/** Validates that a string can be parsed by the URL constructor. */
81137
const isURL = (s: string) => {
82138
try {
83139
new URL(s);
@@ -87,22 +143,33 @@ const isURL = (s: string) => {
87143
}
88144
};
89145

90-
/* ---------- Strategy Manager ---------- */
146+
/**
147+
* Strategy Manager for Rule Validation
148+
* Implements a flexible validation system using the Strategy pattern
149+
* Manages different types of validation rules and their execution
150+
*/
91151
class RuleChecker {
92152
private baseChecks: Check[] = [];
93153
private typeChecks: Record<RuleType, Check[]> = {} as any;
94154
private customCheck: Check | null = null;
95155

156+
/** Register a base check that always runs before type-specific and custom checks. */
96157
registerBase(check: Check) {
97158
this.baseChecks.push(check);
98159
}
160+
/** Register one or more checks for a specific RuleType. */
99161
registerType(type: RuleType, ...checks: Check[]) {
100162
this.typeChecks[type] = checks;
101163
}
164+
/** Register a single custom check that will run after base and type checks. */
102165
registerCustom(check: Check) {
103166
this.customCheck = check;
104167
}
105168

169+
/**
170+
* Run all applicable checks for a single rule against a value.
171+
* Applies transform first, then base checks, type checks, and finally the custom check.
172+
*/
106173
async check(value: any, rule: Rule, allValues: StoreValue): Promise<Res> {
107174
const r = rule || {};
108175
const v = typeof r.transform === 'function' ? r.transform(value) : value;
@@ -239,10 +306,19 @@ checker.registerCustom(async ({ rule: r, value: v, values }) => {
239306
});
240307

241308
/* ---------- API ---------- */
309+
/**
310+
* Validate a single rule against a value.
311+
*/
242312
export async function checkOneRule(value: any, rule: Rule, allValues: StoreValue): Promise<Res> {
243313
return checker.check(value, rule, allValues);
244314
}
245315

316+
/**
317+
* Validate multiple rules using the specified execution mode.
318+
* - serial: run sequentially and stop on first non-warning error
319+
* - parallelFirst: run in parallel and resolve on first non-warning error
320+
* - parallelAll: run all in parallel, collecting all errors and warnings
321+
*/
246322
export async function runRulesWithMode(
247323
value: any,
248324
rules: Rule[],

0 commit comments

Comments
 (0)