Механизм enforce — центральный инструмент библиотеки Vest для
декларативной проверки значений. В отличие от обычных валидаторов,
построенных вокруг условных конструкций, enforce формирует
цепочку правил, которая читается как естественное описание
ограничений.
Базовый пример:
import { enforce } from 'vest';
enforce('admin').equals('admin');
Проверка проходит успешно. Если условие нарушается — выбрасывается исключение.
enforce(10).greaterThan(100);
Результат:
EnforceError
Главная особенность enforce — расширяемость. Система
позволяет:
Расширение выполняется через метод enforce.extend.
Сигнатура:
enforce.extend(object);
Каждое свойство объекта становится новым методом проверки.
Пример:
import { enforce } from 'vest';
enforce.extend({
isEven(value) {
return value % 2 === 0;
},
});
Использование:
enforce(12).isEven();
Если проверка возвращает false, enforce
генерирует ошибку.
enforce(7).isEven();
Пользовательское правило получает значение первым аргументом.
enforce.extend({
validator(value) {
return true;
},
});
Возвращаемые значения:
| Значение | Результат |
|---|---|
true |
Проверка успешна |
false |
Проверка провалена |
string |
Ошибка с текстом |
Promise |
Асинхронная проверка |
Вместо false можно вернуть строку.
enforce.extend({
isPositive(value) {
return value > 0 || 'Число должно быть положительным';
},
});
Использование:
enforce(-10).isPositive();
Ошибка будет содержать пользовательское сообщение.
Дополнительные аргументы передаются после проверяемого значения.
enforce.extend({
longerThan(value, min) {
return value.length > min;
},
});
Использование:
enforce('Alexander').longerThan(5);
Правила могут принимать произвольное количество аргументов.
enforce.extend({
between(value, min, max) {
return value >= min && value <= max;
},
});
Пример:
enforce(15).between(10, 20);
Каждая цепочка сохраняет текущее значение.
enforce.extend({
startsWithUppercase(value) {
return /^[A-Z]/.test(value);
},
});
enforce('John').startsWithUppercase();
Расширения особенно полезны в бизнес-логике.
enforce.extend({
isIIN(value) {
return /^\d{12}$/.test(value);
},
});
Использование:
enforce('990101300123').isIIN();
enforce.extend({
isBIN(value) {
return /^\d{12}$/.test(value);
},
});
enforce.extend({
isKzPhone(value) {
return /^(\+7|8)\d{10}$/.test(value);
},
});
Методы можно вызывать цепочкой.
enforce.extend({
hasUppercase(value) {
return /[A-Z]/.test(value);
},
hasDigit(value) {
return /\d/.test(value);
},
});
Использование:
enforce('Password1')
.hasUppercase()
.hasDigit();
testРасширения полностью интегрируются в Vest Suite.
import { create, test, enforce } from 'vest';
enforce.extend({
isAdult(value) {
return value >= 18;
},
});
const suite = create((data) => {
test('age', 'Возраст меньше допустимого', () => {
enforce(data.age).isAdult();
});
});
enforce поддерживает Promise.
enforce.extend({
async usernameAvailable(value) {
const response = await fetch(`/api/users/${value}`);
return response.status === 404;
},
});
Использование:
await enforce('alex').usernameAvailable();
test('username', 'Имя уже занято', async () => {
await enforce(data.username).usernameAvailable();
});
Расширения могут использовать другие проверки.
enforce.extend({
strongPassword(value) {
enforce(value).longerThan(7);
return (
/[A-Z]/.test(value) &&
/\d/.test(value)
);
},
});
Крупные приложения часто формируют собственный язык валидации.
enforce.extend({
validPrice(value) {
return value >= 0;
},
validQuantity(value) {
return Number.isInteger(value) && value > 0;
},
validSku(value) {
return /^[A-Z]{3}-\d+$/.test(value);
},
});
Использование:
enforce(product.price).validPrice();
enforce(product.quantity).validQuantity();
enforce(product.sku).validSku();
Такой подход значительно повышает читаемость Suite.
Расширения удобно выносить в отдельные файлы.
validators/user.jsimport { enforce } from 'vest';
enforce.extend({
validUsername(value) {
return /^[a-z0-9_]{4,20}$/i.test(value);
},
validDisplayName(value) {
return value.length >= 2;
},
});
import './validators/user';
Расширения глобальны для экземпляра enforce.
После регистрации метод доступен везде:
enforce.extend({
customRule(value) {
return true;
},
});
enforce('x').customRule();
Поэтому рекомендуется:
Если имя совпадает с существующим методом, новое правило переопределит старое.
enforce.extend({
equals() {
return true;
},
});
Подобная практика опасна, поскольку ломает встроенное поведение.
Расширения подходят для сложных структур.
enforce.extend({
hasRequiredAddress(value) {
return (
value &&
value.city &&
value.street &&
value.zipcode
);
},
});
Использование:
enforce(address).hasRequiredAddress();
enforce.extend({
notEmptyArray(value) {
return Array.isArray(value) && value.length > 0;
},
});
enforce.extend({
allPositive(value) {
return value.every((item) => item > 0);
},
});
Правило может выбрасывать ошибки вручную.
enforce.extend({
safeJson(value) {
try {
JSON.parse(value);
return true;
} catch {
throw new Error('Некорректный JSON');
}
},
});
enforce.extend({
validDiscount(value, product) {
if (product.isPremium) {
return value <= 50;
}
return value <= 20;
},
});
enforce.extend({
futureDate(value) {
return new Date(value) > new Date();
},
});
enforce.extend({
validDateRange(value, endDate) {
return new Date(value) < new Date(endDate);
},
});
enforce.extend({
secureUrl(value) {
return /^https:\/\//.test(value);
},
});
enforce.extend({
corporateEmail(value, domain) {
return value.endsWith(`@${domain}`);
},
});
Иногда удобно создавать расширения динамически.
function minLengthRule(min) {
return {
[`min${min}`](value) {
return value.length >= min;
},
};
}
enforce.extend(minLengthRule(10));
Использование:
enforce('longpassword').min10();
function createEnumRule(values) {
return {
inEnum(value) {
return values.includes(value);
},
};
}
enforce.extend(
createEnumRule(['admin', 'moderator'])
);
В TypeScript необходимо расширять интерфейсы типов.
declare module 'vest' {
interface EnforceCustomMatchers<R = unknown> {
isEven(): R;
longerThan(length: number): R;
}
}
После этого IDE начинает распознавать пользовательские методы.
enforce.extend({
longerThan(value: string, min: number) {
return value.length > min;
},
});
enforce.extend({
notEmpty<T>(value: T[]) {
return value.length > 0;
},
});
enforce.extend({
validRole(value) {
return [
'admin',
'manager',
'user',
].includes(value);
},
validCurrency(value) {
return ['KZT', 'USD', 'EUR'].includes(value);
},
validLanguage(value) {
return ['ru', 'kk', 'en'].includes(value);
},
});
test('email', 'Некорректный email', () => {
enforce(formData.email)
.isNotBlank()
.matches(/.+@.+\..+/);
});
Собственные расширения позволяют полностью убрать повторение логики между компонентами.
Рекомендуется:
Плохой пример:
enforce.extend({
expensiveRule(value) {
const regex = new RegExp('^abc');
return regex.test(value);
},
});
Лучше:
const regex = /^abc/;
enforce.extend({
expensiveRule(value) {
return regex.test(value);
},
});
Распространённая структура:
validators/
├── common.js
├── auth.js
├── billing.js
├── products.js
├── users.js
└── index.js
Поскольку расширения являются обычными функциями, их удобно тестировать отдельно.
describe('isEven', () => {
test('валидное число', () => {
expect(() => {
enforce(10).isEven();
}).not.toThrow();
});
test('невалидное число', () => {
expect(() => {
enforce(5).isEven();
}).toThrow();
});
});
enforce.extend({
canPurchaseAlcohol(user) {
return (
user.age >= 21 &&
user.country !== 'restricted'
);
},
});
Без расширений:
if (
value.length < 8 ||
!/[A-Z]/.test(value) ||
!/\d/.test(value)
) {
throw new Error();
}
С расширениями:
enforce(value).strongPassword();
Именно этот подход делает enforce мощным инструментом
для построения масштабируемой системы валидации.