Синхронная валидация

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

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

В библиотеке Vest синхронная валидация строится вокруг декларативных тестов, объединённых в suite — сценарий проверки формы или объекта.


Создание suite

Основной механизм Vest — функция create.

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

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

Что происходит в этом примере

  1. Создаётся suite.
  2. Внутрь передаётся функция с объектом данных.
  3. test описывает отдельную проверку.
  4. enforce выполняет утверждение.
  5. При нарушении условия тест считается проваленным.

Структура test

Сигнатура теста:

test(fieldName, errorMessage, callback);

Аргументы

Аргумент Описание
fieldName Название поля
errorMessage Текст ошибки
callback Логика проверки

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

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

const suite = create((data) => {

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

});

Результат выполнения

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

console.log(result.getErrors());

Результат:

{
  email: ['Email обязателен']
}

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

Vest использует API enforce, напоминающий fluent-интерфейс.

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

Дополнительные проверки длины

enforce(value).shorterThan(20);

enforce(value).shorterThanOrEquals(50);

enforce(value).longerThan(3);

Проверка обязательных полей

Проверка на пустое значение

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

Проверка на undefined

test('age', 'Возраст обязателен', () => {
  enforce(data.age).isDefined();
});

Проверка на null

test('token', 'Токен отсутствует', () => {
  enforce(data.token).isNotNull();
});

Проверка числовых значений

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

test('age', 'Возраст должен быть больше 18', () => {
  enforce(data.age).greaterThan(18);
});

Проверка максимального значения

test('price', 'Цена слишком высокая', () => {
  enforce(data.price).lessThan(1000);
});

Проверка включительно

enforce(value).greaterThanOrEquals(1);

enforce(value).lessThanOrEquals(10);

Проверка типов

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

test('title', 'Должна быть строка', () => {
  enforce(data.title).isString();
});

Проверка числа

test('count', 'Должно быть число', () => {
  enforce(data.count).isNumber();
});

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

test('tags', 'Должен быть массив', () => {
  enforce(data.tags).isArray();
});

Проверка boolean

test('active', 'Некорректное значение', () => {
  enforce(data.active).isBoolean();
});

Проверка email

test('email', 'Некорректный email', () => {
  enforce(data.email).matches(
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  );
});

Проверка через регулярные выражения

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

test('phone', 'Неверный номер телефона', () => {
  enforce(data.phone).matches(/^\+7\d{10}$/);
});

Проверка пароля

test('password', 'Пароль слишком простой', () => {
  enforce(data.password).matches(
    /^(?=.*[A-Z])(?=.*\d).+$/
  );
});

Комбинирование проверок

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

test('username', 'Некорректное имя', () => {

  enforce(data.username).isNotBlank();

  enforce(data.username).longerThanOrEquals(3);

  enforce(data.username).shorterThanOrEquals(20);

});

Однако более читаемым считается разделение на отдельные тесты.


Разделение логики на несколько test

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

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

test('username', 'Максимум 20 символов', () => {
  enforce(data.username).shorterThanOrEquals(20);
});

Преимущества такого подхода

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

Проверка взаимосвязанных полей

Подтверждение пароля

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

Условная валидация

Vest позволяет выполнять проверки только при определённых условиях.

const suite = create((data) => {

  if (data.hasDiscount) {

    test('discount', 'Скидка обязательна', () => {
      enforce(data.discount).isDefined();
    });

  }

});

Проверка вложенных объектов

test('city', 'Город обязателен', () => {
  enforce(data.address.city).isNotBlank();
});

Безопасный доступ

test('city', 'Город обязателен', () => {
  enforce(data.address?.city).isNotBlank();
});

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

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

test('tags', 'Добавьте минимум один тег', () => {
  enforce(data.tags.length).greaterThan(0);
});

Проверка содержимого массива

test('roles', 'Роль admin обязательна', () => {
  enforce(data.roles).contains('admin');
});

Использование each для коллекций

