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

Проверка объектов — одна из ключевых задач при работе с формами, DTO, конфигурациями, API-ответами и вложенными структурами данных. Библиотека Vest предоставляет гибкий механизм декларативной валидации, позволяющий проверять как простые поля, так и сложные древовидные структуры.

Базовая проверка объекта

Рассмотрим объект пользователя:

const user = {
  firstName: 'Alex',
  lastName: 'Johnson',
  email: 'alex@example.com',
  age: 27
};

Создание suite:

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

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

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

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

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

Запуск проверки:

const result = suite(user);

console.log(result.hasErrors()); // false

Проверка отсутствующих свойств

Vest не требует полного соответствия структуры объекта. Если свойство отсутствует, его можно обработать вручную.

const user = {
  firstName: 'Alex'
};
test('email', 'Email обязателен', () => {
  enforce(user.email).isDefined();
});

Также можно комбинировать проверки:

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

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

Объекты часто содержат вложенные структуры:

const user = {
  profile: {
    city: 'Berlin',
    country: 'Germany'
  }
};

Проверка:

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

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

Хотя имя теста содержит точечную нотацию, Vest не интерпретирует путь автоматически. Это всего лишь идентификатор теста. Доступ к данным осуществляется вручную.

Безопасный доступ к вложенным полям

При отсутствии вложенного объекта возможна ошибка:

data.profile.city

Если profile === undefined, произойдёт исключение.

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

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

Либо:

const city = data.profile && data.profile.city;

enforce(city).isNotBlank();

Проверка нескольких уровней вложенности

Пример структуры:

const company = {
  address: {
    location: {
      city: 'Paris',
      zip: '75001'
    }
  }
};

Проверка:

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

  test('address.location.zip', 'ZIP обязателен', () => {
    enforce(data.address?.location?.zip).isNotBlank();
  });
});

Проверка структуры объекта

Vest не содержит встроенного schema-based API, как Joi или Yup. Проверка структуры реализуется вручную.

Например:

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

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

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

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

test('username', 'Имя пользователя должно быть строкой', () => {
  enforce(data.username).isString();
});

Проверка объектов с массивами

Пример:

const user = {
  name: 'Alex',
  roles: ['admin', 'editor']
};

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

test('roles', 'Должна быть хотя бы одна роль', () => {
  enforce(data.roles).isArray();
  enforce(data.roles.length).greaterThan(0);
});

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

Пример:

const data = {
  users: [
    { name: 'Alex', age: 25 },
    { name: '', age: 15 }
  ]
};

Проверка:

