Обработка граничных случаев

Одним из наиболее частых граничных случаев при работе с библиотекой валидации Vest является обработка отсутствующих или неопределённых значений. В реальных формах данные редко поступают в полностью нормализованном виде: поля могут быть undefined, null, пустыми строками или вовсе отсутствовать в объекте состояния.

Внутренняя логика валидации должна учитывать различие между этими состояниями:

  • undefined — поле не задано
  • null — поле явно очищено
  • "" — пользователь ввёл пустую строку
  • отсутствие ключа в объекте — структура данных не синхронизирована

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

rule('email', 'Email обязателен', () => {
  enforce(form.email).isNotEmpty();
});

Такой код падает или даёт неконсистентный результат, если form.email отсутствует.

Корректная обработка включает явное приведение входных данных:

const value = form.email ?? '';

rule('email', 'Email обязателен', () => {
  enforce(value).isNotEmpty();
});

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


Пограничные значения числовых полей

Числа в формах представляют особый класс граничных случаев, особенно когда данные приходят из HTML input, где фактический тип всегда строка.

Основные проблемные ситуации:

  • "" (пустой ввод)
  • "0" (ложно трактуется как falsy в некоторых проверках)
  • отрицательные значения
  • NaN
  • очень большие числа (Number.MAX_SAFE_INTEGER и выше)
  • дробные значения при ожидании целых

Ошибка, часто встречающаяся при валидации:

rule('age', 'Возраст обязателен', () => {
  enforce(form.age).isNotEmpty();
});

Если пользователь вводит 0, проверка может быть ошибочно интерпретирована как провал, если используется нестрогое сравнение.

Корректная стратегия — разделение проверки наличия и проверки диапазона:

const age = Number(form.age);

rule('age', 'Возраст обязателен', () => {
  enforce(form.age).isNotEmpty();
});

rule('age', 'Возраст должен быть положительным', () => {
  enforce(age).greaterThan(0);
});

Особое внимание требуется к NaN, который не равен самому себе и может незаметно проходить проверки:

Number('abc') // NaN

Проверка должна учитывать это явно:

enforce(age).isNotNaN();

Частично заполненные формы

Граничный случай, возникающий при пошаговом заполнении форм, когда часть полей ещё не доступна для пользователя, но уже присутствует в состоянии.

Типичная проблема — каскадные ошибки валидации:

  • поле A зависит от B
  • B ещё не заполнено
  • валидация A падает из-за отсутствия B

Пример некорректной зависимости:

rule('confirmPassword', 'Пароли должны совпадать', () => {
  enforce(form.confirmPassword).equals(form.password);
});

Если password ещё не введён, результат становится неконтролируемым.

Правильный подход — введение условной валидации:

rule('confirmPassword', 'Пароли должны совпадать', () => {
  if (!form.password) return;

  enforce(form.confirmPassword).equals(form.password);
});

Либо использование стадий валидации:

  • базовая проверка
  • зависимая проверка
  • финальная проверка формы

Конфликтующие правила валидации

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

Примеры:

  • поле одновременно «обязательное» и «опциональное при условии»
  • диапазон пересекается с динамическими ограничениями
  • асинхронная проверка перекрывает синхронную

Типичный конфликт:

rule('username', 'Обязательно', () => {
  enforce(form.username).isNotEmpty();
});

rule('username', 'Минимум 3 символа', () => {
  enforce(form.username).longerThanOrEquals(3);
});

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

Для управления конфликтами применяют:

  • приоритеты правил
  • остановку цепочки при первой ошибке
  • группировку правил по логическим блокам

Асинхронные граничные состояния

Асинхронные проверки (например, проверка уникальности логина) создают особый класс проблем:

  • гонки запросов
  • устаревшие ответы
  • повторная валидация до завершения предыдущей

Классическая проблема:

  1. пользователь вводит alex
  2. отправляется запрос проверки
  3. пользователь меняет на alex2
  4. ответ для alex приходит позже и перезаписывает результат

Без контроля состояния это приводит к ложным ошибкам.

Решение — привязка результата к текущему значению:

let lastChecked = '';

rule('username', 'Уже существует', async () => {
  const value = form.username;

  lastChecked = value;

  const exists = await api.checkUsername(value);

  if (lastChecked !== value) return;

  enforce(exists).equals(false);
});

Граничные случаи массивов и списков

Массивы часто используются для динамических полей (email-адреса, телефоны, теги). Основные проблемы:

  • пустой массив
  • массив с undefined элементами
  • частично валидные элементы
  • изменение длины во время валидации

Ошибка:

form.emails.forEach(email => {
  enforce(email).matches(/@/);
});

Если emails равен undefined, возникает исключение.

Базовая защита:

const emails = form.emails ?? [];

emails.forEach(email => {
  enforce(email).matches(/@/);
});

Дополнительный граничный случай — разная валидность элементов:

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

Изменение состояния во время валидации

Одним из наименее очевидных граничных случаев является изменение данных в процессе выполнения правил.

Например:

rule('field', 'Проверка', () => {
  form.field = form.field.trim();
  enforce(form.field).isNotEmpty();
});

Такая практика приводит к:

  • недетерминированным результатам
  • повторным триггерам валидации
  • несогласованности состояния UI

Правильный подход — разделение трансформации и валидации:

  • сначала нормализация данных
  • затем проверка

Граничные случаи зависимых форм

Зависимые поля (country → city, category → subcategory) создают сложные сценарии:

  • значение одного поля становится невалидным при изменении другого
  • старые значения остаются в состоянии
  • правила применяются к устаревшему контексту

Типичный случай:

  • пользователь выбрал страну «A»
  • выбрал город «X»
  • изменил страну на «B», где города «X» нет

Если не сбрасывать зависимое поле, валидация работает с некорректной комбинацией данных.

Подход:

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

Граничные случаи инициализации

Во время первичной загрузки формы состояние часто неполное:

  • данные приходят асинхронно
  • часть значений заполняется по умолчанию
  • часть остаётся неопределённой

Если валидация запускается до полной инициализации, возникают ложные ошибки.

Особенно критично это для:

  • серверных значений
  • автозаполнения
  • восстановленных сессий

Решение заключается в разделении фаз:

  • инициализация состояния
  • активация валидации
  • пользовательское взаимодействие

Граничные числовые и логические переполнения

Хотя JavaScript не имеет классического переполнения целых чисел, существуют логические пределы:

  • потеря точности при больших числах
  • некорректные сравнения дробей
  • ошибки округления

Пример:

0.1 + 0.2 !== 0.3

В контексте валидации это может приводить к ложным отрицаниям при проверке сумм, балансов или процентов.

Для устойчивости применяются:

  • округление до фиксированной точности
  • сравнение с допуском (epsilon)
const EPS = 0.0001;

enforce(Math.abs(sum - expected)).lessThan(EPS);

Граничные состояния ошибок и повторной валидации

Ошибки валидации не являются статичными. Они могут:

  • исчезать при изменении соседних полей
  • сохраняться после исправления значения
  • дублироваться при повторном запуске правил

Особенно проблемно это в динамических формах, где правила пересчитываются частично.

Подходы:

  • полная пересборка состояния ошибок при каждом цикле
  • детерминированное вычисление результата
  • отсутствие «инкрементальных» мутаций состояния ошибок