Всем привет! Поговорим немного про валидацию данных. Что в этом сложного и зачем вообще это может понадобиться, скажем, в проекте написанном на typescript? Typescript довольно неплохо всё контролирует, остаётся максимум проверять ввод пользователя. То есть закинуть в проект десяток регулярок и всё, казалось бы тему можно закрывать, но… Далеко не всегда, а в случае с вебом примерно никогда, весь проект находится в единой кодовой базе и использует одни и теже типы. На стыке таких кодовых баз как раз и возникают ситуации, когда ожидание не соответствует реальности и тут typescript уже не помощник. Несколько примеров:
- Клиентская часть приложения получает данные от АПИ и валидирует их. Во-первых, АПИ может неожиданно и порой без уведомлений меняться, во-вторых, "серверные ребята" иногда сами не в курсе на что способно их АПИ, например, в каком-то поле вместо гарантированного массива, пусть и пустого, в полнолуние может отдаваться
null
. Описывая данные на клиенте, программисты как бы определяют с чем умеет работать клиент и если что-то идёт не так, то намного приятнее сразу видеть сообщение в консоли о первоисточнике проблемы, а не расковыривать непонятный баг уже там, где это вылезло во view-слое (и хорошо ещё если это сразу будет замечено). Также сейчас уже есть решения (1, 2) позволяющие переносить типы с сервера на клиент. Я пока не пробовал так делать, но, вполне возможно, за этим будущее. - Обратная ситуация, когда сервер проверяет присланные параметры чтобы немедленно прекратить обработку запроса при их несоответствии ожидаемым. Думаю тут не нужны какие-то подробности о том, почему это важно делать.
- Валидация данных перед сохранением их в базе данных тоже будет совсем не лишней. Для примера можно посмотреть, как это организовано в одном из моих велосипедов: MaraquiaORM#Валидация.
Я думаю примеры вполне убедительны и теперь уже нет ощущения, что можно обойтись простыми регулярками, ведь речь не просто про пользовательский ввод, а про валидацию сложных, обычно вложенных на несколько уровней данных. Здесь уже нужна специальная библиотека. И такие конечно же есть! Так уж получается, что за последние лет 10, каждый раз начиная новый проект, я пытаюсь заиспользовать в нём очередную такую библиотеку, подстроив её под свои нужды. И каждый раз что-то идёт не так, что порой приводит к замене испытуемого прямо посреди активной разработки. Я не буду рассказывать про все изученные мной варианты, скажу лишь про опробованные в текущем проекте.
type-check
Маленькая и довольно удобная библиотека. Схема описывается в виде строки. Используя многострочные строки можно описывать довольно сложные структуры:
`{
ID: String,
creator: {
fname: String | Null,
mname: String | Null,
lname: String | Null,
email: [String]
} | Undefined,
sender: Maybe {
name: String,
email: String
},
type: Number,
subject: String,
...
}`
Есть и довольно серьёзные недостатки:
- IDE никак не помогает с набором схемы, что особенно стало напрягать при переходе на typescript.
- Практически бесполезные сообщения об ошибках. Не использовал эту библиотеку уже больше года и возможно что-то изменилось (судя по коду, нет). Сообщения были в стиле "Ожидалась строка, получено null". А теперь представьте, у вас приходит массив штук на 200 объектов в каждом из которых есть поля со строками и лишь в одном объекте одно из полей сломалось. Как найти это поле? Просматривать все 200 элементов? Я так несколько раз помучился и это сильно
сломало мне жизньиспортило впечатление о библиотеке. Обычно я вообще не хочу знать, что там ожидалось и получено, а хочу открыть схему данных и найти нужное в ней поле и тоже самое в самих данных. Другими словами в сообщении об ошибке критически важно иметь keypath до нужного места в данных/схеме, а что там ожидалось и пришло вообще можно не выводить. - Совсем мелочь конечно, но отступы в примере выше при сжатии кода никуда не исчезнут.
Joi
Github
Версия для браузера: joi-browser
Наверное самая известная библиотека на данную тему с кучей возможностей и бесконечным API. Сначала я использовал её на сервере и она отлично себя показала. В какой-то момент я решил заменить ею type-check
на клиенте. На тот момент я почти не контролировал размер бандла, никаких проблем с этим просто не было. Но за год он сильно вырос и на мобильном Интернете первая загрузка приложения стала совсем не комфортной. Было решено организовать ленивую загрузку компонентов. Отчёт webpack-bundle-analyzer показал кучу гигантов в бандле и все они легко отправлялись в создаваемые webpack-ом чанки. Все кроме Joi
. Многие компоненты общаются с сервером и все ответы сервера валидируются, то есть выносить Joi
в какой-то чанк не имеет смысла, он просто будет всегда загружаться сразу после основного. В какой-то момент основной бандл выглядел так: тыц. Конечно же возникло непроходящее желание что-то с этим сделать. Хотелось такую же удобную библиотеку, но намного меньше.
Yup
В ридми обещают примерно тот же Joi
, но по размеру пригодный для фронтэнда. На деле же он всего примерно в два раза меньше, то есть Yup
по прежнему оставался самой большой библиотекой в основном бандле. Кроме того появились дополнительные минусы:
- Библиотека по умолчанию пропускает все
undefined
. Постоянно писать.required()
не особо приятно, да и мне больше нравится когда изначально всё нельзя и где надо разрешается. ВJoi
есть опцияpresence: 'required'
позволяющая настроить это поведение. Я создал запрос с адским номером 666, но пока авторы молчат. - Нет возможности проверить значения объекта допуская все ключи. В
Joi
для этого используется object.pattern с первым аргументом допускающим любые строки. Наверно здесь ещё можно было бы как-то выкрутиться, да и первый минус авторы может поправят, но учитывая размер, ждать или что-то править самому совсем не хотелось.
Ow
Следующий претендент наконец-то оказался действительно маленьким, плюс не заставлял постоянно писать ()
там, где без этого можно обойтись. Например, записать валидатор допускающий строку или undefined
можно так:
let optionalStringValidator = ow.optional.string;
ow(optionalStringValidator, '1'); // Ok
ow(optionalStringValidator, undefined); // Ok
Шикарно! А что с null
? Перевернув всю документацию я нашёл следующий способ:
ow.any(ow.optional.string, ow.null);
О ужас! При попытке переписать часть валидации в проекте я чуть пальцы себе не сломал набирая это. Завёл issue на добавление ow.nullable
, на что был отправлен сюда. Если кратко, там говорят, что null
вообще не нужен. Приводимые там аргументы тоже вполне адекватные учитывая первую строку в их ридми:
Function argument validation for humans
То есть эта библиотека для валидации значений приходящих в качестве аргументов функции. На огромные вложенные структуры приходящие с сервера, видимо, особо не расчитывали.
Дальнейшее изучение и попытки использования выявили ещё несколько особенностей, которые опять же хорошо объяснялись той самой строчкой в ридми, но не очень мне подходили. На самом деле это довольно хорошая библиотека, она просто для немного других целей.
Примерно здесь, я уже совсем устал разочаровываться и решил написать свою библиотеку с блекджеком и девственницами. Да да, я опять к вам с очередным велосипедом :). Знакомьтесь:
OmYumYum
Немного примеров:
import om from 'omyumyum';
const isOptionalNumber = om.number.or.undefined;
isOptionalNumber('1');
// => false
isOptionalNumber(1);
// => true
isOptionalNumber(undefined);
// => true
.or
можно использовать сколько угодно раз бесконечно увеличивая допустимые варианты:
om.number.or.string.or.null.or.undefined;
При этом постоянно генерируется почти обычная функция, принимающая любой аргумент и возврацающая boolean
.
Если нужно чтобы функция в случае неудачи проверки бросала ошибку:
om(om.number, '1');
// бросает TypeError
Или с каррированием:
const isNumberOrThrow = om(om.number);
isNumberOrThrow('1')
// бросает TypeError
Получаемая функция не совсем обычная, так как имеет дополнительные методы. .or
уже показан, часть методов будет зависеть от выбранного типа (см. API), например, строку можно усилить регулярным выражением:
const isNonEmptyString = om.string.pattern(/\S/); // == `om.string.nonEmpty`
isNonEmptyString(' ');
// => false
isNonEmptyString('1');
// => true
А для объекта можно указать его форму:
const isUserData = om.object.shape({
name: om.string,
age: om.number.or.vacuum // `.or.vacuum` == `.or.null.or.undefined`
});
isUserData({});
// => false
isUserData({ age: 20 })
// => false
isUserData({ name: 'Иванушка' });
// => true
isUserData({ name: 'Иванушка', age: null });
// => true
isUserData({ name: 'Иванушка', age: 20 });
// => true
Обещанный keypath до проблемного места:
om(om.array.of(om.object.shape({ name: om.string })), [{ name: 'Иванушка' }, { name: null }]);
// бросает TypeError('Type mismatch at "[1].name"')
Если встроенных возможностей не хватает, всегда можно использовать .custom(validator: (value: any) => boolean)
:
const isEmailOrPhone = om.custom(require('is-email')).or.custom(require('is-phone'));
isEmailOrPhone('test@test.test');
// => true
В наличии так же ожидаемый .and
используемый для объединения и улучшения типов:
const isNonZeroString = om.string.and.custom(str => str.length > 0); // == `om.string.nonZero`
isNonZeroString('');
// => false
isNonZeroString('1');
// => true
.and
имеет преимущество над .or
, но так как .custom()
принимает валидатор точно того же вида, что и создаётся библиотекой, то это можно обойти:
// объект может содержать либо поле `age`, либо поле `birthday`
om.object.shape({ name: om.string }).and.custom(
om.object.shape({ age: om.number })
.or.object.shape({ birthday: om.date })]
);
Можно продолжать улучшать ранее созданные валидаторы. Старые при этом никак не портятся. Попробуем улучшить созданный ранее isUserData
:
const isImprovedUserData = isUserData.and.object.shape({
friends: om.array.of(isUserData).or.vacuum
});
isImprovedUserData({
name: 'Иванушка',
age: 20,
friends: [{ name: 'Алёнушка', age: 18 }]
});
// => true
Ну и остался .not
:
const isNotVacuum = om.not.null.and.not.undefined; // == `om.not.vacuum`
isNotVacuum(1);
// => true
isNotVacuum(null);
// => false
isNotVacuum(undefined);
// => false
Остальные доступные методы можно посмотреть в API библиотеки.
Плюсы библиотеки:
- Лаконичный синтаксис с
.or
,.and
,.not
и минимумом скобок. В сочетании с автодополнением typescript-а набор превращается в одно удовольствие. - Крохотный вес даже по сравнению с
Ow
(почти в 10 раз менеше (minify+gzip)), а в сравнении сJoi
библиотека — как пёрышко рядом с горой. - Няшное название :)
Минусы библиотеки:
- Меньше типов и их модификаторов. И вряд ли их станет сильно больше. Все три приведённых в начале статьи сценария использования (что-то там про стыки и кодовые базы) предполагают передачу обычных текстовых данных, в большинстве случаев это JSON. То есть, на мой взгляд, такая библиотека должна поддерживать типы возможные в JSON, плюс
undefined
и немного часто используемых типов. Тот жеOw
зачем-то напичкан поддержкой всяких типизированных массивов и прочей ерундой. Я думаю это лишнее. - Не умеет преобразовывать данные как
Joi
. Я думаюJoi
тоже довольно плохо с этим справляется. По крайней мере мне его возможностей совсем не хватает и я, при необходимости, делаю преобразования совсем другими инструментами. Возможно это дальнейшее направление развития дляomyumyum
.
Всё! Если понравилась статья ставь лайк, подписывайся на канал и удачи)).