Композиция suite

В библиотеке Vest любая логика проверки данных строится вокруг декларативного описания наборов правил, которые объединяются в единые структуры. Эти структуры называются suite и представляют собой базовую единицу композиции. Именно механизм композиции suite определяет масштабируемость валидаторов, их повторное использование и возможность построения сложных проверочных сценариев без дублирования кода.

Suite в Vest — это не просто контейнер тестов, а функциональная единица, которая сама может содержать другие suite, тесты (test), условные ветвления и вспомогательные конструкции. Такой подход позволяет рассматривать валидацию как дерево зависимостей, где каждая ветвь может быть переиспользована или встроена в другую ветку без изменения внутренней логики.

Ключевая особенность заключается в том, что suite является функцией, принимающей контекст выполнения, но при этом возвращающей декларативную структуру:

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

const loginSuite = suite('login', (data) => {
  test('email', 'Invalid email', () => {
    enforce(data.email).isEmail();
  });

  test('password', 'Password is required', () => {
    enforce(data.password).isNotEmpty();
  });
});

Даже в таком простом виде suite уже является композируемым блоком, который можно встроить в более крупную систему.

Вложенная композиция suite

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

const addressSuite = suite('address', (data) => {
  test('city', 'City is required', () => {
    enforce(data.city).isNotEmpty();
  });

  test('zip', 'ZIP is invalid', () => {
    enforce(data.zip).matches(/^\d{5}$/);
  });
});

const userSuite = suite('user', (data) => {
  test('name', 'Name required', () => {
    enforce(data.name).isNotEmpty();
  });

  addressSuite(data.address);
});

Здесь addressSuite становится частью userSuite, но при этом сохраняет автономность. Такой подход формирует принцип слабой связанности: изменения в addressSuite не требуют модификации userSuite, если интерфейс данных остаётся стабильным.

Переиспользование через композицию функций

Suite в Vest фактически является функцией высшего порядка, что делает возможным функциональную композицию. Один из распространённых паттернов — фабрики suite, которые создают проверочные блоки на основе параметров.

const requiredField = (fieldName, message = 'Required') =>
  (data) => {
    test(fieldName, message, () => {
      enforce(data[fieldName]).isNotEmpty();
    });
  };

const profileSuite = suite('profile', (data) => {
  requiredField('firstName', 'First name required')(data);
  requiredField('lastName', 'Last name required')(data);
});

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

Композиция через условные ветвления

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

const paymentSuite = suite('payment', (data) => {
  if (data.method === 'card') {
    suite('card', () => {
      test('cardNumber', 'Invalid card number', () => {
        enforce(data.cardNumber).matches(/^\d{16}$/);
      });
    });
  }

  if (data.method === 'paypal') {
    test('email', 'Invalid PayPal email', () => {
      enforce(data.email).isEmail();
    });
  }
});

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

Инверсия композиции через инжекцию данных

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

const createAuthSuite = (extraRules) =>
  suite('auth', (data) => {
    test('username', 'Required', () => {
      enforce(data.username).isNotEmpty();
    });

    extraRules?.(data);
  });

const strongPasswordRule = (data) => {
  test('password', 'Weak password', () => {
    enforce(data.password).longerThan(8);
  });
};

const authSuite = createAuthSuite(strongPasswordRule);

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

Композиция через декомпозицию домена

При проектировании сложных систем suite часто отражает предметную область. Разбиение на доменные блоки позволяет естественным образом организовать композицию.

const billingSuite = suite('billing', (data) => {
  test('card', 'Card missing', () => {
    enforce(data.card).isNotEmpty();
  });
});

const shippingSuite = suite('shipping', (data) => {
  test('address', 'Address required', () => {
    enforce(data.address).isNotEmpty();
  });
});

const checkoutSuite = suite('checkout', (data) => {
  billingSuite(data.billing);
  shippingSuite(data.shipping);
});

Такой подход формирует композицию на уровне архитектуры: каждый suite представляет независимый модуль, который затем агрегируется в более крупные сценарии.

Повторное использование suite как функции-строителя

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

const withLogging = (name, fn) =>
  suite(name, (data) => {
    console.log(`Validating ${name}`);
    fn(data);
  });

const secureSuite = withLogging('secure', (data) => {
  test('token', 'Missing token', () => {
    enforce(data.token).isNotEmpty();
  });
});

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

Глубокая композиция и масштабируемость структуры

При увеличении количества доменных правил suite начинает формировать многоуровневую иерархию. На практике это выражается в разделении на:

  • базовые suite (атомарные проверки)
  • доменные suite (логические группы)
  • агрегирующие suite (композиционные сценарии)

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

const validators = {
  email: (value) => enforce(value).isEmail(),
  required: (value) => enforce(value).isNotEmpty(),
};

const baseUserSuite = suite('baseUser', (data) => {
  test('email', 'Invalid email', () => validators.email(data.email));
  test('name', 'Required', () => validators.required(data.name));
});

const extendedUserSuite = suite('extendedUser', (data) => {
  baseUserSuite(data);
  test('role', 'Role required', () => {
    enforce(data.role).isNotEmpty();
  });
});

Композиция в таком виде становится механизмом построения архитектуры приложения, а не только инструментом валидации данных.