Cross-field валидация

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

В основе подхода лежит возможность доступа к значениям других полей внутри текущего прогона валидации. Vest предоставляет для этого механизм ссылок на поля через field, а также композицию правил внутри suite.

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

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

  test('confirm', 'Пароль не подтверждён', () => {
    enforce(data.confirm).isNotEmpty();
  });

  test('confirm', 'Пароли не совпадают', () => {
    enforce(data.confirm).equals(data.password);
  });
});

Здесь второе и третье правило для confirm формируют cross-field зависимость: значение подтверждения оценивается относительно password.

Использование field для явных зависимостей

Более декларативный подход строится на field, позволяющем явно указывать зависимость между полями без обращения к исходному объекту данных.

test('confirm', 'Пароли не совпадают', () => {
  enforce(field('confirm')).equals(field('password'));
});

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

Множественные зависимости между полями

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

test('endDate', 'Дата окончания должна быть позже начала', () => {
  enforce(field('endDate')).isAfter(field('startDate'));
});

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

test('discount', 'Скидка не может превышать цену', () => {
  enforce(field('discount')).lessThanOrEquals(field('price'));
});

test('total', 'Итоговая сумма некорректна', () => {
  enforce(field('total'))
    .equals(field('price').minus(field('discount')));
});

Условная cross-field валидация

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

test('state', 'Штат обязателен для выбранной страны', () => {
  when(field('country')).equals('US', () => {
    enforce(field('state')).isNotEmpty();
  });
});

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

Взаимозависимость через skipWhen

В сценариях, где одна ошибка делает бессмысленными другие проверки, применяется skipWhen.

test('confirm', 'Пароли не совпадают', () => {
  skipWhen(!field('password'), () => {
    enforce(field('confirm')).equals(field('password'));
  });
});

Логика становится чувствительной к состоянию других полей, что уменьшает шум от каскадных ошибок.

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

Cross-field валидация часто используется для числовых интервалов: минимальные и максимальные значения должны быть согласованы.

test('minPrice', 'Минимальная цена не может быть больше максимальной', () => {
  enforce(field('minPrice')).lessThanOrEquals(field('maxPrice'));
});

test('maxPrice', 'Максимальная цена должна быть не меньше минимальной', () => {
  enforce(field('maxPrice')).greaterThanOrEquals(field('minPrice'));
});

Дублирование проверок в обе стороны обеспечивает устойчивость интерфейса при изменении любого из полей.

Кросс-проверки массивов и списков

Для коллекций значений cross-field логика распространяется на элементы массива и их агрегаты.

test('items', 'Сумма элементов должна совпадать с total', () => {
  const sum = field('items').reduce((acc, x) => acc + x.amount, 0);

  enforce(field('total')).equals(sum);
});

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

test('primaryEmail', 'Email должен отличаться от резервного', () => {
  enforce(field('primaryEmail')).notEquals(field('backupEmail'));
});

Работа с вычисляемыми зависимостями

Cross-field валидация может включать вычисляемые значения, формируемые на основе нескольких полей одновременно.

test('age', 'Возраст не соответствует дате рождения', () => {
  const birthDate = new Date(field('birthDate'));
  const age = new Date().getFullYear() - birthDate.getFullYear();

  enforce(field('age')).equals(age);
});

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

Приоритеты и порядок выполнения проверок

В Vest порядок объявления тестов влияет на читаемость, но не ограничивает возможность cross-field взаимодействия. Однако логически связанные проверки обычно группируются рядом, чтобы упростить сопровождение:

  • базовая проверка поля
  • зависимость от других полей
  • агрегированные условия
test('password', 'Пароль обязателен', () => {
  enforce(field('password')).isNotEmpty();
});

test('confirm', 'Подтверждение обязательно', () => {
  enforce(field('confirm')).isNotEmpty();
});

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

Инварианты формы как набор cross-field правил

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

Типичные инварианты:

  • согласованность диапазонов
  • равенство связанных значений
  • условная обязательность
  • взаимное исключение полей
test('startDate', 'Неверный диапазон дат', () => {
  enforce(field('startDate')).lessThan(field('endDate'));
});

test('endDate', 'Неверный диапазон дат', () => {
  enforce(field('endDate')).greaterThan(field('startDate'));
});

Изоляция cross-field логики в отдельных тестах

Для повышения предсказуемости поведения формы cross-field проверки выделяются в отдельные test, не смешиваясь с локальной валидацией одного поля. Такой подход снижает связанность правил и упрощает диагностику ошибок.

test('confirm', 'Некорректное подтверждение', () => {
  enforce(field('confirm')).equals(field('password'));
});

Изоляция также позволяет переиспользовать набор зависимостей при изменении структуры формы без переработки всех правил.

Стабильность состояния при перекрёстных проверках

Cross-field логика чувствительна к промежуточным состояниям формы. При частичном заполнении некоторые зависимости становятся временно некорректными. Vest позволяет учитывать это через комбинирование условий when и проверок на пустые значения.

test('confirm', 'Пароли не совпадают', () => {
  when(field('password')).isNotEmpty(() => {
    enforce(field('confirm')).equals(field('password'));
  });
});

Такой подход предотвращает ложные ошибки при незавершённом вводе данных.