V for Validator
Когда перед нами стоит выбор инструмента для валидации пользовательских данных, то речь чаще идет о интерфейсе задания правил. Сегодня таких инструментов превеликое множество от декларативных до объектных. Каждый валидатор пытается быть выразительным и простым в использовании. Но я хочу обратить ваше внимание на результат работы валидатора — отчеты. Каждый разработчик норовит сделать свое решение и если для интерфейсов от разнообразия лишь польза, то для получаемого результата наоборот. В общем, давайте взглянем на проблему.
Осторожно! После прочтения статьи вы, возможно, захотите выкинуть ваш любимый валидатор.
Сегодня средства валидации многообразны, но бедны в возможностях. Вы часто можете встретить сообщение об ошибке в виде: логин должен содержать цифры или буквы
. Это классический пример плохого дизайна отчета об ошибках. Возьмем сообщение компилятора 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.