data.items.forEach((item, index) => {

  test(`items.${index}.title`, 'Название обязательно', () => {
    enforce(item.title).isNotBlank();
  });

});

Выполнение suite

const result = suite(formData);

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

Все ошибки

result.getErrors();

Ошибки поля

result.getErrors('email');

Есть ли ошибки

result.hasErrors();

Проверка конкретного поля

result.hasErrors('password');

Получение валидных полей

result.getValid();

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

result.isValid('email');

Проверка всей формы

result.isValid();

Сложный пример синхронной валидации формы

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

const registrationSuite = create((data) => {

  test('name', 'Имя обязательно', () => {
    enforce(data.name).isNotBlank();
  });

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

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

  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(
      /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    );
  });

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

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

  test('password', 'Пароль должен содержать цифру', () => {
    enforce(data.password).matches(/\d/);
  });

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

  test('age', 'Возраст должен быть больше 18', () => {
    enforce(data.age).greaterThan(18);
  });

});

Результат работы suite

const result = registrationSuite({
  name: '',
  email: 'wrong',
  password: '123',
  confirmPassword: '111',
  age: 15
});

console.log(result.getErrors());

Результат:

{
  name: [
    'Имя обязательно',
    'Минимум 2 символа'
  ],

  email: [
    'Некорректный email'
  ],

  password: [
    'Минимум 8 символов'
  ],

  confirmPassword: [
    'Пароли не совпадают'
  ],

  age: [
    'Возраст должен быть больше 18'
  ]
}

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

Vest позволяет добавлять собственные синхронные валидаторы.

Создание кастомного правила

import { enforce } from 'vest';

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

Использование собственного валидатора

test('count', 'Число должно быть чётным', () => {
  enforce(data.count).isEven();
});

Создание сложного кастомного валидатора

enforce.extend({

  isStrongPassword(value) {

    return (
      /[A-Z]/.test(value) &&
      /\d/.test(value) &&
      value.length >= 8
    );

  }

});

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

test('password', 'Слабый пароль', () => {
  enforce(data.password).isStrongPassword();
});

Группировка логики валидации

Вынос функций

function validateEmail(email) {

  enforce(email).isNotBlank();

  enforce(email).matches(
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  );

}

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

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

Переиспользуемые проверки

const passwordRules = (password) => {

  enforce(password).longerThanOrEquals(8);

  enforce(password).matches(/\d/);

};

Валидация нескольких форм

export const loginSuite = create((data) => {

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

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

});
export const profileSuite = create((data) => {

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

});

Типичные ошибки

Смешивание синхронной и асинхронной логики

Неверно:

test('email', 'Ошибка', async () => {
  await fetch('/api');
});

Синхронная валидация должна выполняться мгновенно и не содержать Promise.


Слишком много проверок в одном test

Плохо:

test('username', 'Ошибка', () => {

  enforce(data.username).isNotBlank();

  enforce(data.username).longerThanOrEquals(3);

  enforce(data.username).shorterThanOrEquals(20);

  enforce(data.username).matches(/^[a-z]+$/);

});

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


Проверка без защиты от undefined

Проблемный код:

enforce(data.user.address.city).isNotBlank();

Безопасный вариант:

enforce(data.user?.address?.city).isNotBlank();

Практический шаблон синхронной валидации

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

export const formSuite = create((data) => {

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

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

  test('field', 'Максимум 20 символов', () => {
    enforce(data.field).shorterThanOrEquals(20);
  });

});

Архитектура синхронной валидации

Эффективная структура обычно строится следующим образом:

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

Преимущества синхронной валидации в Vest

Высокая скорость

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

Простая композиция

Тесты легко комбинируются и масштабируются.

Декларативный подход

Логика описывается как набор правил, а не как цепочка условий.

Читаемость

Каждый test отвечает за одно конкретное правило.

Переиспользуемость

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

Предсказуемость

Синхронные проверки не зависят от состояния асинхронных процессов и всегда выполняются последовательно.