Упаковка расширений

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

Расширение в Joi представляет собой декларативное описание набора изменений к существующим типам или добавление новых типов схем. Внутри расширение обычно состоит из нескольких ключевых частей:

  • базовый тип, к которому добавляется поведение;
  • новые правила (rules), расширяющие логику валидации;
  • кастомные сообщения об ошибках;
  • модификации описания схемы (description, metadata);
  • пред- и пост-обработчики значений.

Внутренне Joi применяет расширения на этапе компиляции схемы, создавая производную структуру, которая затем используется при валидации. Это означает, что расширение не является «динамическим плагином» в классическом смысле, а участвует в формировании итогового валидатора.

Базовая форма расширения

Создание расширения начинается с определения объекта, который описывает изменения:

import JoiBase from 'joi';

const Joi = JoiBase.extend((joi) => ({
  type: 'positiveInt',
  base: joi.number(),
  messages: {
    'positiveInt.base': '{{#label}} должно быть положительным целым числом',
  },
  rules: {
    positive: {
      validate(value, helpers) {
        if (value <= 0) {
          return helpers.error('positiveInt.base');
        }
        return value;
      }
    }
  }
}));

Здесь происходит сразу несколько операций:

  • создаётся новый тип positiveInt, основанный на number;
  • добавляется правило positive;
  • определяется сообщение об ошибке;
  • реализуется функция validate, которая участвует в процессе проверки.

Упаковка логики правил

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

Пример группировки:

  • числовые ограничения (диапазоны, знаки, шаг);
  • строковые форматы (UUID, slug, телефон);
  • бизнес-правила (ID клиента, артикулы, внутренние коды).

Такой подход позволяет создавать предсказуемую структуру API:

Joi.productCode().region('EU').strict()

Каждый вызов — это отдельное правило внутри расширения.

Схема сообщений и локализация

Расширения в Joi часто включают собственные сообщения ошибок. Это необходимо для сохранения согласованности с доменной моделью приложения.

Сообщения описываются через шаблоны:

messages: {
  'productCode.invalid': 'Код продукта {{#value}} не соответствует формату',
  'productCode.region': 'Регион {{#region}} не поддерживается'
}

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

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

Инкапсуляция расширений

При проектировании расширений важно соблюдать принцип изоляции. Расширение не должно зависеть от глобального состояния Joi, кроме базового экземпляра.

Правильная структура модуля:

/src
  index.js
  rules/
  messages/
  types/

Файл экспорта:

export default function productCodeExtension(joi) {
  return joi.extend((joi) => ({
    type: 'productCode',
    base: joi.string(),
    rules: {
      format: {
        validate(value, helpers) {
          if (!/^PRD-[0-9]{6}$/.test(value)) {
            return helpers.error('productCode.invalid');
          }
          return value;
        }
      }
    }
  }));
}

Такая форма позволяет подключать расширение как функцию, не загрязняя глобальное пространство.

Упаковка в npm модуль

Расширения Joi часто распространяются как отдельные пакеты. При этом важно учитывать несколько факторов:

1. Зависимости

Joi должен быть объявлен как peer dependency:

{
  "peerDependencies": {
    "joi": ">=17.0.0"
  }
}

Это предотвращает дублирование экземпляров библиотеки, что критично для корректной работы схем.

2. Экспорт

Поддерживаются оба формата:

  • CommonJS
  • ESM
module.exports = productCodeExtension;

или

export default productCodeExtension;

3. Структура пакета

Минимальная структура:

package.json
index.js
README.md
src/

README часто содержит не только описание, но и примеры интеграции, поскольку расширения используются как API.

Совместимость версий

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

Основные риски:

  • изменение API extend;
  • изменение формата ошибок;
  • изменение компиляции схем.

Для контроля совместимости используют:

  • фиксированные диапазоны версий;
  • тесты на уровне интеграции;
  • CI-валидацию с несколькими версиями Joi.

Сборка и транспиляция

При распространении расширений часто применяется транспиляция:

  • Babel для поддержки старых сред;
  • TypeScript для типизации;
  • bundlers (Rollup, esbuild) для ESM-вывода.

Особое внимание уделяется tree-shaking: расширение должно быть максимально модульным, чтобы не тянуть лишний код.

Типизация расширений

В TypeScript расширения Joi могут быть описаны через декларации модулей. Это позволяет расширять типы схем:

declare module 'joi' {
  interface StringSchema {
    productCode(): this;
  }
}

Типизация обеспечивает:

  • автодополнение в IDE;
  • проверку корректности цепочек вызовов;
  • контроль API расширений.

Композиция расширений

В крупных проектах несколько расширений могут комбинироваться:

const JoiExtended = Joi
  .extend(productCodeExtension)
  .extend(regionExtension)
  .extend(currencyExtension);

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

Интеграция в доменную модель

Расширения Joi часто становятся отражением предметной области. Вместо простых проверок формируется слой доменных типов:

  • OrderId
  • UserEmail
  • CountryCode
  • SKU

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

Тестирование расширений

Расширения требуют отдельного тестового покрытия:

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

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