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

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

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

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


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

Наиболее распространённая задача — ограничение минимального и максимального количества элементов.

Минимальное количество элементов

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

const suite = vest.create('tags_form', (data = {}) => {
  test('tags', 'Добавьте хотя бы один тег', () => {
    enforce(data.tags).isArray();
    enforce(data.tags.length).greaterThan(0);
  });
});

Проверка выполняется в два этапа:

  1. подтверждение, что значение действительно массив;
  2. контроль количества элементов.

Ограничение максимального размера

test('tags', 'Допустимо не более 10 тегов', () => {
  enforce(data.tags.length).lessThanOrEquals(10);
});

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

test('skills', 'Количество навыков должно быть от 3 до 8', () => {
  enforce(data.skills.length)
    .greaterThanOrEquals(3);

  enforce(data.skills.length)
    .lessThanOrEquals(8);
});

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

Во многих случаях массив существует, но содержит пустые элементы.

Непустые строки

test('emails', 'Список содержит пустые email', () => {
  data.emails.forEach(email => {
    enforce(email.trim()).isNotBlank();
  });
});

Игнорирование пробелов

test('tags', 'Тег не может состоять только из пробелов', () => {
  data.tags.forEach(tag => {
    enforce(tag.trim()).longerThan(0);
  });
});

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

Массивы часто содержат объекты.

Массив объектов пользователей

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

Проверка:

test('users', 'Некорректный пользователь', () => {
  data.users.forEach(user => {
    enforce(user.name).isNotBlank();
    enforce(user.age).greaterThan(17);
  });
});

Валидация вложенных свойств

Проверка адресов

test('addresses', 'Заполните адрес полностью', () => {
  data.addresses.forEach(address => {
    enforce(address.city).isNotBlank();
    enforce(address.street).isNotBlank();
    enforce(address.zip).matches(/^\d{6}$/);
  });
});

Разделение ошибок по индексам

При работе с динамическими формами важно понимать, какой именно элемент массива содержит ошибку.

Использование динамических названий полей

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

Теперь ошибки будут храниться отдельно:

users[0].email
users[1].email
users[2].email

Это особенно важно для интерфейсов с повторяющимися блоками формы.


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

Уникальные email

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

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

Уникальность объектов

Например, массив товаров:

[
  { id: 1, title: 'Phone' },
  { id: 2, title: 'Laptop' }
]

Проверка:

test('products', 'Товары дублируются', () => {
  const ids = data.products.map(product => product.id);

  const unique = new Set(ids);

  enforce(unique.size).equals(ids.length);
});

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

Хотя бы один элемент должен удовлетворять условию

test('roles', 'Нужен хотя бы один администратор', () => {
  const hasAdmin = data.roles.some(
    role => role === 'admin'
  );

  enforce(hasAdmin).isTruthy();
});

Все элементы должны соответствовать условию

test('prices', 'Цена должна быть положительной', () => {
  const valid = data.prices.every(
    price => price > 0
  );

  enforce(valid).isTruthy();
});

Проверка массивов файлов

Формы загрузки файлов обычно используют массивы.

Ограничение количества файлов

test('files', 'Можно загрузить максимум 5 файлов', () => {
  enforce(data.files.length)
    .lessThanOrEquals(5);
});

Проверка размера файлов

test('files', 'Файл слишком большой', () => {
  data.files.forEach(file => {
    enforce(file.size)
      .lessThanOrEquals(5 * 1024 * 1024);
  });
});

Проверка типов файлов

test('files', 'Недопустимый тип файла', () => {
  const allowed = [
    'image/png',
    'image/jpeg'
  ];

  data.files.forEach(file => {
    enforce(allowed.includes(file.type))
      .isTruthy();
  });
});

Вложенные массивы

Иногда массив содержит другие массивы.

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

const data = {
  matrix: [
    [1, 2],
    [3, 4]
  ]
};

Проверка вложенных элементов

test('matrix', 'Матрица содержит отрицательные числа', () => {
  data.matrix.forEach(row => {
    row.forEach(cell => {
      enforce(cell).greaterThan(0);
    });
  });
});

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

Пример заказа

const data = {
  items: [
    {
      price: 100,
      quantity: 2
    }
  ]
};

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

test('items', 'Количество должно быть больше нуля', () => {
  data.items.forEach(item => {
    enforce(item.quantity).greaterThan(0);
  });
});

test('items', 'Цена должна быть положительной', () => {
  data.items.forEach(item => {
    enforce(item.price).greaterThan(0);
  });
});

Проверка комбинаций значений

Недопустимые сочетания

test('permissions', 'Конфликт ролей', () => {
  const hasGuest = data.permissions.includes('guest');
  const hasAdmin = data.permissions.includes('admin');

  enforce(hasGuest && hasAdmin).isFalsy();
});

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

Проверки могут зависеть от других полей формы.

Проверка только при включённой опции

test('phones', 'Добавьте телефон', () => {
  if (!data.enablePhones) {
    return;
  }

  enforce(data.phones.length)
    .greaterThan(0);
});

