Функция create

Функция create — центральный механизм библиотеки Vest, предназначенный для создания валидационных suites. Она формирует изолированное пространство проверки, в котором описываются правила, зависимости, условия выполнения и состояние валидации.

Через create определяется:

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

Базовая сигнатура:

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

const suite = create((data = {}) => {
  test('username', 'Имя пользователя обязательно', () => {
    enforce(data.username).isNotBlank();
  });
});

После создания suite функция может вызываться многократно:

const result = suite({
  username: '',
});

Архитектура validation suite

create возвращает функцию-валидатор, сохраняющую внутреннее состояние между вызовами.

const suite = create((data) => {
  // правила
});

Внутри такого suite библиотека хранит:

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

Это позволяет Vest выполнять оптимизированную повторную валидацию без полного пересчёта всех правил.


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

Простейший пример:

import { create, test } from 'vest';

const suite = create((data) => {
  test('email', 'Email обязателен', () => {
    if (!data.email) {
      throw new Error();
    }
  });
});

Вызов:

const result = suite({
  email: '',
});

Проверка ошибок:

result.hasErrors('email');

Получение сообщений:

result.getErrors('email');

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

Чаще всего create применяется совместно с enforce.

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

const suite = create((data) => {
  test('password', 'Минимум 8 символов', () => {
    enforce(data.password).longerThanOrEquals(8);
  });
});

enforce предоставляет декларативный API проверок:

enforce(value).isString();
enforce(value).isNotEmpty();
enforce(value).matches(/[A-Z]/);

Аргументы callback-функции

Функция, передаваемая в create, может принимать параметры.

const suite = create((data, currentField) => {
  // ...
});

Первый аргумент

Обычно содержит данные формы.

suite({
  email: 'admin@mail.com',
  password: '12345678',
});

Второй аргумент

Используется для selective validation.

suite(data, 'email');

Пример:

const suite = create((data, currentField) => {
  only(currentField);

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

  test('password', 'Слишком короткий пароль', () => {
    enforce(data.password).longerThan(5);
  });
});

Повторное использование suite

Suite создаётся один раз и используется повторно.

const loginSuite = create((data) => {
  test('email', 'Введите email', () => {
    enforce(data.email).isNotBlank();
  });
});

Повторные вызовы:

loginSuite({ email: '' });
loginSuite({ email: 'user@mail.com' });

Состояние обновляется автоматически.


Состояние валидации

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

const result = suite(data);

Проверка ошибок

result.hasErrors();
result.hasErrors('email');

Проверка pending-состояний

result.isPending();
result.isPending('email');

Получение ошибок

result.getErrors();
result.getErrors('email');

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

result.isValid();

Selective Validation

Одна из ключевых возможностей create — частичная валидация.

suite(data, 'email');

В сочетании с only:

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

const suite = create((data, currentField) => {
  only(currentField);

  test('email', 'Email обязателен', () => {
    enforce(data.email).isNotBlank();
  });

  test('password', 'Введите пароль', () => {
    enforce(data.password).isNotBlank();
  });
});

Если вызывается:

suite(data, 'email');

проверяется только поле email.


Условная логика внутри create

Suite поддерживает обычный JavaScript-код.

const suite = create((data) => {
  if (data.accountType === 'company') {
    test('companyName', 'Введите название компании', () => {
      enforce(data.companyName).isNotBlank();
    });
  }
});

Это важное отличие Vest от декларативных схемных валидаторов.


Вложенные условия

const suite = create((data) => {
  if (data.isAdmin) {
    test('accessLevel', 'Укажите уровень доступа', () => {
      enforce(data.accessLevel).isNotBlank();
    });

    if (data.superAdmin) {
      test('secretKey', 'Нужен секретный ключ', () => {
        enforce(data.secretKey).isNotBlank();
      });
    }
  }
});

Асинхронная валидация

create поддерживает async/await.

const suite = create((data) => {
  test(
    'email',
    'Email уже используется',
    async () => {
      const exists = await checkEmail(data.email);

      enforce(exists).isFalsy();
    }
  );
});

Во время выполнения:

result.isPending('email');

вернёт true.


Параллельное выполнение тестов

Асинхронные проверки внутри suite выполняются независимо.

const suite = create((data) => {
  test('email', 'Email занят', async () => {
    await verifyEmail(data.email);
  });

  test('username', 'Имя занято', async () => {
    await verifyUsername(data.username);
  });
});

Vest управляет состоянием каждого теста отдельно.


Группировка правил

create позволяет логически группировать проверки.

const suite = create((data) => {
  if (data.step === 1) {
    test('name', 'Введите имя', () => {
      enforce(data.name).isNotBlank();
    });
  }

  if (data.step === 2) {
    test('address', 'Введите адрес', () => {
      enforce(data.address).isNotBlank();
    });
  }
});

Подобная структура особенно полезна в многошаговых формах.


Работа с skip

Некоторые проверки можно пропускать.

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

const suite = create((data) => {
  skip(data.disablePasswordValidation, () => {
    test('password', 'Пароль обязателен', () => {
      enforce(data.password).isNotBlank();
    });
  });
});

Работа с omitWhen

omitWhen полностью исключает тесты из выполнения.

import { create, omitWhen } from 'vest';