const suite = create((data = {}) => {
  data.users.forEach((user, index) => {
    test(`users.${index}.name`, 'Имя обязательно', () => {
      enforce(user.name).isNotBlank();
    });

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

Генерация динамических путей

Имена тестов могут строиться динамически:

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

Это особенно полезно при отображении ошибок рядом с элементами формы.

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

Иногда объект может отсутствовать полностью:

const user = {
  settings: null
};

Проверка:

test('settings', 'Настройки должны быть объектом', () => {
  if (data.settings !== null) {
    enforce(data.settings).isObject();
  }
});

Либо:

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

Проверка объекта целиком

Иногда требуется валидировать не отдельные поля, а состояние объекта в целом.

Например:

const product = {
  price: 100,
  discount: 150
};

Проверка логики:

test('product', 'Скидка не может превышать цену', () => {
  enforce(data.discount).lessThanOrEquals(data.price);
});

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

Пример адреса доставки:

const address = {
  country: 'USA',
  state: ''
};

Правило:

test('state', 'Для США необходимо указать штат', () => {
  if (data.country === 'USA') {
    enforce(data.state).isNotBlank();
  }
});

Валидация объекта с условными правилами

Vest позволяет строить сложные сценарии.

const payment = {
  method: 'card',
  cardNumber: ''
};

Проверка:

test('cardNumber', 'Номер карты обязателен', () => {
  if (data.method === 'card') {
    enforce(data.cardNumber).isNotBlank();
  }
});

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

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

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

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

Если isGuest === true, тесты внутри omitWhen пропускаются.

Группировка проверок объектов

При работе с большими объектами полезно логически разделять проверки.

const suite = create(data => {
  // Блок профиля
  test('profile.name', 'Имя обязательно', () => {
    enforce(data.profile?.name).isNotBlank();
  });

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

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

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

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

Проверки вложенных объектов удобно выносить в отдельные функции.

function validateAddress(address) {
  enforce(address.city).isNotBlank();
  enforce(address.zip).isNotBlank();
}

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

test('address', 'Адрес некорректен', () => {
  validateAddress(data.address);
});

Более гибкий вариант:

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

  test(`${prefix}.zip`, 'ZIP обязателен', () => {
    enforce(address.zip).isNotBlank();
  });
}

Проверка DTO из API

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

fetch('/api/user')
  .then(res => res.json())
  .then(data => {
    const result = suite(data);

    if (result.hasErrors()) {
      console.log(result.getErrors());
    }
  });

Пример проверки:

const suite = create(data => {
  test('id', 'ID обязателен', () => {
    enforce(data.id).isNumber();
  });

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

Нормализация перед проверкой

Перед валидацией объект часто преобразуется.

const normalized = {
  ...data,
  email: data.email?.trim(),
  username: data.username?.toLowerCase()
};

После этого:

const result = suite(normalized);

Проверка пустых объектов

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

test('settings', 'Объект настроек пуст', () => {
  enforce(Object.keys(data.settings).length).greaterThan(0);
});

Либо:

test('settings', 'Настройки обязательны', () => {
  enforce(data.settings).isNotEmpty();
});

Проверка уникальности значений внутри объекта

Пример:

const data = {
  tags: ['js', 'react', 'js']
};

Проверка:

test('tags', 'Теги должны быть уникальными', () => {
  const unique = new Set(data.tags);

  enforce(unique.size).equals(data.tags.length);
});

Валидация сложных конфигурационных объектов

Пример:

const config = {
  server: {
    host: 'localhost',
    port: 3000
  },
  security: {
    ssl: true,
    cert: ''
  }
};

Проверка:

const suite = create(data => {
  test('server.host', 'Host обязателен', () => {
    enforce(data.server?.host).isNotBlank();
  });

  test('server.port', 'Порт должен быть числом', () => {
    enforce(data.server?.port).isNumber();
  });

  test('security.cert', 'SSL сертификат обязателен', () => {
    if (data.security?.ssl) {
      enforce(data.security.cert).isNotBlank();
    }
  });
});

Проверка объектов с пользовательскими правилами

Vest поддерживает расширение enforce.

enforce.extend({
  isValidCoordinates(value) {
    return (
      typeof value.lat === 'number' &&
      typeof value.lng === 'number'
    );
  }
});

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

test('location', 'Некорректные координаты', () => {
  enforce(data.location).isValidCoordinates();
});

Асинхронная проверка объектов

Пример проверки объекта пользователя через API:

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

const suite = create(async data => {
  test('username', 'Имя уже занято', async () => {
    const response = await fetch(`/api/check/${data.username}`);
    const result = await response.json();

    enforce(result.available).isTruthy();
  });
});

Проверка объектов с each

Для коллекций удобно использовать вспомогательные функции.

data.items.forEach((item, index) => {
  test(`items.${index}.price`, 'Цена обязательна', () => {
    enforce(item.price).isNumber();
  });
});

Получение ошибок вложенных объектов

После выполнения suite:

const result = suite(data);

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

result.getErrors();

Пример результата:

{
  'profile.name': ['Имя обязательно'],
  'address.city': ['Город обязателен']
}

Получение ошибок конкретного поля:

result.getErrors('profile.name');

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

Vest поддерживает selective validation.

suite(data, 'email');

Будет выполнена только проверка поля email.

Для вложенных структур:

suite(data, 'profile.city');

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

Производительность при работе с большими объектами

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

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

Нежелательный вариант:

test('items', 'Ошибка', () => {
  hugeArray.filter(...).map(...).reduce(...);
});

Предпочтительно:

const prepared = preprocess(hugeArray);

test('items', 'Ошибка', () => {
  enforce(prepared.valid).isTruthy();
});

Архитектура проверки сложных объектов

Крупные схемы удобно разделять по доменным областям.

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

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

Главная suite:

const suite = create(data => {
  validateProfile(data.profile);
  validateAddress(data.address);
});

Такой подход облегчает:

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