Современные структуры данных в JavaScript редко ограничиваются плоскими объектами. Практические модели включают вложенные сущности, массивы объектов, динамические коллекции и древовидные структуры. Рекурсивная валидация становится необходимым механизмом, когда правило проверки должно применяться не только к верхнему уровню, но и ко всем вложенным уровням структуры произвольной глубины.
В контексте Vest рекурсивная валидация реализуется через повторное применение тестовых наборов (suites) к вложенным сегментам данных с сохранением контекста пути поля. Это позволяет строить декларативные правила, которые масштабируются на сложные схемы данных без потери читаемости и управляемости.
Типичная модель данных, требующая рекурсивной валидации:
const model = {
user: {
name: "Alex",
address: {
city: "Almaty",
street: {
name: "Abay",
building: 10
}
}
}
}
Каждый уровень структуры требует независимых, но согласованных правил валидации. При этом важно сохранять связь между вложенными полями и общей моделью через путь поля.
Vest использует строковые пути для идентификации полей:
user.nameuser.address.cityuser.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 опирается на разделение двух процессов:
testЭто позволяет не смешивать логику навигации по данным и бизнес-правила валидации.
const suite = create((data = {}) => {
validateNode(data);
});
Таким образом, Vest становится исполнительным слоем, а рекурсивная функция — генератором тестов.
Одной из ключевых задач рекурсивной валидации является сохранение контекста ветки данных. Ошибка в глубоко вложенном поле должна точно указывать путь.
Пример структуры ошибок:
profile.contacts.emailprofile.contacts.phoneprofile.settings.notifications.emailVest агрегирует результаты по путям, что позволяет группировать ошибки по уровням вложенности.
В реальных моделях данные могут иметь условную вложенность.
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();
});
});
}
});
});
Такая схема не требует заранее определённой модели и адаптируется к структуре входных данных.
В основе рекурсивной валидации лежит универсальная модель:
Эта модель совпадает с классическими алгоритмами обхода дерева (DFS), адаптированными под декларативный стиль Vest.
Vest сохраняет декларативную природу даже при рекурсивной логике. Вместо описания алгоритма обхода структура описывает правила, а обход становится вспомогательным механизмом.
test("user.address.street.name", "Улица обязательна", () => {
enforce(data.user?.address?.street?.name).isNotEmpty();
});
Даже без явной рекурсии система работает на уровне концепции вложенных путей, где рекурсивность скрыта внутри структуры данных и логики построения тестов.