Когда перед нами стоит выбор инструмента для валидации пользовательских данных, то речь чаще идет о интерфейсе задания правил. Сегодня таких инструментов превеликое множество от декларативных до объектных. Каждый валидатор пытается быть выразительным и простым в использовании. Но я хочу обратить ваше внимание на результат работы валидатора – отчеты. Каждый разработчик норовит сделать свое решение и если для интерфейсов от разнообразия лишь польза, то для получаемого результата наоборот. В общем, давайте взглянем на проблему.
Осторожно! После прочтения статьи вы, возможно, захотите выкинуть ваш любимый валидатор.
Сегодня средства валидации многообразны, но бедны в возможностях. Вы часто можете встретить сообщение об ошибке в виде: логин должен содержать цифры или буквы
. Это классический пример плохого дизайна отчета об ошибках. Возьмем сообщение компилятора go, встретившего некорректный символ:
test.go:16:1: invalid character U+0023 '#'
Компилятор указывает место возникновения и причину ошибки. А теперь представьте, что компилятор заменит его на сообщение:
test.go: file should contain valid code
Как вам такое!? Почему мы ждем от инструмента подробного отчета, а пользователю возвращаем клочок информации. Чем исходный код отличается от значения логина "в глазах" программы?
Текущее положение дел
Вот список самых распространненных отчетов об ошибках:
- Валидатор возвращает строку, массив/объект строк.
- Валидатор возвращает
true/false
(npm validator). - Валидатор выбрасывает исключение.
- Валидатор выводит отчет в консоль (npm prop-types).
Такие данные непригодны для дальнейшего использования, например для интернационализации или интерпретации, а значит и бесполезны. Как следствие, библиотеки не взаимозаменяемы, а компоненты системы завязываются на уникальное представление. Чтобы передать отчет на клиент, приходится писать собственные обертки.
Давайте попробуем это исправить и сформируем общие требования к представлению отчета.
Требования
Забегая вперед скажу, что этот вариант уже несколько лет успешно работает в продакшене.
Вот требования к отчету на которые опирался я:
- Удобная программная обработка: значения вместо сообщений.
- Представление объектов любой структуры: храним полные пути.
- Удобная интернационализация: использование ID для правил.
- Сохранение читаемости: использование человекопонятных кодов.
- Переносимость: отчет не привязан к среде исполнения или конкретному языку.
ValidationReport
Так появился ValidationReport – массив состоящий из объектов Issue. Каждый Issue – это объект, содержащий поля path
, rule
и details
.
path
– массив строк или чисел. Путь поля внутри объекта. Может быть пустым, в случае, если валидируемое значение – примитив.rule
– строка. Код ошибки.details
– объект. Объект произвольного вида, содержащий данные о причине ошибки.
JavaScript:
[
{
path: ['login'],
rule: 'syntax',
details: {
pos: 1,
expect: ['LETTER', 'NUMBER'],
is: '$',
},
},
]
Go:
type Details map[string]interface{};
type Issue struct {
Path []string `json:"path"`
Rule string `json:"rule"`
Details Details `json:"details"`
}
type Report []Issue;
//...
issue:= Issue{
Path: []string{"login"},
Rule: "syntax",
Details: Details{
"pos": 1,
"expect": []string{"LETTER", "NUMBER"},
"is": "$",
},
}
report := Report{issue}
Такой отчет легко конвертируется в любое другое представление, он подробен и нагляден. Теперь вместо логин должен содержать цифры или буквы
становится возможным вывести: Логин содержит недопустимый символ '$': позиция 1
. При валидации вложенных структур, легко управлять путями.
Специфические коды ошибок могут быть представлены в виде URI.
Пример
В виде примера реализуем некоторые библиотечные функции, валидатор для логина и имплементацию на JavaScript в функциональном стиле. Готовый код на jsbin.
Библиотечные функции
Здесь будут реализованы два метода для создания Issue (createIssue) и для добавления префикса к значению Issue.path (pathRefixer):
function createIssue(path, rule, details = {}) {
return {path, rule, details};
}
function pathPrefixer(...prefix) {
return ({path, rule, details}) => ({
path: [...prefix, ...path],
rule,
details,
});
}
Валидатор логина
Собственно тот самый валидатор логина.
const LETTER = /[a-z]/;
const NUMBER = /[0-9]/;
function validCharsLength(login) {
let i;
for (i = 0; i < login.length; i++) {
const char = login[i];
if (i === 0) {
if (! LETTER.test(char)) {
break;
}
}
else {
if (! LETTER.test(char) && ! NUMBER.test(char)) {
break;
}
}
}
return i;
}
function validateLogin(login) {
const validLength = validCharsLength(login);
if (validLength < login.length) {
return [
createIssue([], 'syntax', {
pos: validLength,
expect: validLength > 0 ? ['NUMBER', 'LETTER'] : ['LETTER'],
is: login.slice(validLength, validLength + 1),
}),
];
}
else {
return [];
}
}
function stringifySyntaxIssue({details}) {
return `Invalid character "${details.is}" at postion ${details.pos}.`;
}
Имплементация
Реализация на уровне приложения. Добавляем функцию проверки модели и абстрактного запроса использующего модель:
function validateUser(user) {
return validateSyntax(user.login)
.map(pathPrefixer('login'));
}
function validateUsersRequest(request) {
return request.users
.reduce((reports, user, i) => {
const report = validateUser(user)
.map(pathPrefixer('users', i));
return [...reports, ...report];
}, []);
}
const usersRequest = {
users: [
{login: 'adm!n'},
{login: 'u$er'},
{login: '&@%#'},
],
};
function stringifyLoginSyntaxIssue(issue) {
return `User #${issue.path[1]} login: ${stringifySyntaxIssue(issue)}`;
}
const report = validateUsersRequest(usersRequest);
const loginSyntaxIssues = report.filter(
({path, rule}) => path[2] === 'login' && rule === 'syntax'
);
console.log(report);
console.log(loginSyntaxIssues.map(stringifyLoginSyntaxIssue).join('\n'));
Заключение
Использование ValidationReport позволит комбинировать различные библиотеки для валидации и управлять процессом на свое усмотрение: например выполнить трудоемкие проверки параллельно, а затем конкатенировать результат. Отчеты от разных программ представляются однотипно и позволяют переиспользовать код их обработчиков.
Реализация
На сегодняшний день существует пакет для nodejs:
- npm-пакет validation-report.
titulusdesiderio
Простая валидация банально проще. Простите за тавтологию.
Ваш
ValidationReport
клёвый, но не бесплатный. На его формирование нужно потратить много больше человекочасов чем на функцию с парой штукif (!someRule) return false
.Кстати, никто не знает, случаем, валидатора с подобным цепочным синтаксисом:
С удовольствием использовал бы нечто подобное, особенно если бы оно умело генерировать
ValidationReport
rumkin Автор
Это как сравнивать голый http.createServer и express. Использование if сохранит вам пару часов в простых случаях, но на реализации комплексных условий вы потратите больше времени.
TypedProps как раз реализует подобный интерфейс и генерирует ValidationReport.
titulusdesiderio
ValidationReport
никак не упрощает валидацию по комплексным условиям.rumkin Автор
Задача VR не упрощать валидацию как таковую, это задача валидатора. VR упрощает создание отчетов, делая различные инструменты совместимыми между собой и как следствие взаимозаменяемыми.
valichek
это плагин к фреймворку, но именно об этом aurelia.io/docs/plugins/validation
KwI
Вот валидатор с подобным синтаксисом