Если вспомнить все ТЗ с описаниями валидации полей — они всегда выглядили примерно так:
- не должно быть короче 6 символов
- не должно превышать 12 символов
- должно включать только латинские символы, цифры и знак подчёркивания
Требования часто приходят набором простых однозначных фраз. А мы, программисты, переводим эти требования в код.
Можно превращать их в одно ультимативное регулярное выражение, вроде
const validateLogin = login => /^[a-zA-z_\d]{6,12}$/.test(login);
Но лучше писать более простые функций которые легче читать и связывать с непосредственным ТЗ:
const charMatch = new RegExp('^[a-zA-Z_0-9]*$');
const validateLogin = login => {
if (login.length < 6) return false;
if (login.length > 12) return false;
if (!charMatch.test(login)) return false;
return true;
};
А что если ещё сильнее упростить этот код до чего-то вроде:
const validateLogin = login =>
validate(login)
.notLessThan(6)
.notLongerThan(12)
.hasOnly(['a-z','A-Z','0-9','_']);
Основная идея заключается в том, что требования к валидации зачастую можно разложить на список независимых типичных утверждений. А эти утвержджени (asserts) можно собрать в коллекцию и переиспользовать.
Именно этим и занимается библиотека validate.it.js. Ядро которой не превышает и ста строк и делает не так уж много:
- позволяет вызывать assert'ы цепочно
- собирает и обрабатывает результаты
Вышеописанное значит, что выполнив код вроде этого:
validate('Pa$$w0rd')
.hasLettersLatin()
.hasNumbers()
.has("!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+");
Вы получите вот такой результат.
{
ok: true,
base: 'Pa$$w0rd',
asserts: ['hasLettersLatin', 'hasNumbers', 'has'],
errors: []
}
Думаю это и так очевидно, но немного распишу
ok
bool — статус валидации
true
— валидация прошла успешноfalse
— валидация провалена
base
string — валидируемая строкаasserts
array — список имён вызванных assert'ов в порядке очерёдности их вызоваerrors
array — массив отчётов в формате Validation Report всех провалившихся assert'ов
Вот пример провалившейся валидации
validate('bob')
.hasLettersLatin()
.hasNumbers();
// -->
{
ok: false,
base: 'bob',
asserts: ['hasLettersLatin', 'hasNumbers'],
errors: [
{
path: [],
rule: 'hasNumbers',
details: {
string: 'bob',
subStrings: ["1","2","3","4","5","6","7","8","9","0"],
found: false,
message: '"bob" has no numbers'
}
}
]
}
Эта простая идея позволяет писать код валидации максимально приближённым к требованиям. При этом сами assert'ы получаются очень простыми и являются чистыми функциями. Их легко поддерживать, тестировать, и добавлять новые.
Кстати да, если вам не хватает какого-то assert'а — вы легко можете на-лету добавить его через @static .extend
или использовать assert .eval
.
Но не жадничайте своих assert'ов сообществу. Станьте контрибьютером!
Контрибьютерам
А теперь сюрприз. Самих assert'ов в библиотеке ещё практически и нет. Только парочка базовых вроде .has
, .match
, .eval
и ещё немного для примеров. Нету даже тех, которые я использовал в листингах этого поста. Всё дело в том, что мне хотелось привлечь внимание к идее, а не к реализации.
Более того, я считаю, что моё видение необходимых assert'ов может сильно отличаться от видения сообщества. И, делая этот инструмент "под себя" я могу сделать его неудобным для других JS разработчиков. И у меня возникла идея — привлечь к созданию этого инструмента JS сообщество. Что бы самому ничего не делать он покрывал потребности сообщества, а не мои собственные.
Я приглашаю JS разработчиков стать контрибьютерами validate.it.js.
Всех кто хочет контрибьютить в опенсорс, но не знает с какого проекта начать.
Всех кто хочет сделать для себя и для всего сообщества удобный инструмент валидации.
Вместе мы сможем наполнить коллекцию реально необходимыми всем нам assert'ами.
При этом контрибьютером может быть разработчик любого уровня. Ведь кто-то захочет заняться валидацией по длине строки, а кому-то, например, интереснее реализовывать проверку на соответствие даты всем вариантам стандарта ISO 8601.
К pull-request'ам предъявляются весьма мягкие требования:
- наличие тестов
- наличие описания — что бы вставить его в раздел с перечнем assert'ов.
Validation Report
Кстати о вышеупомянутых VR которыми наполняется массив .errors
в случае провалившихся assert'ов. Я искал какой-то стандарт для представления ошибок валидации. И по своему опыту знал, что банального true
/false
и даже null
/'error message'
недостаточно. И судя по всему, такого стандарта на сегодняшний день нету.
Зато есть замечательная идея хабраюзера rumkin
"Универсальный интерфейс отчётов валидации" — @rumkin / Validation Report. git
Это простой DTO, который позволяет передать максимально подробную информацию о причинах провала валидации. Состоит из:
path
array — местоположение объекта проверки. В контексте валидации строк — это всегда пустой массив[]
.rule
string — имя assert'а или правило по которому проводилась валидация.details
object — детали или описание в "машино-понятном виде" причины провала валидации. У этого параметра нету чётко-стандартизированный структуры, кроме одного поля:
details.message
string — описание в "человеко-понятном виде". Это не стандартное для VR поле, но в рамках данного проекта оно обязательно.
Пример:
{
path: [],
rule: 'notLongerThen',
details: {
string: 'markopolo',
length: 9,
max: 6,
message: '"markopolo" longer then 6 characters'
}
}
rumkin, кстати, написал небольшой тул для генерации Validation Report'ов. но ради 0Dependecy я не использую конкретно эту реализацию. Благо генерация VR достаточно простая задача.
Будет здорово, если VR однажды станут стандартом, хотя бы локальным.
P.S.
Не стесняйтесь задавать вопросы — буду только рад расширить contributer's guide или ридми проекта.
Комментарии (14)
FilimoniC
15.03.2018 19:09Проверка "3 условия из 4х" реализована как-нибудь?
titulusdesiderio Автор
15.03.2018 20:35нет. неожиданный кейс… но впринципе это легко реализуемо уже на клиенте проверкой кол-ва ошибок
validate(string) .rule1() .rule2() .rule3() .rule4() .errors.length < 2;
TheShock
16.03.2018 03:53Если честно, API мне кажется не очень удачным. Сложно реюзать. Я б его инвертировал:
passwordRules = new Validator() .hasLettersLatin() .hasNumbers(); passwordRules.validate(password1); passwordRules.validate(password2);
RubaXa
16.03.2018 08:08import {composeRules, minLength, maxLength, has} from '...'; const passwordRule = composeRules( minLength(6), maxLength(32), has(/[0-9]/, 'numbers'), has(/^[a-z]+$/i, 'only-latin'), ); const result = passwordRule(inputPass.value);
titulusdesiderio Автор
16.03.2018 09:54а чем плох:
const validatePassword = pass => validate(pass) .hasLettersLatin() .hasNumbers(); validatePassword(password1); validatePassword(password2);
RubaXa
16.03.2018 11:46если мы говорим про валидацию, которая выполняется на каждый чих пользователя, то ответ: ВСЕМ.
titulusdesiderio Автор
16.03.2018 12:26можно как-то более развёрнуто?
RubaXa
16.03.2018 14:56Плохо тем, что на каждый чих вместо уже подготовленной (возможно оптимальной под конкретный паттерн) функции (которую барузер успешно заJITит), будет создаваться новый объект, методы которого вдобавок завраплены, т.е. как не крути, это всё оверхед.
Ок, допустим он небольшой, но всё равно остаётся проблема повторного использования, да, можно завернуть это в метод, как вы написали, но это нужно догадаться, без шуток.
Кроме этого, если оформить в функциональном стиле, мы получим достаточно гибкую штуку и комбинации правил, типа
import {validate, compose, minLength, regExp} import '...'; const emailRule = compose('email', [ minLength(6), regExp(/^.+@.+\.[a-z]+$/), ]); const onlyGmail = compose('only-gmail', [ emailRule(), regExp(/@gmail.com$/), ]); validate(onlyGmail, input.value);
titulusdesiderio Автор
16.03.2018 15:48Думаю этот спор всё-равно ни к чему не приведёт, но я не вижу тут оверхеда...
В чём конкретно проблема того что ассерты завраплены? С клиентской стороны этого не видно, а для разработки это проще. Не приходится тащить кусок ядра в каждый ассерт в отличае от вашей идеи.
Объект создаётся только потому что с ним работать удобнее. В нём не лежит ни одной функции, и он легко сериализуется в JSON. Все ассерты находятся в прототипе который JS движок не будет пересоздавать на каждый чих.
Да, функциональщина это круто. Но тут получается функциональщина ради функциональщины.
Кроме этого, если оформить в функциональном стиле, мы получим ...
… усложнение кода библиотеки, повышение порога вход, усложнение кода клиента и стоимости его поддержки, без выйгрыша в чём либо.
Теоретическиcompose
вместе с хитрымcreateComplexRule
могут дать какой-то прирост в производительности по сравнению сrunAssertAsMethod
. Но это уж точно выглядит оверхедом и преждевременной оптимизацией
RubaXa
16.03.2018 15:08Хотя думаю погорячился, так что всё это ИМХО, просто у себя я именно так и сделал, вот:
Правилаexport function minLength(min: number): ValidateRule { return ({value}) => value.length >= min ? null : { id: 'minLength', detail: { min, }, }; } export function required(): ValidateRule { return createComplexRule('required', {}, [minLength(1)]); } export function email(): ValidateRule { return createComplexRule('email', {}, [regexp(/^.+@.+\..+$/)]); } export function password(additionalRules: ValidateRule[] = []): ValidateRule { return createComplexRule('password', {}, [ required(), minLength(6), ].concat(additionalRules)); }
mmMike
По моему, единственная причина использование "min|max число, максимальная длинна… и пр. — это случай, когда эти условия задаются не в коде, а в настройках. Да еще настройка рассчитана на людей, который понятия не имеют о regexp.
В коде, лично мне, проще регулярные выражения писать, чем такую лапшу из проверок.
Oborotenby
Регэкспы могут быть абсолютно нечитабельны.
mmMike
Могут быть… На первый взгляд.
Но когда с ними часто работаешь, то наступает просветление.
Очень формальный и простой язык описания. Нужно только научится мыслить как парсер и просветление наступит.
Но для тех кто код будет смотреть ВСЕГДА оставляю комментарий что данный regexp делает.
BerkutEagle
Ничто не мешает брать список валидаторов и параметры для них хоть из конфигов, хоть из базы. Ошибки будут накапливаться.