Расширение enforce

Механизм enforce — центральный инструмент библиотеки Vest для декларативной проверки значений. В отличие от обычных валидаторов, построенных вокруг условных конструкций, enforce формирует цепочку правил, которая читается как естественное описание ограничений.

Базовый пример:

import { enforce } from 'vest';

enforce('admin').equals('admin');

Проверка проходит успешно. Если условие нарушается — выбрасывается исключение.

enforce(10).greaterThan(100);

Результат:

EnforceError

Главная особенность enforce — расширяемость. Система позволяет:

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

Принцип расширения

Расширение выполняется через метод enforce.extend.

Сигнатура:

enforce.extend(object);

Каждое свойство объекта становится новым методом проверки.

Пример:

import { enforce } from 'vest';

enforce.extend({
  isEven(value) {
    return value % 2 === 0;
  },
});

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

enforce(12).isEven();

Если проверка возвращает false, enforce генерирует ошибку.

enforce(7).isEven();

Контракт пользовательского валидатора

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

enforce.extend({
  validator(value) {
    return true;
  },
});

Возвращаемые значения:

Значение Результат
true Проверка успешна
false Проверка провалена
string Ошибка с текстом
Promise Асинхронная проверка

Возврат текстового сообщения

Вместо false можно вернуть строку.

enforce.extend({
  isPositive(value) {
    return value > 0 || 'Число должно быть положительным';
  },
});

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

enforce(-10).isPositive();

Ошибка будет содержать пользовательское сообщение.


Параметры пользовательских правил

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

enforce.extend({
  longerThan(value, min) {
    return value.length > min;
  },
});

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

enforce('Alexander').longerThan(5);

Несколько параметров

Правила могут принимать произвольное количество аргументов.

enforce.extend({
  between(value, min, max) {
    return value >= min && value <= max;
  },
});

Пример:

enforce(15).between(10, 20);

Доступ к исходному значению

Каждая цепочка сохраняет текущее значение.

enforce.extend({
  startsWithUppercase(value) {
    return /^[A-Z]/.test(value);
  },
});
enforce('John').startsWithUppercase();

Создание доменных правил

Расширения особенно полезны в бизнес-логике.

Проверка ИИН Казахстана

enforce.extend({
  isIIN(value) {
    return /^\d{12}$/.test(value);
  },
});

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

enforce('990101300123').isIIN();

Проверка BIN

enforce.extend({
  isBIN(value) {
    return /^\d{12}$/.test(value);
  },
});

Проверка номера телефона

enforce.extend({
  isKzPhone(value) {
    return /^(\+7|8)\d{10}$/.test(value);
  },
});

Комбинирование расширений

Методы можно вызывать цепочкой.

enforce.extend({
  hasUppercase(value) {
    return /[A-Z]/.test(value);
  },

  hasDigit(value) {
    return /\d/.test(value);
  },
});

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

enforce('Password1')
  .hasUppercase()
  .hasDigit();

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

Расширения полностью интегрируются в Vest Suite.

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

enforce.extend({
  isAdult(value) {
    return value >= 18;
  },
});

const suite = create((data) => {
  test('age', 'Возраст меньше допустимого', () => {
    enforce(data.age).isAdult();
  });
});

Создание асинхронных правил

enforce поддерживает Promise.

enforce.extend({
  async usernameAvailable(value) {
    const response = await fetch(`/api/users/${value}`);

    return response.status === 404;
  },
});

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

await enforce('alex').usernameAvailable();

Асинхронные проверки в Suite

test('username', 'Имя уже занято', async () => {
  await enforce(data.username).usernameAvailable();
});

Композиция правил

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

enforce.extend({
  strongPassword(value) {
    enforce(value).longerThan(7);

    return (
      /[A-Z]/.test(value) &&
      /\d/.test(value)
    );
  },
});

Создание DSL для проекта

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

enforce.extend({
  validPrice(value) {
    return value >= 0;
  },

  validQuantity(value) {
    return Number.isInteger(value) && value > 0;
  },

  validSku(value) {
    return /^[A-Z]{3}-\d+$/.test(value);
  },
});

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

enforce(product.price).validPrice();

enforce(product.quantity).validQuantity();

enforce(product.sku).validSku();

Такой подход значительно повышает читаемость Suite.


Переиспользуемые модули расширений

Расширения удобно выносить в отдельные файлы.

validators/user.js

import { enforce } from 'vest';

enforce.extend({
  validUsername(value) {
    return /^[a-z0-9_]{4,20}$/i.test(value);
  },

  validDisplayName(value) {
    return value.length >= 2;
  },
});

Подключение

import './validators/user';

Изоляция расширений

Расширения глобальны для экземпляра enforce.

