Валидация связанных данных

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

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

  • подтверждение пароля зависит от основного пароля;
  • диапазоны дат требуют согласованности начала и конца периода;
  • адресные формы используют условные поля (например, «регион» влияет на обязательность «индекса»);
  • бизнес-логика формирует динамические ограничения (скидка доступна только при определённом типе пользователя);
  • массивы объектов требуют согласованности между элементами.

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

Модель валидации в Vest

Библиотека строится вокруг декларативного описания тестов (tests), которые группируются в сценарии (suite). Каждый тест получает доступ к значению текущего поля, а также к контексту всей формы.

Ключевой момент — наличие полного состояния данных внутри каждого валидатора:

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

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

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

Здесь видно, что проверка confirmPassword зависит от password, а значит валидация становится связанной по данным.

Контекст формы как источник зависимостей

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

Это приводит к следующим особенностям:

  • любая проверка может обращаться к любому полю;
  • зависимость между полями становится явной в коде;
  • пересчёт результатов возможен на уровне всей формы или отдельных полей.

Пример зависимой логики:

const suite = create((data) => {
  test('startDate', 'Некорректная дата начала', () => {
    enforce(new Date(data.startDate)).lessThanOrEquals(new Date(data.endDate));
  });

  test('endDate', 'Некорректная дата окончания', () => {
    enforce(new Date(data.endDate)).greaterThanOrEquals(new Date(data.startDate));
  });
});

Обе проверки взаимно зависят друг от друга, образуя симметричную связь.

Условная валидация на основе других полей

Одной из ключевых задач связанных данных является условная обязательность полей.

Пример: поле «VAT номер» обязательно только для компаний.

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

  test('vatNumber', 'VAT обязателен для компаний', () => {
    if (data.isCompany) {
      enforce(data.vatNumber).isNotEmpty();
    }
  });
});

Здесь зависимость выражена через флаг isCompany, который влияет на поведение другой проверки.

Такой подход позволяет:

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

Групповые зависимости и согласованность наборов данных

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

const suite = create((data) => {
  test('items', 'Корзина не может быть пустой', () => {
    enforce(data.items.length).greaterThan(0);
  });

  test('items', 'Сумма не должна превышать лимит', () => {
    const total = data.items.reduce((sum, item) => sum + item.price, 0);
    enforce(total).lessThanOrEquals(data.userLimit);
  });
});

Здесь данные массива связаны с внешним контекстом пользователя. Любое изменение одного элемента влияет на общий результат.

Каскадные зависимости

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

Пример: выбор страны влияет на регион, а регион — на доступные города.

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

  test('region', 'Регион не соответствует стране', () => {
    enforce(data.region).inside(getRegionsByCountry(data.country));
  });

  test('city', 'Город не принадлежит региону', () => {
    enforce(data.city).inside(getCitiesByRegion(data.region));
  });
});

Зависимости образуют направленный граф:

country → region → city

Изменение одного узла влияет на последующие проверки.

Использование динамических правил

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

const suite = create((data) => {
  test('discountCode', 'Недействительный код', () => {
    const allowedCodes = data.isPremium
      ? ['PREMIUM10', 'VIP20']
      : ['WELCOME5'];

    enforce(data.discountCode).inside(allowedCodes);
  });
});

Здесь логика валидации полностью определяется текущим состоянием формы.

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

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

const isAdult = (data) => data.age >= 18;
const hasConsent = (data) => data.parentConsent === true;

const suite = create((data) => {
  test('age', 'Возраст некорректен', () => {
    enforce(data.age).greaterThan(0);
  });

  test('parentConsent', 'Требуется согласие', () => {
    if (!isAdult(data)) {
      enforce(hasConsent(data)).isTruthy();
    }
  });
});

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

Валидация перекрёстных ограничений

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

Пример: кредитный лимит и ежемесячный платёж:

const suite = create((data) => {
  test('monthlyPayment', 'Платёж слишком высок', () => {
    enforce(data.monthlyPayment).lessThanOrEquals(data.income * 0.4);
  });

  test('creditLimit', 'Лимит не соответствует доходу', () => {
    enforce(data.creditLimit).lessThanOrEquals(data.income * 10);
  });

  test('combinedRisk', 'Суммарная нагрузка превышает допустимую', () => {
    const risk = data.monthlyPayment * 12 + data.creditLimit * 0.1;
    enforce(risk).lessThanOrEquals(data.income * 3);
  });
});

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

Реактивные зависимости и повторные вычисления

Механизм работы Vest позволяет оптимизировать пересчёт: при изменении одного поля пересчитываются только затронутые тесты.

Это особенно важно для связанных данных:

  • изменение password затрагивает confirmPassword;
  • изменение country затрагивает region и city;
  • изменение items влияет на агрегированные проверки.

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

Вложенные структуры и зависимые объекты

Связанные данные часто представлены в виде вложенных объектов:

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

  test('user.settings.notifications', 'Некорректная настройка', () => {
    if (data.user.settings.emailNotifications) {
      enforce(data.user.profile.email).isNotEmpty();
    }
  });
});

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

Консистентность состояния как цель связанных проверок

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

В рамках Vest это выражается через:

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

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