Пользовательские форматы вывода

Работа с пользовательскими форматами вывода в Vest строится вокруг преобразования результата выполнения валидационных наборов (suites) в структуру, удобную для конкретного слоя приложения: UI-форм, API-ответов, логирования или интеграции с внешними системами.

Любой запуск vest.run() возвращает объект результата, содержащий стандартные секции:

  • errors — список ошибок валидации
  • warnings — предупреждения (если используются)
  • tests — подробный список выполненных тестов
  • isValid — итоговый статус
  • valid / invalid fields — агрегированная информация по полям

Каждый элемент ошибки обычно содержит:

  • field — имя поля
  • message — текст ошибки
  • test — идентификатор теста
  • group — группа (если использовалась группировка)
  • дополнительные метаданные, если они были добавлены через кастомные проверки

Эта структура универсальна, но редко используется напрямую в UI без преобразования.


Зачем нужны пользовательские форматы вывода

Стандартный формат результата не всегда подходит под конкретную задачу:

  • фронтенд-форма требует ошибок, сгруппированных по полям
  • API должен возвращать компактный JSON без лишних данных
  • логирование требует плоской структуры
  • интернационализация требует замены сообщений
  • сложные формы требуют вложенной структуры ошибок

Поэтому результат Vest часто преобразуется в кастомный формат.


Базовые подходы к преобразованию результата

1. Группировка ошибок по полям

Наиболее распространённый формат — объект, где ключи являются именами полей:

function groupErrorsByField(result) {
  return result.errors.reduce((acc, error) => {
    if (!acc[error.field]) {
      acc[error.field] = [];
    }
    acc[error.field].push(error.message);
    return acc;
  }, {});
}

Результат:

{
  "email": ["Email обязателен", "Неверный формат email"],
  "password": ["Слишком короткий пароль"]
}

Такой формат удобен для форм с отображением ошибок под каждым полем.


2. Преобразование в плоский список сообщений

Иногда требуется полностью линейная структура:

function flattenErrors(result) {
  return result.errors.map(error => ({
    field: error.field,
    message: error.message
  }));
}

Используется в:

  • системах логирования
  • аналитике ошибок
  • серверных ответах

3. Формат для API-ответов

Часто требуется строгая контрактная структура:

function toApiResponse(result) {
  return {
    success: result.isValid,
    errors: result.errors.map(e => ({
      path: e.field,
      detail: e.message
    }))
  };
}

Особенность такого формата — унификация ключей (path, detail) независимо от внутренней структуры Vest.


Работа с вложенными полями

Vest поддерживает валидацию вложенных структур, например:

  • user.email
  • user.profile.name
  • addresses[0].city

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

Разбиение path на структуру

function buildNestedErrors(errors) {
  const result = {};

  for (const error of errors) {
    const path = error.field.split('.');
    let current = result;

    for (let i = 0; i < path.length; i++) {
      const key = path[i];

      if (i === path.length - 1) {
        if (!current[key]) current[key] = [];
        current[key].push(error.message);
      } else {
        if (!current[key]) current[key] = {};
        current = current[key];
      }
    }
  }

  return result;
}

Результат:

{
  "user": {
    "email": ["Email обязателен"],
    "profile": {
      "name": ["Имя слишком короткое"]
    }
  }
}

Кастомизация сообщений на этапе преобразования

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

Пример локализации:

const messagesMap = {
  REQUIRED: "Поле обязательно",
  MIN_LENGTH: "Слишком короткое значение"
};

function localizeErrors(result) {
  return result.errors.map(err => ({
    field: err.field,
    message: messagesMap[err.test] || err.message
  }));
}

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


Формирование UI-совместимого формата

Для фронтенд-фреймворков часто используется структура:

function toFormState(result) {
  return {
    errors: groupByField(result.errors),
    valid: result.isValid,
    touched: {}
  };
}

Дополнительно могут добавляться:

  • dirty состояния
  • warnings
  • errorCount

Фильтрация и селективный вывод

Иногда требуется показывать не все ошибки сразу.

Фильтр по приоритету:

function filterCriticalErrors(result) {
  return result.errors.filter(e => e.severity === "critical");
}

Ограничение количества ошибок на поле:

function limitErrorsPerField(result, limit = 1) {
  const grouped = groupErrorsByField(result);

  for (const key in grouped) {
    grouped[key] = grouped[key].slice(0, limit);
  }

  return grouped;
}

Преобразование в формат для журналирования

Для серверных систем часто важна трассировка:

function toLogFormat(result) {
  return {
    timestamp: Date.now(),
    status: result.isValid ? "valid" : "invalid",
    errors: result.errors.map(e => ({
      field: e.field,
      message: e.message,
      test: e.test
    }))
  };
}

Такой формат удобен для:

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

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

В реальных приложениях часто используется комбинирование:

function formatResult(result) {
  return {
    api: toApiResponse(result),
    form: groupErrorsByField(result),
    log: toLogFormat(result)
  };
}

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


Нормализация ошибок перед форматированием

Перед преобразованием часто выполняется нормализация:

  • удаление дубликатов
  • сортировка
  • приведение сообщений к единому виду
function normalizeErrors(errors) {
  const seen = new Set();

  return errors.filter(err => {
    const key = `${err.field}:${err.message}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

Расширение через пользовательские функции-форматтеры

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

const formatters = {
  api: toApiResponse,
  form: groupErrorsByField,
  log: toLogFormat
};

function format(result, type) {
  return formatters[type](result);
}

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

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

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

Если в тестах добавляются дополнительные данные, они могут быть использованы при выводе:

test("email", "Email invalid", () => {
  enforce(value).matches(/@/).message("invalid_email");
});

Позже:

function enrichErrors(result) {
  return result.errors.map(e => ({
    ...e,
    humanMessage: translate(e.message),
    severity: getSeverity(e.test)
  }));
}

Формирование стабильного контракта вывода

При построении библиотек или API поверх Vest важно фиксировать контракт:

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

Это позволяет менять внутреннюю реализацию suite без влияния на потребителей результата.