После регистрации метод доступен везде:

enforce.extend({
  customRule(value) {
    return true;
  },
});
enforce('x').customRule();

Поэтому рекомендуется:

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

Конфликт имён

Если имя совпадает с существующим методом, новое правило переопределит старое.

enforce.extend({
  equals() {
    return true;
  },
});

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


Проверки объектов

Расширения подходят для сложных структур.

enforce.extend({
  hasRequiredAddress(value) {
    return (
      value &&
      value.city &&
      value.street &&
      value.zipcode
    );
  },
});

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

enforce(address).hasRequiredAddress();

Проверка массивов

enforce.extend({
  notEmptyArray(value) {
    return Array.isArray(value) && value.length > 0;
  },
});

Проверка элементов массива

enforce.extend({
  allPositive(value) {
    return value.every((item) => item > 0);
  },
});

Использование исключений

Правило может выбрасывать ошибки вручную.

enforce.extend({
  safeJson(value) {
    try {
      JSON.parse(value);

      return true;
    } catch {
      throw new Error('Некорректный JSON');
    }
  },
});

Возврат сложной логики

enforce.extend({
  validDiscount(value, product) {
    if (product.isPremium) {
      return value <= 50;
    }

    return value <= 20;
  },
});

Проверка дат

enforce.extend({
  futureDate(value) {
    return new Date(value) > new Date();
  },
});

Проверка диапазонов дат

enforce.extend({
  validDateRange(value, endDate) {
    return new Date(value) < new Date(endDate);
  },
});

Проверка URL

enforce.extend({
  secureUrl(value) {
    return /^https:\/\//.test(value);
  },
});

Проверка email корпоративного домена

enforce.extend({
  corporateEmail(value, domain) {
    return value.endsWith(`@${domain}`);
  },
});

Генерация универсальных фабрик

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

function minLengthRule(min) {
  return {
    [`min${min}`](value) {
      return value.length >= min;
    },
  };
}

enforce.extend(minLengthRule(10));

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

enforce('longpassword').min10();

Использование замыканий

function createEnumRule(values) {
  return {
    inEnum(value) {
      return values.includes(value);
    },
  };
}

enforce.extend(
  createEnumRule(['admin', 'moderator'])
);

Расширения и TypeScript

В TypeScript необходимо расширять интерфейсы типов.

declare module 'vest' {
  interface EnforceCustomMatchers<R = unknown> {
    isEven(): R;
    longerThan(length: number): R;
  }
}

После этого IDE начинает распознавать пользовательские методы.


Типизация параметров

enforce.extend({
  longerThan(value: string, min: number) {
    return value.length > min;
  },
});

Generic-проверки

enforce.extend({
  notEmpty<T>(value: T[]) {
    return value.length > 0;
  },
});

Создание набора enterprise-валидаторов

enforce.extend({
  validRole(value) {
    return [
      'admin',
      'manager',
      'user',
    ].includes(value);
  },

  validCurrency(value) {
    return ['KZT', 'USD', 'EUR'].includes(value);
  },

  validLanguage(value) {
    return ['ru', 'kk', 'en'].includes(value);
  },
});

Интеграция с React-формами

test('email', 'Некорректный email', () => {
  enforce(formData.email)
    .isNotBlank()
    .matches(/.+@.+\..+/);
});

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


Производительность расширений

Рекомендуется:

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

Плохой пример:

enforce.extend({
  expensiveRule(value) {
    const regex = new RegExp('^abc');

    return regex.test(value);
  },
});

Лучше:

const regex = /^abc/;

enforce.extend({
  expensiveRule(value) {
    return regex.test(value);
  },
});

Организация каталога валидаторов

Распространённая структура:

validators/
├── common.js
├── auth.js
├── billing.js
├── products.js
├── users.js
└── index.js

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

Поскольку расширения являются обычными функциями, их удобно тестировать отдельно.

describe('isEven', () => {
  test('валидное число', () => {
    expect(() => {
      enforce(10).isEven();
    }).not.toThrow();
  });

  test('невалидное число', () => {
    expect(() => {
      enforce(5).isEven();
    }).toThrow();
  });
});

Инкапсуляция сложных бизнес-правил

enforce.extend({
  canPurchaseAlcohol(user) {
    return (
      user.age >= 21 &&
      user.country !== 'restricted'
    );
  },
});

Создание декларативной модели валидации

Без расширений:

if (
  value.length < 8 ||
  !/[A-Z]/.test(value) ||
  !/\d/.test(value)
) {
  throw new Error();
}

С расширениями:

enforce(value).strongPassword();

Именно этот подход делает enforce мощным инструментом для построения масштабируемой системы валидации.