Кастомные enforce операторы

В основе библиотеки лежит функция enforce, представляющая собой единый интерфейс описания утверждений (assertions). Она реализует цепочку вызовов, где каждое последующее звено добавляет новое ограничение к проверяемому значению.

Ключевая особенность архитектуры — расширяемость через пользовательские операторы. Оператор в контексте Vest представляет собой метод, прикреплённый к enforce(value), который принимает дополнительные параметры и возвращает либо успешное выполнение, либо ошибку валидации.


Понятие оператора в системе enforce

Оператор — это функция-предикат, интегрированная в цепочку enforce, которая:

  • принимает текущее значение;
  • принимает дополнительные аргументы (если необходимо);
  • выполняет проверку;
  • сигнализирует об ошибке при несоответствии условиям.

Базовая форма выглядит как:

enforce(value).operatorName(args);

Каждый оператор расширяет поведение базового объекта проверки, сохраняя единый стиль декларативного описания правил.


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

Встроенные операторы предоставляют стандартные проверки:

  • равенство и сравнение;
  • проверка строк (длина, шаблоны);
  • числовые ограничения;
  • логические условия.

Однако прикладные задачи часто требуют доменной логики:

  • проверка внутренних идентификаторов;
  • валидация специфических форматов;
  • бизнес-правила, зависящие от внешних данных.

Именно здесь используются кастомные операторы, позволяющие расширять enforce без модификации ядра.


Механизм регистрации кастомных операторов

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

Обобщённая форма регистрации:

enforce.extend('operatorName', (value, ...args) => {
  return true | false | Error;
});

Или более развернутый вариант с сообщением об ошибке:

enforce.extend('isEven', (value) => {
  if (value % 2 !== 0) {
    return 'Значение должно быть чётным';
  }
});

После регистрации оператор становится доступным в цепочке:

enforce(4).isEven();

Поведение функции оператора

Функция кастомного оператора получает:

  • value — текущее значение проверки;
  • дополнительные параметры, переданные пользователем;
  • внутренний контекст исполнения (в некоторых реализациях — метаданные цепочки).

Возвращаемые значения определяют результат:

  • undefined или true — проверка пройдена;
  • string — сообщение об ошибке;
  • false — стандартная ошибка без уточнения;
  • Error — исключительная ситуация с кастомным объектом ошибки.

Формирование сообщений об ошибках

Гибкость системы заключается в возможности возвращать динамические сообщения:

enforce.extend('minWords', (value, min) => {
  const count = value.trim().split(/\s+/).length;

  if (count < min) {
    return `Минимальное количество слов: ${min}`;
  }
});

Использование:

enforce('hello world').minWords(3);

Сообщения могут строиться на основе:

  • параметров оператора;
  • текущего значения;
  • внешнего контекста (например, локализации).

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

Кастомные операторы могут комбинироваться с встроенными, формируя сложные правила:

enforce(email)
  .isString()
  .isEmail()
  .domainAllowed(['example.com']);

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


Переиспользование и фабрики операторов

Часто требуется параметризованная логика. Для этого используются фабрики операторов:

const minLength = (min) =>
  (value) => {
    if (value.length < min) {
      return `Минимальная длина: ${min}`;
    }
  };

enforce.extend('minLength', minLength);

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

enforce('abc').minLength(5);

Асинхронные кастомные операторы

Некоторые проверки требуют обращения к внешним ресурсам:

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

Асинхронный оператор возвращает Promise:

enforce.extend('isUniqueUsername', async (value, api) => {
  const exists = await api.checkUsername(value);

  if (exists) {
    return 'Имя пользователя уже занято';
  }
});

Использование:

await enforce('john').isUniqueUsername(api);

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


Типизация кастомных операторов (TypeScript)

При использовании TypeScript расширение оператора требует декларации типов:

declare module 'vest' {
  interface Enforce {
    minWords(min: number): Enforce;
  }
}

Реализация:

enforce.extend('minWords', (value: string, min: number) => {
  const count = value.trim().split(/\s+/).length;

  if (count < min) {
    return `Минимум слов: ${min}`;
  }
});

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

  • автодополнение;
  • проверку сигнатур;
  • предотвращение некорректного использования API.

Контекст и состояние цепочки

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

  • предыдущие проверки;
  • накопленные ошибки;
  • метаданные поля.

Пример использования контекста:

enforce.extend('matchesField', (value, fieldValue) => {
  if (value !== fieldValue) {
    return 'Значения не совпадают';
  }
});

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


Паттерны проектирования кастомных операторов

На практике выделяются устойчивые подходы:

Доменные операторы

Инкапсулируют бизнес-логику:

enforce.extend('isValidINN', (value) => {
  // алгоритм проверки ИНН
});

Составные операторы

Объединяют несколько проверок:

enforce.extend('strongPassword', (value) => {
  if (value.length < 8) return 'Слишком короткий пароль';
  if (!/[A-Z]/.test(value)) return 'Нет заглавной буквы';
  if (!/[0-9]/.test(value)) return 'Нет цифры';
});

Делегирующие операторы

Используют внешние сервисы:

enforce.extend('isDisposableEmail', async (value, service) => {
  const result = await service.check(value);

  if (result.disposable) {
    return 'Временные email-адреса запрещены';
  }
});

Ошибки проектирования кастомных операторов

При расширении системы часто возникают типовые проблемы:

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

Рациональная декомпозиция операторов повышает читаемость цепочек enforce и упрощает поддержку системы валидации.