Создание наборов валидации

В библиотеке Vest набор валидации (suite) представляет собой структурированную единицу, в которой объединяются правила проверки данных. Такой подход позволяет описывать валидацию декларативно, группировать проверки по смыслу и переиспользовать их в разных частях приложения. Основная идея заключается в том, что suite ведёт себя как тестовый набор: каждая проверка выполняется независимо, но результаты агрегируются в единый объект состояния.

Создание набора начинается с функции suite, внутри которой описываются правила через test. Каждый тест содержит условие и сообщение об ошибке, которое возвращается при провале проверки.

import { suite, test, enforce } from 'vest';

const userSuite = suite('user', (data = {}) => {
  test('username', 'Имя пользователя обязательно', () => {
    enforce(data.username).isNotEmpty();
  });

  test('username', 'Имя пользователя должно быть не короче 3 символов', () => {
    enforce(data.username).longerThanOrEquals(3);
  });

  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });
});

Каждый test привязан к конкретному полю. Первый аргумент определяет ключ поля, второй — текст ошибки, третий — функция проверки.

Принцип группировки проверок

Vest позволяет группировать проверки по полям без создания отдельных структур. Это особенно важно для форм, где одно поле может иметь несколько независимых правил.

test('password', 'Пароль обязателен', () => {
  enforce(data.password).isNotEmpty();
});

test('password', 'Пароль должен содержать минимум 8 символов', () => {
  enforce(data.password).longerThanOrEquals(8);
});

test('password', 'Пароль должен содержать цифру', () => {
  enforce(data.password).matches(/\d/);
});

Такой подход обеспечивает детализированную обратную связь, где каждое правило возвращает отдельную ошибку.

Передача данных в suite

Набор валидации принимает входные данные как аргумент функции, что позволяет динамически валидировать любые структуры.

const result = userSuite({
  username: 'alex',
  email: 'alex@mail.com'
});

Результат выполнения содержит информацию о валидности и ошибки по каждому полю.

Состояние и повторный запуск

Каждый вызов suite пересчитывает состояние заново. Это важно для реактивных интерфейсов, где данные изменяются часто.

const result1 = userSuite({ username: 'a', email: '' });
const result2 = userSuite({ username: 'alex', email: 'alex@mail.com' });

Такой подход исключает необходимость ручного сброса состояния.

Условная валидация

Vest поддерживает условные проверки, которые позволяют включать или исключать правила в зависимости от входных данных.

test('phone', 'Телефон обязателен', () => {
  if (data.contactMethod === 'phone') {
    enforce(data.phone).isNotEmpty();
  }
});

Условная логика внутри теста даёт гибкость без усложнения структуры suite.

Переиспользование логики валидации

Наборы можно строить модульно, разделяя проверки на логические блоки. Это позволяет собирать сложные схемы из простых частей.

const emailTests = (data) => {
  test('email', 'Email обязателен', () => {
    enforce(data.email).isNotEmpty();
  });

  test('email', 'Email некорректен', () => {
    enforce(data.email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });
};

const userSuite = suite('user', (data) => {
  emailTests(data);

  test('username', 'Имя пользователя обязательно', () => {
    enforce(data.username).isNotEmpty();
  });
});

Такой стиль позволяет масштабировать валидацию без дублирования кода.

Асинхронные проверки

Vest поддерживает асинхронные тесты, что важно для проверки уникальности данных через API или обращения к серверу.

test('username', 'Имя уже существует', async () => {
  const exists = await checkUsernameExists(data.username);
  enforce(exists).isFalsy();
});

Асинхронные проверки выполняются так же, как и синхронные, но возвращают Promise.

Параллельное выполнение проверок

Асинхронные тесты внутри suite могут выполняться параллельно, что снижает время валидации при множественных запросах.

const result = await userSuite({
  username: 'alex',
  email: 'alex@mail.com'
});

Vest агрегирует результаты после завершения всех тестов.

Динамическое формирование набора

Набор может строиться динамически, что позволяет создавать валидацию на основе конфигурации.

const createUserSuite = (rules = {}) =>
  suite('user', (data) => {
    if (rules.usernameRequired) {
      test('username', 'Обязательное поле', () => {
        enforce(data.username).isNotEmpty();
      });
    }

    if (rules.emailCheck) {
      test('email', 'Некорректный email', () => {
        enforce(data.email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
      });
    }
  });

Такой подход используется в системах с настраиваемыми формами.

Работа с результатом выполнения

Результат suite представляет собой структурированный объект, содержащий ошибки по ключам полей. Это упрощает интеграцию с UI.

const result = userSuite(data);

if (result.hasErrors()) {
  console.log(result.getErrors('email'));
}

Ошибки можно извлекать как по отдельным полям, так и в агрегированном виде.

Композиция наборов

Несколько suite можно комбинировать для построения более сложной логики проверки.

const authSuite = suite('auth', (data) => {
  test('password', 'Пароль обязателен', () => {
    enforce(data.password).isNotEmpty();
  });
});

const profileSuite = suite('profile', (data) => {
  authSuite(data);

  test('nickname', 'Никнейм обязателен', () => {
    enforce(data.nickname).isNotEmpty();
  });
});

Композиция позволяет разделять ответственность между модулями.

Изоляция проверок и предсказуемость

Каждый test изолирован и не влияет на другие проверки. Это упрощает отладку и делает поведение валидации предсказуемым даже при большом количестве правил.

test('field', 'Ошибка A', () => {
  enforce(data.field).equals('A');
});

test('field', 'Ошибка B', () => {
  enforce(data.field).equals('B');
});

Каждое правило выполняется независимо, и результат агрегируется без побочных эффектов.

Использование enforce как декларативного слоя

enforce выступает промежуточным слоем между данными и логикой проверки. Он позволяет писать проверки в цепочечном стиле.

enforce(data.age).greaterThan(18);
enforce(data.email).matches(/@/);

Такой стиль повышает читаемость и делает код ближе к декларативному описанию требований.