Проверка объектов — одна из ключевых задач при работе с формами, DTO, конфигурациями, API-ответами и вложенными структурами данных. Библиотека Vest предоставляет гибкий механизм декларативной валидации, позволяющий проверять как простые поля, так и сложные древовидные структуры.
Рассмотрим объект пользователя:
const user = {
firstName: 'Alex',
lastName: 'Johnson',
email: 'alex@example.com',
age: 27
};
Создание suite:
import { create, test, enforce } from 'vest';
const suite = create((data = {}) => {
test('firstName', 'Имя обязательно', () => {
enforce(data.firstName).isNotBlank();
});
test('lastName', 'Фамилия обязательна', () => {
enforce(data.lastName).isNotBlank();
});
test('email', 'Некорректный email', () => {
enforce(data.email).matches(/^\S+@\S+\.\S+$/);
});
test('age', 'Возраст должен быть больше 18', () => {
enforce(data.age).greaterThan(18);
});
});
Запуск проверки:
const result = suite(user);
console.log(result.hasErrors()); // false
Vest не требует полного соответствия структуры объекта. Если свойство отсутствует, его можно обработать вручную.
const user = {
firstName: 'Alex'
};
test('email', 'Email обязателен', () => {
enforce(user.email).isDefined();
});
Также можно комбинировать проверки:
test('email', 'Email обязателен', () => {
enforce(user.email).isDefined();
enforce(user.email).isNotBlank();
});
Объекты часто содержат вложенные структуры:
const user = {
profile: {
city: 'Berlin',
country: 'Germany'
}
};
Проверка:
const suite = create((data = {}) => {
test('profile.city', 'Город обязателен', () => {
enforce(data.profile.city).isNotBlank();
});
test('profile.country', 'Страна обязательна', () => {
enforce(data.profile.country).isNotBlank();
});
});
Хотя имя теста содержит точечную нотацию, Vest не интерпретирует путь автоматически. Это всего лишь идентификатор теста. Доступ к данным осуществляется вручную.
При отсутствии вложенного объекта возможна ошибка:
data.profile.city
Если profile === undefined, произойдёт исключение.
Безопасный вариант:
test('profile.city', 'Город обязателен', () => {
enforce(data.profile?.city).isNotBlank();
});
Либо:
const city = data.profile && data.profile.city;
enforce(city).isNotBlank();
Пример структуры:
const company = {
address: {
location: {
city: 'Paris',
zip: '75001'
}
}
};
Проверка:
const suite = create((data = {}) => {
test('address.location.city', 'Город обязателен', () => {
enforce(data.address?.location?.city).isNotBlank();
});
test('address.location.zip', 'ZIP обязателен', () => {
enforce(data.address?.location?.zip).isNotBlank();
});
});
Vest не содержит встроенного schema-based API, как Joi или Yup. Проверка структуры реализуется вручную.
Например:
test('profile', 'Профиль обязателен', () => {
enforce(data.profile).isObject();
});
Проверка массива:
test('roles', 'Роли должны быть массивом', () => {
enforce(data.roles).isArray();
});
Проверка типа строки:
test('username', 'Имя пользователя должно быть строкой', () => {
enforce(data.username).isString();
});
Пример:
const user = {
name: 'Alex',
roles: ['admin', 'editor']
};
Проверка массива:
test('roles', 'Должна быть хотя бы одна роль', () => {
enforce(data.roles).isArray();
enforce(data.roles.length).greaterThan(0);
});
Пример:
const data = {
users: [
{ name: 'Alex', age: 25 },
{ name: '', age: 15 }
]
};
Проверка:
const suite = create((data = {}) => {
data.users.forEach((user, index) => {
test(`users.${index}.name`, 'Имя обязательно', () => {
enforce(user.name).isNotBlank();
});
test(`users.${index}.age`, 'Возраст должен быть больше 18', () => {
enforce(user.age).greaterThan(18);
});
});
});
Имена тестов могут строиться динамически:
test(`items.${index}.title`, 'Название обязательно', () => {
enforce(item.title).isNotBlank();
});
Это особенно полезно при отображении ошибок рядом с элементами формы.
Иногда объект может отсутствовать полностью:
const user = {
settings: null
};
Проверка:
test('settings', 'Настройки должны быть объектом', () => {
if (data.settings !== null) {
enforce(data.settings).isObject();
}
});
Либо:
if (data.settings) {
test('settings.theme', 'Тема обязательна', () => {
enforce(data.settings.theme).isNotBlank();
});
}
Иногда требуется валидировать не отдельные поля, а состояние объекта в целом.
Например:
const product = {
price: 100,
discount: 150
};
Проверка логики:
test('product', 'Скидка не может превышать цену', () => {
enforce(data.discount).lessThanOrEquals(data.price);
});
Пример адреса доставки:
const address = {
country: 'USA',
state: ''
};
Правило:
test('state', 'Для США необходимо указать штат', () => {
if (data.country === 'USA') {
enforce(data.state).isNotBlank();
}
});
Vest позволяет строить сложные сценарии.
const payment = {
method: 'card',
cardNumber: ''
};
Проверка:
test('cardNumber', 'Номер карты обязателен', () => {
if (data.method === 'card') {
enforce(data.cardNumber).isNotBlank();
}
});
omitWhenomitWhen позволяет отключать проверки при выполнении
условия.
import { create, test, enforce, omitWhen } from 'vest';
const suite = create(data => {
omitWhen(data.isGuest, () => {
test('email', 'Email обязателен', () => {
enforce(data.email).isNotBlank();
});
});
});
Если isGuest === true, тесты внутри
omitWhen пропускаются.
При работе с большими объектами полезно логически разделять проверки.
const suite = create(data => {
// Блок профиля
test('profile.name', 'Имя обязательно', () => {
enforce(data.profile?.name).isNotBlank();
});
test('profile.email', 'Email обязателен', () => {
enforce(data.profile?.email).isNotBlank();
});
// Блок адреса
test('address.city', 'Город обязателен', () => {
enforce(data.address?.city).isNotBlank();
});
test('address.zip', 'ZIP обязателен', () => {
enforce(data.address?.zip).isNotBlank();
});
});
Проверки вложенных объектов удобно выносить в отдельные функции.
function validateAddress(address) {
enforce(address.city).isNotBlank();
enforce(address.zip).isNotBlank();
}
Использование:
test('address', 'Адрес некорректен', () => {
validateAddress(data.address);
});
Более гибкий вариант:
function validateAddress(address, prefix = 'address') {
test(`${prefix}.city`, 'Город обязателен', () => {
enforce(address.city).isNotBlank();
});
test(`${prefix}.zip`, 'ZIP обязателен', () => {
enforce(address.zip).isNotBlank();
});
}
Vest удобно использовать для проверки серверных данных.
fetch('/api/user')
.then(res => res.json())
.then(data => {
const result = suite(data);
if (result.hasErrors()) {
console.log(result.getErrors());
}
});
Пример проверки:
const suite = create(data => {
test('id', 'ID обязателен', () => {
enforce(data.id).isNumber();
});
test('email', 'Email обязателен', () => {
enforce(data.email).isString();
});
});
Перед валидацией объект часто преобразуется.
const normalized = {
...data,
email: data.email?.trim(),
username: data.username?.toLowerCase()
};
После этого:
const result = suite(normalized);
Проверка наличия свойств:
test('settings', 'Объект настроек пуст', () => {
enforce(Object.keys(data.settings).length).greaterThan(0);
});
Либо:
test('settings', 'Настройки обязательны', () => {
enforce(data.settings).isNotEmpty();
});
Пример:
const data = {
tags: ['js', 'react', 'js']
};
Проверка:
test('tags', 'Теги должны быть уникальными', () => {
const unique = new Set(data.tags);
enforce(unique.size).equals(data.tags.length);
});
Пример:
const config = {
server: {
host: 'localhost',
port: 3000
},
security: {
ssl: true,
cert: ''
}
};
Проверка:
const suite = create(data => {
test('server.host', 'Host обязателен', () => {
enforce(data.server?.host).isNotBlank();
});
test('server.port', 'Порт должен быть числом', () => {
enforce(data.server?.port).isNumber();
});
test('security.cert', 'SSL сертификат обязателен', () => {
if (data.security?.ssl) {
enforce(data.security.cert).isNotBlank();
}
});
});
Vest поддерживает расширение enforce.
enforce.extend({
isValidCoordinates(value) {
return (
typeof value.lat === 'number' &&
typeof value.lng === 'number'
);
}
});
Использование:
test('location', 'Некорректные координаты', () => {
enforce(data.location).isValidCoordinates();
});
Пример проверки объекта пользователя через API:
import { create, test, enforce } from 'vest';
const suite = create(async data => {
test('username', 'Имя уже занято', async () => {
const response = await fetch(`/api/check/${data.username}`);
const result = await response.json();
enforce(result.available).isTruthy();
});
});
eachДля коллекций удобно использовать вспомогательные функции.
data.items.forEach((item, index) => {
test(`items.${index}.price`, 'Цена обязательна', () => {
enforce(item.price).isNumber();
});
});
После выполнения suite:
const result = suite(data);
Получение ошибок:
result.getErrors();
Пример результата:
{
'profile.name': ['Имя обязательно'],
'address.city': ['Город обязателен']
}
Получение ошибок конкретного поля:
result.getErrors('profile.name');
Vest поддерживает selective validation.
suite(data, 'email');
Будет выполнена только проверка поля email.
Для вложенных структур:
suite(data, 'profile.city');
Это особенно полезно в формах с большим количеством вложенных объектов.
При валидации крупных структур рекомендуется:
test;Нежелательный вариант:
test('items', 'Ошибка', () => {
hugeArray.filter(...).map(...).reduce(...);
});
Предпочтительно:
const prepared = preprocess(hugeArray);
test('items', 'Ошибка', () => {
enforce(prepared.valid).isTruthy();
});
Крупные схемы удобно разделять по доменным областям.
function validateProfile(profile) {
test('profile.name', 'Имя обязательно', () => {
enforce(profile.name).isNotBlank();
});
}
function validateAddress(address) {
test('address.city', 'Город обязателен', () => {
enforce(address.city).isNotBlank();
});
}
Главная suite:
const suite = create(data => {
validateProfile(data.profile);
validateAddress(data.address);
});
Такой подход облегчает: