Ом-ням-ням и валидация данных
- суббота, 26 октября 2019 г. в 00:42:08
Всем привет! Поговорим немного про валидацию данных. Что в этом сложного и зачем вообще это может понадобиться, скажем, в проекте написанном на typescript? Typescript довольно неплохо всё контролирует, остаётся максимум проверять ввод пользователя. То есть закинуть в проект десяток регулярок и всё, казалось бы тему можно закрывать, но… Далеко не всегда, а в случае с вебом примерно никогда, весь проект находится в единой кодовой базе и использует одни и теже типы. На стыке таких кодовых баз как раз и возникают ситуации, когда ожидание не соответствует реальности и тут typescript уже не помощник. Несколько примеров:
null. Описывая данные на клиенте, программисты как бы определяют с чем умеет работать клиент и если что-то идёт не так, то намного приятнее сразу видеть сообщение в консоли о первоисточнике проблемы, а не расковыривать непонятный баг уже там, где это вылезло во view-слое (и хорошо ещё если это сразу будет замечено). Также сейчас уже есть решения (1, 2) позволяющие переносить типы с сервера на клиент. Я пока не пробовал так делать, но, вполне возможно, за этим будущее.Я думаю примеры вполне убедительны и теперь уже нет ощущения, что можно обойтись простыми регулярками, ведь речь не просто про пользовательский ввод, а про валидацию сложных, обычно вложенных на несколько уровней данных. Здесь уже нужна специальная библиотека. И такие конечно же есть! Так уж получается, что за последние лет 10, каждый раз начиная новый проект, я пытаюсь заиспользовать в нём очередную такую библиотеку, подстроив её под свои нужды. И каждый раз что-то идёт не так, что порой приводит к замене испытуемого прямо посреди активной разработки. Я не буду рассказывать про все изученные мной варианты, скажу лишь про опробованные в текущем проекте.
Маленькая и довольно удобная библиотека. Схема описывается в виде строки. Используя многострочные строки можно описывать довольно сложные структуры:
`{
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,
...
}`Есть и довольно серьёзные недостатки:
Github
Версия для браузера: joi-browser
Наверное самая известная библиотека на данную тему с кучей возможностей и бесконечным API. Сначала я использовал её на сервере и она отлично себя показала. В какой-то момент я решил заменить ею type-check на клиенте. На тот момент я почти не контролировал размер бандла, никаких проблем с этим просто не было. Но за год он сильно вырос и на мобильном Интернете первая загрузка приложения стала совсем не комфортной. Было решено организовать ленивую загрузку компонентов. Отчёт webpack-bundle-analyzer показал кучу гигантов в бандле и все они легко отправлялись в создаваемые webpack-ом чанки. Все кроме Joi. Многие компоненты общаются с сервером и все ответы сервера валидируются, то есть выносить Joi в какой-то чанк не имеет смысла, он просто будет всегда загружаться сразу после основного. В какой-то момент основной бандл выглядел так: тыц. Конечно же возникло непроходящее желание что-то с этим сделать. Хотелось такую же удобную библиотеку, но намного меньше.
В ридми обещают примерно тот же Joi, но по размеру пригодный для фронтэнда. На деле же он всего примерно в два раза меньше, то есть Yup по прежнему оставался самой большой библиотекой в основном бандле. Кроме того появились дополнительные минусы:
undefined. Постоянно писать .required() не особо приятно, да и мне больше нравится когда изначально всё нельзя и где надо разрешается. В Joi есть опция presence: 'required' позволяющая настроить это поведение. Я создал запрос с адским номером 666, но пока авторы молчат.Joi для этого используется object.pattern с первым аргументом допускающим любые строки. Наверно здесь ещё можно было бы как-то выкрутиться, да и первый минус авторы может поправят, но учитывая размер, ждать или что-то править самому совсем не хотелось.Следующий претендент наконец-то оказался действительно маленьким, плюс не заставлял постоянно писать () там, где без этого можно обойтись. Например, записать валидатор допускающий строку или 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
То есть эта библиотека для валидации значений приходящих в качестве аргументов функции. На огромные вложенные структуры приходящие с сервера, видимо, особо не расчитывали.
Дальнейшее изучение и попытки использования выявили ещё несколько особенностей, которые опять же хорошо объяснялись той самой строчкой в ридми, но не очень мне подходили. На самом деле это довольно хорошая библиотека, она просто для немного других целей.
Примерно здесь, я уже совсем устал разочаровываться и решил написать свою библиотеку с блекджеком и девственницами. Да да, я опять к вам с очередным велосипедом :). Знакомьтесь:
Немного примеров:
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() принимает валидатор точно того же вида, что и создаётся библиотекой, то это можно обойти:
// я не придумал здесь нормальный пример, но что-то вроде этого:
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 библиотека — как пёрышко рядом с горой.undefined и немного часто используемых типов. Тот же Ow зачем-то напичкан поддержкой всяких типизированных массивов и прочей ерундой. Я думаю это лишнее.Joi. Я думаю Joi тоже довольно плохо с этим справляется. По крайней мере мне его возможностей совсем не хватает и я, при необходимости, делаю преобразования совсем другими инструментами. Возможно это дальнейшее направление развития для omyumyum.Всё! Если понравилась статья ставь лайк, подписывайся на канал и удачи)).