Рекурсивная валидация

Природа рекурсивной валидации

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

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


Представление вложенных структур

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

const model = {
  user: {
    name: "Alex",
    address: {
      city: "Almaty",
      street: {
        name: "Abay",
        building: 10
      }
    }
  }
}

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


Путь поля как основа рекурсии

Vest использует строковые пути для идентификации полей:

  • user.name
  • user.address.city
  • user.address.street.building

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

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

const suite = create((data = {}) => {

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

  test("user.address.city", "Город обязателен", () => {
    enforce(data.user?.address?.city).isNotEmpty();
  });

});

Несмотря на отсутствие явной рекурсии, структура тестов уже отражает вложенность модели.


Рекурсивная стратегия через функции-помощники

При увеличении глубины вложенности ручное описание каждого пути становится неэффективным. Рекурсивный подход строится через функции обхода структуры.

const validateAddress = (address = {}) => {
  test("city", "Город обязателен", () => {
    enforce(address.city).isNotEmpty();
  });

  test("street.name", "Улица обязательна", () => {
    enforce(address.street?.name).isNotEmpty();
  });

  test("street.building", "Номер дома обязателен", () => {
    enforce(address.street?.building).isNotEmpty();
  });
};

Далее эта функция может вызываться внутри основной suite с префиксом пути:

const suite = create((data = {}) => {

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

  validateAddress(data.user?.address);

});

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


Рекурсивная валидация массивов

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

const data = {
  users: [
    { name: "Alex" },
    { name: "" }
  ]
};

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

const suite = create((data = {}) => {

  (data.users || []).forEach((user, index) => {

    test(`users.${index}.name`, "Имя обязательно", () => {
      enforce(user.name).isNotEmpty();
    });

  });

});

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


Глубокая рекурсия через обход дерева

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

const validateNode = (node, path = "") => {

  if (typeof node !== "object" || node === null) {
    return;
  }

  Object.entries(node).forEach(([key, value]) => {
    const currentPath = path ? `${path}.${key}` : key;

    if (Array.isArray(value)) {
      value.forEach((item, index) => {
        validateNode(item, `${currentPath}.${index}`);
      });
    } else if (typeof value === "object") {
      validateNode(value, currentPath);
    } else {
      test(currentPath, "Поле обязательно", () => {
        enforce(value).isNotEmpty();
      });
    }
  });
};

Такая функция превращает структуру данных в набор тестов Vest, где каждый путь становится отдельной проверкой.


Разделение ответственности: генерация и выполнение тестов

Рекурсивная валидация в Vest опирается на разделение двух процессов:

  1. Обход структуры данных
  2. Регистрация тестов через test

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

const suite = create((data = {}) => {
  validateNode(data);
});

Таким образом, Vest становится исполнительным слоем, а рекурсивная функция — генератором тестов.


Контекст пути и изоляция веток

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

Пример структуры ошибок:

  • profile.contacts.email
  • profile.contacts.phone
  • profile.settings.notifications.email

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


Рекурсивная валидация и условные ветвления

В реальных моделях данные могут иметь условную вложенность.

const data = {
  type: "company",
  company: {
    name: "ACME",
    employees: [
      { name: "John" }
    ]
  }
};

Рекурсивная логика учитывает условия:

const suite = create((data = {}) => {

  test("type", "Тип обязателен", () => {
    enforce(data.type).isNotEmpty();
  });

  if (data.type === "company") {

    test("company.name", "Название компании обязательно", () => {
      enforce(data.company?.name).isNotEmpty();
    });

    (data.company?.employees || []).forEach((emp, i) => {
      test(`company.employees.${i}.name`, "Имя сотрудника обязательно", () => {
        enforce(emp.name).isNotEmpty();
      });
    });

  }

});

Рекурсивность здесь становится зависимой от ветвления бизнес-логики.


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

При больших моделях данных рекурсивные валидаторы дробятся на независимые модули.

const validateEmployee = (emp, path) => {
  test(`${path}.name`, "Имя обязательно", () => {
    enforce(emp.name).isNotEmpty();
  });
};

const validateCompany = (company = {}) => {

  test("name", "Название обязательно", () => {
    enforce(company.name).isNotEmpty();
  });

  (company.employees || []).forEach((emp, i) => {
    validateEmployee(emp, `employees.${i}`);
  });
};

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


Оптимизация рекурсивных проверок

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

Практический подход включает:

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

Рекурсия и динамические схемы

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

const suite = create((data = {}) => {

  Object.entries(data).forEach(([key, value]) => {

    if (typeof value === "object" && value !== null) {
      Object.keys(value).forEach(subKey => {
        test(`${key}.${subKey}`, "Значение обязательно", () => {
          enforce(value[subKey]).isNotEmpty();
        });
      });
    }

  });

});

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


Рекурсивная валидация как обобщённая модель обхода

В основе рекурсивной валидации лежит универсальная модель:

  1. Узел данных рассматривается как потенциальная ветка
  2. Если узел содержит вложенность — применяется повторная валидация
  3. Если узел является примитивом — применяется проверка
  4. Путь формируется на каждом уровне рекурсии

Эта модель совпадает с классическими алгоритмами обхода дерева (DFS), адаптированными под декларативный стиль Vest.


Связь рекурсии и декларативного подхода Vest

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

test("user.address.street.name", "Улица обязательна", () => {
  enforce(data.user?.address?.street?.name).isNotEmpty();
});

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