Валидация отдельных элементов

Vest позволяет запускать проверку только изменённой части формы.

Частичная проверка

suite(data, `users[2].email`);

Будет выполнена только проверка:

test('users[2].email', ...)

Это существенно уменьшает нагрузку при больших массивах.


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

Массивы часто становятся необязательными.

Пропуск проверки

import { skipWhen } from 'vest';

skipWhen(
  data.tags.length === 0,
  () => {
    test('tags', 'Слишком много тегов', () => {
      enforce(data.tags.length)
        .lessThanOrEquals(10);
    });
  }
);

Проверка массивов с асинхронными операциями

Например, проверка списка логинов на сервере.

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

test.memo(
  'users',
  'Логин уже существует',
  async () => {
    for (const user of data.users) {
      const response = await fetch(
        `/api/users/${user.login}`
      );

      const exists = await response.json();

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

Оптимизация больших массивов

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

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

data.items.forEach(item => {
  const duplicate = data.items.find(
    current => current.id === item.id
  );

  enforce(duplicate).isDefined();
});

Здесь выполняется вложенный поиск.


Оптимизированный вариант

test('items', 'Найдены дубликаты', () => {
  const ids = new Set();

  data.items.forEach(item => {
    enforce(ids.has(item.id)).isFalsy();

    ids.add(item.id);
  });
});

Сложность уменьшается с O(n²) до O(n).


Использование группировок

Группы для массивов

import { group } from 'vest';

group('users', () => {
  data.users.forEach((user, index) => {
    test(
      `users[${index}].name`,
      'Введите имя',
      () => {
        enforce(user.name).isNotBlank();
      }
    );
  });
});

Группы помогают:

  • логически объединять проверки;
  • выполнять выборочную валидацию;
  • упрощать обработку ошибок.

Обработка динамического удаления элементов

При удалении элементов индексы меняются.

Потенциальная проблема

users[0]
users[1]
users[2]

После удаления users[1]:

users[0]
users[1]

Ошибки могут смещаться.


Решение через идентификаторы

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

Такой подход более стабилен для интерфейсов с динамическими списками.


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

Комплексная валидация массива товаров

test('products', 'Список товаров пуст', () => {
  enforce(data.products.length)
    .greaterThan(0);
});

data.products.forEach((product, index) => {
  test(
    `products[${index}].title`,
    'Введите название',
    () => {
      enforce(product.title).isNotBlank();
    }
  );

  test(
    `products[${index}].price`,
    'Некорректная цена',
    () => {
      enforce(product.price).greaterThan(0);
    }
  );

  test(
    `products[${index}].stock`,
    'Некорректный остаток',
    () => {
      enforce(product.stock)
        .greaterThanOrEquals(0);
    }
  );
});

Создание переиспользуемых валидаторов

Функция проверки массива email

function validateEmails(emails) {
  emails.forEach(email => {
    enforce(email)
      .matches(/^\S+@\S+\.\S+$/);
  });
}

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

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

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

Дерево категорий

const categories = [
  {
    title: 'Books',
    children: [
      {
        title: 'Programming',
        children: []
      }
    ]
  }
];

Рекурсивная валидация

function validateCategory(categories) {
  categories.forEach(category => {
    enforce(category.title).isNotBlank();

    if (category.children) {
      validateCategory(category.children);
    }
  });
}

test('categories', 'Ошибка структуры категорий', () => {
  validateCategory(data.categories);
});

Проверка массивов через enforce.shape

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

test('users', 'Некорректный пользователь', () => {
  data.users.forEach(user => {
    enforce(user).shape({
      name: value => enforce(value).isNotBlank(),
      age: value => enforce(value).greaterThan(17)
    });
  });
});

Такой подход делает код более декларативным.


Типичные ошибки при валидации массивов

Отсутствие проверки типа

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

enforce(data.tags.length).greaterThan(0);

Если tags окажется null, произойдёт ошибка.

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

enforce(data.tags).isArray();
enforce(data.tags.length).greaterThan(0);

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

Недостаточно:

enforce(data.users.length).greaterThan(0);

Необходимо также валидировать содержимое массива.


Использование общих ошибок

Плохой вариант:

test('users', 'Ошибка в списке пользователей', () => {

Лучше:

test(`users[${index}].email`, ...)

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


Практический пример полной проверки массива

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

const suite = vest.create('order_form', (data = {}) => {

  test('items', 'Добавьте товары', () => {
    enforce(data.items).isArray();
    enforce(data.items.length)
      .greaterThan(0);
  });

  group('items', () => {

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

      test(
        `items[${index}].title`,
        'Введите название товара',
        () => {
          enforce(item.title)
            .isNotBlank();
        }
      );

      test(
        `items[${index}].price`,
        'Цена должна быть больше нуля',
        () => {
          enforce(item.price)
            .greaterThan(0);
        }
      );

      test(
        `items[${index}].quantity`,
        'Количество должно быть больше нуля',
        () => {
          enforce(item.quantity)
            .greaterThan(0);
        }
      );

    });

  });

});