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