const suite = create((data) => {
  omitWhen(data.isGuest, () => {
    test('passport', 'Введите паспортные данные', () => {
      enforce(data.passport).isNotBlank();
    });
  });
});

Разница между skip и omitWhen:

Механизм Тест создаётся Тест выполняется
skip Да Нет
omitWhen Нет Нет

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

Помимо ошибок suite поддерживает предупреждения.

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

const suite = create((data) => {
  warn();

  test('password', 'Пароль слишком простой', () => {
    enforce(data.password).longerThan(6);
  });
});

Проверка предупреждений:

result.hasWarnings();

Динамическая генерация тестов

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

const fields = ['firstName', 'lastName', 'city'];

const suite = create((data) => {
  fields.forEach((field) => {
    test(field, `${field} обязательно`, () => {
      enforce(data[field]).isNotBlank();
    });
  });
});

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

const suite = create((data) => {
  for (const phone of data.phones) {
    test(`phone_${phone.id}`, 'Некорректный номер', () => {
      enforce(phone.number).matches(/^\d+$/);
    });
  }
});

Композиция validation logic

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

function validateProfile(data) {
  test('firstName', 'Введите имя', () => {
    enforce(data.firstName).isNotBlank();
  });

  test('lastName', 'Введите фамилию', () => {
    enforce(data.lastName).isNotBlank();
  });
}

function validateSecurity(data) {
  test('password', 'Пароль слишком короткий', () => {
    enforce(data.password).longerThan(7);
  });
}

const suite = create((data) => {
  validateProfile(data);
  validateSecurity(data);
});

Использование внешнего состояния

Suite может использовать внешние переменные.

const blockedDomains = ['spam.com'];

const suite = create((data) => {
  test('email', 'Домен запрещён', () => {
    const domain = data.email.split('@')[1];

    enforce(blockedDomains.includes(domain)).isFalsy();
  });
});

Ошибки выполнения

Если внутри теста выбрасывается ошибка, Vest интерпретирует это как failed validation.

test('email', 'Email обязателен', () => {
  if (!data.email) {
    throw new Error();
  }
});

Однако предпочтительнее использовать enforce.


Изоляция состояния

Каждый вызов create создаёт независимый validation context.

const loginSuite = create(() => {});
const registerSuite = create(() => {});

Их состояния не пересекаются.


Очистка состояния

Некоторые сценарии требуют сброса состояния suite.

suite.reset();

После reset:

  • очищаются ошибки;
  • удаляются pending-задачи;
  • стирается история выполнения.

Работа с only

only ограничивает набор выполняемых тестов.

const suite = create((data, fieldName) => {
  only(fieldName);

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

Это критически важно для производительности больших форм.


Производительность create

Vest использует несколько оптимизаций:

  • selective validation;
  • кэширование состояния;
  • независимые async-тесты;
  • повторное использование suite;
  • lazy execution.

Поэтому create подходит для крупных интерфейсов с большим количеством полей.


Создание сложной формы

Пример полноценного suite:

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

const registrationSuite = create((data, currentField) => {
  only(currentField);

  test('email', 'Введите email', () => {
    enforce(data.email).isNotBlank();
    enforce(data.email).matches(/@/);
  });

  test('password', 'Минимум 8 символов', () => {
    enforce(data.password).longerThanOrEquals(8);
  });

  test('confirmPassword', 'Пароли не совпадают', () => {
    enforce(data.confirmPassword).equals(data.password);
  });

  omitWhen(!data.isCompany, () => {
    test('companyName', 'Введите название компании', () => {
      enforce(data.companyName).isNotBlank();
    });
  });
});

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

const result = registrationSuite(formData, 'email');

Проверка состояния:

result.hasErrors('email');
result.getErrors('email');
result.isValid();

Практика организации suites

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

validators/
├── authSuite.js
├── profileSuite.js
├── paymentSuite.js
└── shared/
    ├── emailRules.js
    └── passwordRules.js

Пример переиспользуемых правил:

export function emailRules(value) {
  enforce(value).isNotBlank();
  enforce(value).matches(/@/);
}

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

test('email', 'Некорректный email', () => {
  emailRules(data.email);
});

Типичные ошибки при использовании create

Создание suite внутри компонента

Плохо:

function Component() {
  const suite = create(() => {});
}

Каждый рендер создаёт новый state container.

Правильно:

const suite = create(() => {});

function Component() {

}

Полная валидация на каждый ввод

Плохо:

suite(data);

на каждое изменение поля.

Лучше:

suite(data, fieldName);

Смешивание бизнес-логики и валидации

Плохо:

test('price', 'Ошибка', () => {
  saveToDatabase();
});

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


Интеграция с UI-фреймворками

create не зависит от конкретного UI.

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

  • React
  • Vue.js
  • Angular
  • Svelte
  • обычным JavaScript.

Пример для React:

const result = suite(formData, changedField);

if (result.hasErrors('email')) {
  setError(result.getErrors('email')[0]);
}

Внутренний жизненный цикл suite

Каждый вызов проходит несколько этапов:

  1. запуск callback внутри create;
  2. регистрация тестов;
  3. выполнение sync-проверок;
  4. запуск async-проверок;
  5. обновление validation state;
  6. возврат result object.

Такой подход делает create не просто функцией запуска тестов, а полноценным orchestration-механизмом системы валидации.