File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
export type FoxValidationErrors = Record<string, unknown>;
export type FoxValidator = (value: string) => FoxValidationErrors | null;
function isEmpty(value: string): boolean {
return value.trim().length === 0;
}
function required(value: string): FoxValidationErrors | null {
return isEmpty(value) ? { required: true } : null;
}
function minLength(requiredLength: number): FoxValidator {
return (value) => {
if (isEmpty(value) || value.length >= requiredLength) {
return null;
}
return { minlength: { requiredLength, actualLength: value.length } };
};
}
function maxLength(requiredLength: number): FoxValidator {
return (value) => {
if (value.length <= requiredLength) {
return null;
}
return { maxlength: { requiredLength, actualLength: value.length } };
};
}
function pattern(requiredPattern: RegExp): FoxValidator {
return (value) => {
if (isEmpty(value) || requiredPattern.test(value)) {
return null;
}
return { pattern: { requiredPattern: requiredPattern.source, actualValue: value } };
};
}
function email(value: string): FoxValidationErrors | null {
if (isEmpty(value) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return null;
}
return { email: true };
}
const CHARACTER_KIND_PATTERNS = {
letter: /[a-zA-Z]/,
digit: /\d/,
symbol: /[^a-zA-Z0-9]/,
} as const;
export type FoxCharacterKind = keyof typeof CHARACTER_KIND_PATTERNS;
function countCharacterKinds(value: string): FoxCharacterKind[] {
return (Object.keys(CHARACTER_KIND_PATTERNS) as FoxCharacterKind[]).filter(
(kind) => CHARACTER_KIND_PATTERNS[kind].test(value)
);
}
function characterKinds(requiredKinds: number): FoxValidator {
return (value) => {
if (isEmpty(value)) {
return null;
}
const kinds = countCharacterKinds(value);
if (kinds.length >= requiredKinds) {
return null;
}
return { characterKinds: { requiredKinds, actualKinds: kinds.length, kinds } };
};
}
function noRepeatedCharacters(max: number): FoxValidator {
return (value) => {
if (isEmpty(value)) {
return null;
}
let run = 1;
for (let i = 1; i < value.length; i += 1) {
run = value[i] === value[i - 1] ? run + 1 : 1;
if (run > max) {
return { repeatedCharacters: { max, character: value[i] } };
}
}
return null;
};
}
function noSequentialCharacters(max: number): FoxValidator {
return (value) => {
if (isEmpty(value)) {
return null;
}
let ascending = 1;
let descending = 1;
for (let i = 1; i < value.length; i += 1) {
const step = value.charCodeAt(i) - value.charCodeAt(i - 1);
ascending = step === 1 ? ascending + 1 : 1;
descending = step === -1 ? descending + 1 : 1;
if (ascending > max || descending > max) {
return { sequentialCharacters: { max, at: i - max } };
}
}
return null;
};
}
function notContaining(
forbidden: string[] | (() => string[]),
options: { caseSensitive?: boolean } = {}
): FoxValidator {
return (value) => {
if (isEmpty(value)) {
return null;
}
const list = typeof forbidden === 'function' ? forbidden() : forbidden;
const haystack = options.caseSensitive ? value : value.toLowerCase();
for (const raw of list) {
const needle = options.caseSensitive ? raw : raw.toLowerCase();
if (needle.length > 0 && haystack.includes(needle)) {
return { notContaining: { forbidden: raw } };
}
}
return null;
};
}
function equalTo(other: () => string): FoxValidator {
return (value) => (isEmpty(value) || value === other() ? null : { equalTo: true });
}
function compose(validators: FoxValidator[]): FoxValidator {
return (value) => {
const merged = validators.reduce<FoxValidationErrors>((acc, validate) => {
const errors = validate(value);
return errors ? { ...acc, ...errors } : acc;
}, {});
return Object.keys(merged).length > 0 ? merged : null;
};
}
export const foxValidators = {
required,
minLength,
maxLength,
pattern,
email,
characterKinds,
noRepeatedCharacters,
noSequentialCharacters,
notContaining,
equalTo,
compose,
};
export interface FoxPasswordPolicy {
minLength?: number;
maxLength?: number;
kinds?: number;
maxRepeated?: number;
maxSequential?: number;
forbidden?: string[] | (() => string[]);
required?: boolean;
}
export function foxPasswordValidator(policy: FoxPasswordPolicy = {}): FoxValidator {
const {
minLength: min = 8,
maxLength: max,
kinds = 2,
maxRepeated,
maxSequential,
forbidden,
required: isRequired = false,
} = policy;
const validators: FoxValidator[] = [];
if (isRequired) validators.push(required);
validators.push(minLength(min));
if (max !== undefined) validators.push(maxLength(max));
validators.push(characterKinds(kinds));
if (maxRepeated !== undefined) validators.push(noRepeatedCharacters(maxRepeated));
if (maxSequential !== undefined) validators.push(noSequentialCharacters(maxSequential));
if (forbidden !== undefined) validators.push(notContaining(forbidden));
return compose(validators);
}