Валидация полей формы

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

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

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

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

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

  test('password', 'Пароль слишком короткий', () => {
    enforce(data.password).longerThan(6);
  });
});

Каждый test связан с конкретным полем и сообщением ошибки. При несоответствии условию ошибка фиксируется внутри результата выполнения схемы.


Структура suite и модель выполнения

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

Основные принципы:

  • каждая проверка изолирована
  • ошибки группируются по ключам полей
  • выполнение может прерываться при использовании стратегий short-circuit
  • результат кэшируется внутри выполнения suite
const suite = create((data) => {
  test('username', () => {
    enforce(data.username).isNotEmpty();
  });

  test('age', () => {
    enforce(data.age).greaterThan(18);
  });
});

Проверка обязательных полей

Наиболее частый сценарий — контроль заполненности обязательных значений. Vest предоставляет набор предикатов через enforce, позволяющих описывать такие правила декларативно.

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

test('lastName', 'Фамилия обязательна', () => {
  enforce(data.lastName).isNotEmpty();
});

Семантика isNotEmpty учитывает null, undefined, пустую строку и аналогичные пустые значения.


Форматные ограничения и регулярные выражения

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

test('phone', 'Неверный формат телефона', () => {
  enforce(data.phone).matches(/^\+?[0-9]{10,15}$/);
});

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

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


Числовые ограничения

Для числовых полей применяются проверки диапазонов и сравнений.

test('age', 'Возраст должен быть не менее 18', () => {
  enforce(data.age).greaterThanOrEquals(18);
});

test('score', 'Слишком большое значение', () => {
  enforce(data.score).lessThanOrEquals(100);
});

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


Асинхронная валидация

Асинхронные проверки применяются для запросов к серверу: проверка уникальности имени пользователя, email или других ресурсов.

test('username', 'Имя уже занято', async () => {
  const exists = await api.checkUsername(data.username);
  enforce(exists).equals(false);
});

Асинхронные тесты интегрируются в общий поток выполнения suite без изменения структуры описания.


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

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

test('companyName', 'Название компании обязательно', () => {
  if (data.isCompany) {
    enforce(data.companyName).isNotEmpty();
  }
});

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


Группировка ошибок и структура результата

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

Типичная структура результата:

{
  hasErrors: true,
  errors: {
    email: ['Некорректный email'],
    password: ['Пароль слишком короткий']
  }
}

Группировка позволяет легко интегрировать результат в UI-слой формы.


Повторное использование правил

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

const required = (field, message) => {
  test(field, message, () => {
    enforce(data[field]).isNotEmpty();
  });
};

required('username', 'Имя обязательно');
required('email', 'Email обязателен');

Подход снижает дублирование и упрощает сопровождение.


Валидация массивов и списков

Поля, содержащие коллекции значений, требуют итеративной проверки.

test('tags', 'Каждый тег должен быть строкой', () => {
  data.tags.forEach(tag => {
    enforce(tag).isString();
  });
});

Также применяются ограничения на длину массива:

test('tags', 'Слишком много тегов', () => {
  enforce(data.tags.length).lessThanOrEquals(5);
});

Кэширование и оптимизация выполнения

Внутренняя модель выполнения suite допускает оптимизации за счёт:

  • повторного использования результатов
  • пропуска неактуальных тестов
  • группировки зависимых проверок

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


Интеграция с UI-слоем

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

const result = validationSuite(formData);

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

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


Композиция нескольких suite

Сложные формы могут быть разделены на логические блоки, каждый из которых имеет собственную suite. Результаты объединяются на уровне контроллера формы.

const userSuite = createUserSuite(data);
const addressSuite = createAddressSuite(data);

const result = {
  ...userSuite,
  ...addressSuite
};

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