pocket-hook-form
Version:
pocket-store base hook form
130 lines (116 loc) • 3.83 kB
text/typescript
// src/utils/validate.ts
import type {FieldRules, FieldError, FormValue} from '../models/type';
/** Fast emptiness check for common field types. */
function isEmptyValue(v: unknown): boolean {
if (v == null) return true; // null | undefined
if (typeof v === 'string') {
// treat pure whitespace as empty
// micro-opt: manual trim-like scan to avoid allocation
for (let i = 0; i < v.length; i++) {
const c = v.charCodeAt(i);
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) return false; // ' ', \t, \n, \r
}
return true;
}
if (Array.isArray(v)) return v.length === 0;
return false;
}
/**
* Run validators in order with minimal allocations..
* - Avoids creating an array unless needed.
*/
export function runValidators<V, T extends FormValue>(
value: V,
all: any,
rules?: FieldRules<V, T>,
criteria: 'firstError' | 'all' = 'firstError',
): undefined | FieldError | FieldError[] {
if (!rules) return undefined;
const wantAll = criteria === 'all';
const isStr = typeof value === 'string';
const empty = isEmptyValue(value);
const hasRequired = !!rules.required;
// We'll hold the first error separately to avoid array allocs on firstError path.
let firstErr: FieldError | undefined;
let allErrs: FieldError[] | undefined;
const emit = (e: FieldError) => {
if (wantAll) {
if (!allErrs) allErrs = [e];
else allErrs.push(e);
return false; // continue collecting
}
if (!firstErr) firstErr = e;
return true; // stop (firstError)
};
// 1) required
if (hasRequired && empty) {
const msg =
typeof rules.required === 'string' ? rules.required : 'Required';
if (emit({type: 'required', message: msg})) return firstErr!;
}
// 2) If empty & NOT required → skip the rest
if (empty && !hasRequired) {
// nothing else to validate; return whatever we have (likely undefined)
return wantAll ? allErrs : firstErr;
}
// 3) minLength/maxLength (strings only)
if (isStr) {
const len = (value as unknown as string).length;
if (rules.minLength != null) {
const min =
typeof rules.minLength === 'number'
? rules.minLength
: rules.minLength.value;
if (len < min) {
const msg =
typeof rules.minLength === 'number'
? `Min length ${min}`
: rules.minLength.message ?? `Min length ${min}`;
if (emit({type: 'minLength', message: msg}) && !wantAll) {
return firstErr!;
}
}
}
if (rules.maxLength != null) {
const max =
typeof rules.maxLength === 'number'
? rules.maxLength
: rules.maxLength.value;
if (len > max) {
const msg =
typeof rules.maxLength === 'number'
? `Max length ${max}`
: rules.maxLength.message ?? `Max length ${max}`;
if (emit({type: 'maxLength', message: msg}) && !wantAll) {
return firstErr!;
}
}
}
if (rules.pattern) {
const re =
rules.pattern instanceof RegExp ? rules.pattern : rules.pattern.value;
if (!re.test(value as unknown as string)) {
const msg =
rules.pattern instanceof RegExp
? 'Invalid format'
: rules.pattern.message ?? 'Invalid format';
if (emit({type: 'pattern', message: msg}) && !wantAll) {
return firstErr!;
}
}
}
}
// 5) custom validate
if (rules.validate) {
const res = rules.validate(value as V, all);
if (res !== true && res !== undefined) {
const msg = typeof res === 'string' ? res : 'Invalid';
if (emit({type: 'validate', message: msg}) && !wantAll) {
return firstErr!;
}
}
}
// Return according to criteria mode
if (wantAll) return allErrs;
return firstErr;
}