В библиотеке Vest пользовательские правила позволяют
расширять встроенный механизм валидации и создавать собственные
проверки, адаптированные под бизнес-логику приложения. Такой подход
особенно важен в проектах, где стандартных валидаторов недостаточно:
проверка уникальности, сложные условия зависимости полей, асинхронные
запросы, интеграция с API, корпоративные ограничения и вычисляемые
правила.
Пользовательские правила строятся вокруг функции
enforce.extend(), которая добавляет новые методы в систему
enforce.
Механизм enforce представляет собой цепочку
валидаторов:
enforce(value).isNotEmpty().longerThan(3);
После расширения API появляется возможность создавать собственные методы:
enforce.extend({
isEven(value) {
return value % 2 === 0;
}
});
Теперь правило становится частью общей цепочки:
enforce(10).isEven();
import { enforce } from 'vest';
enforce.extend({
isPositive(value) {
return value > 0;
}
});
Использование:
test('amount', 'Число должно быть положительным', () => {
enforce(amount).isPositive();
});
Пользовательское правило должно возвращать:
true — проверка успешнаfalse — проверка не пройденаПример:
enforce.extend({
startsWithA(value) {
return value.startsWith('A');
}
});
Часто валидатору требуется дополнительный параметр.
enforce.extend({
minItems(value, min) {
return value.length >= min;
}
});
Использование:
enforce(tags).minItems(3);
enforce.extend({
inRange(value, min, max) {
return value >= min && value <= max;
}
});
Применение:
enforce(age).inRange(18, 60);
enforce.extend({
hasSpecialCharacter(value) {
return /[!@#$%^&*]/.test(value);
}
});
Использование:
enforce(password).hasSpecialCharacter();
Один из наиболее популярных сценариев пользовательской валидации.
enforce.extend({
strongPassword(value) {
return (
/[A-Z]/.test(value) &&
/[a-z]/.test(value) &&
/[0-9]/.test(value) &&
value.length >= 8
);
}
});
Пример:
test('password', 'Слабый пароль', () => {
enforce(password).strongPassword();
});
Пользовательское правило может использовать любые утилиты.
import isEmail from 'validator/lib/isEmail';
enforce.extend({
corporateEmail(value) {
return (
isEmail(value) &&
value.endsWith('@company.com')
);
}
});
Внутри пользовательского правила можно использовать существующие
методы enforce.
enforce.extend({
validUsername(value) {
enforce(value)
.longerThan(3)
.shorterThan(20);
return /^[a-z0-9_]+$/i.test(value);
}
});
enforce.extend({
validTaxNumber(value) {
if (!/^\d{12}$/.test(value)) {
return false;
}
const digits = value.split('').map(Number);
const checksum =
digits.slice(0, 11).reduce((acc, num) => acc + num, 0) % 10;
return checksum === digits[11];
}
});
enforce.extend({
uniqueItems(value) {
return new Set(value).size === value.length;
}
});
Использование:
enforce(selectedIds).uniqueItems();
enforce.extend({
hasRequiredFields(value, fields) {
return fields.every(field => {
return value[field] !== undefined;
});
}
});
Пример:
enforce(user).hasRequiredFields([
'name',
'email',
'role'
]);
Vest поддерживает асинхронные проверки через async и
Promise.
enforce.extend({
async usernameAvailable(value) {
const response = await fetch(`/api/users/${value}`);
const data = await response.json();
return !data.exists;
}
});
Использование:
test(
'username',
'Имя пользователя уже занято',
async () => {
await enforce(username).usernameAvailable();
}
);
Асинхронные правила особенно полезны при интеграции с сервером.
enforce.extend({
async validPromoCode(value) {
const response = await fetch(
`/api/promo/${value}`
);
const data = await response.json();
return data.valid;
}
});
enforce.extend({
async secureUsername(value) {
if (value.length < 5) {
return false;
}
const response = await fetch(
`/api/check/${value}`
);
const data = await response.json();
return data.available;
}
});
Лучше выносить правила в отдельный модуль.
import { enforce } from 'vest';
enforce.extend({
isAdult(value) {
return value >= 18;
},
validPhone(value) {
return /^\+7\d{10}$/.test(value);
},
containsOnlyLetters(value) {
return /^[a-zа-я]+$/i.test(value);
}
});
Подключение:
import './validators';
В крупных приложениях пользовательские правила часто разделяются по доменам.
enforce.extend({
securePassword(value) {
return (
value.length >= 10 &&
/[A-Z]/.test(value) &&
/\d/.test(value)
);
}
});
enforce.extend({
validCurrency(value) {
return ['USD', 'EUR', 'KZT'].includes(value);
}
});
function createRangeValidator(min, max) {
return value => value >= min && value <= max;
}
enforce.extend({
validScore: createRangeValidator(0, 100)
});
Vest поддерживает расширение типов.
declare module 'vest' {
interface EnforceCustomMatchers<R = void> {
isPrime(): R;
}
}
enforce.extend({
isPrime(value: number) {
if (value <= 1) {
return false;
}
for (let i = 2; i < value; i++) {
if (value % i === 0) {
return false;
}
}
return true;
}
});
Использование:
enforce(17).isPrime();
enforce.extend({
matches(value, compareValue) {
return value === compareValue;
}
});
Пример:
test('confirmPassword', 'Пароли не совпадают', () => {
enforce(confirmPassword).matches(password);
});
Пользовательские правила могут использовать глобальные данные приложения.
const forbiddenDomains = [
'spam.com',
'fake.com'
];
enforce.extend({
allowedDomain(value) {
const domain = value.split('@')[1];
return !forbiddenDomains.includes(domain);
}
});
Vest позволяет строить собственный DSL поверх
enforce.
enforce.extend({
productCode(value) {
return /^[A-Z]{3}-\d{4}$/.test(value);
}
});
Использование становится декларативным:
enforce(code).productCode();
enforce.extend({
futureDate(value) {
return new Date(value) > new Date();
}
});
enforce.extend({
adultBirthday(value) {
const birth = new Date(value);
const now = new Date();
const age =
now.getFullYear() - birth.getFullYear();
return age >= 18;
}
});
enforce.extend({
matchesRegex(value, pattern) {
return pattern.test(value);
}
});
Использование:
enforce(phone).matchesRegex(/^\+7\d{10}$/);
enforce.extend({
requiredIf(value, condition) {
if (!condition) {
return true;
}
return value !== '';
}
});
Пример:
enforce(companyName).requiredIf(isBusiness);
Нежелательно выбрасывать исключения без необходимости.
enforce.extend({
unsafe(value) {
return value.name.length > 3;
}
});
Если value.name отсутствует:
TypeError
enforce.extend({
safe(value) {
return value?.name?.length > 3;
}
});
Некоторые рекомендации:
Плохо:
enforce.extend({
hugeCalculation(value) {
return expensiveOperation(value);
}
});
Лучше:
const cache = new Map();
enforce.extend({
optimizedRule(value) {
if (cache.has(value)) {
return cache.get(value);
}
const result = expensiveOperation(value);
cache.set(value, result);
return result;
}
});
Пользовательские правила должны быть максимально чистыми.
Нежелательно:
enforce.extend({
trackValidation(value) {
analytics.send(value);
return true;
}
});
Правило должно заниматься исключительно проверкой данных.
describe('isPositive', () => {
test('валидное число', () => {
expect(
enforce(10).isPositive()
).toBeTruthy();
});
test('невалидное число', () => {
expect(
enforce(-5).isPositive()
).toBeFalsy();
});
});
test('usernameAvailable', async () => {
await expect(
enforce('admin')
.usernameAvailable()
).resolves.toBeTruthy();
});
Плохо:
enforce.extend({
invalid(value) {
return value;
}
});
Правильно:
enforce.extend({
valid(value) {
return Boolean(value);
}
});
Если имя совпадает со встроенным методом, пользовательское правило может переопределить стандартное поведение.
enforce.extend({
equals() {
return true;
}
});
Такое переопределение может привести к трудноуловимым ошибкам.
В больших приложениях обычно применяются:
import { enforce } from 'vest';
enforce.extend({
strongPassword(value) {
return (
value.length >= 8 &&
/[A-Z]/.test(value) &&
/\d/.test(value)
);
},
validPhone(value) {
return /^\+7\d{10}$/.test(value);
},
uniqueItems(value) {
return new Set(value).size === value.length;
},
futureDate(value) {
return new Date(value) > new Date();
},
matches(value, compareValue) {
return value === compareValue;
}
});
Использование:
test('password', 'Слабый пароль', () => {
enforce(password).strongPassword();
});
test('phone', 'Некорректный номер', () => {
enforce(phone).validPhone();
});
test('eventDate', 'Дата должна быть будущей', () => {
enforce(eventDate).futureDate();
});