V for Validator

    Когда перед нами стоит выбор инструмента для валидации пользовательских данных, то речь чаще идет о интерфейсе задания правил. Сегодня таких инструментов превеликое множество от декларативных до объектных. Каждый валидатор пытается быть выразительным и простым в использовании. Но я хочу обратить ваше внимание на результат работы валидатора – отчеты. Каждый разработчик норовит сделать свое решение и если для интерфейсов от разнообразия лишь польза, то для получаемого результата наоборот. В общем, давайте взглянем на проблему.


    Осторожно! После прочтения статьи вы, возможно, захотите выкинуть ваш любимый валидатор.

    Сегодня средства валидации многообразны, но бедны в возможностях. Вы часто можете встретить сообщение об ошибке в виде: логин должен содержать цифры или буквы. Это классический пример плохого дизайна отчета об ошибках. Возьмем сообщение компилятора go, встретившего некорректный символ:


    test.go:16:1: invalid character U+0023 '#'

    Компилятор указывает место возникновения и причину ошибки. А теперь представьте, что компилятор заменит его на сообщение:


    test.go: file should contain valid code

    Как вам такое!? Почему мы ждем от инструмента подробного отчета, а пользователю возвращаем клочок информации. Чем исходный код отличается от значения логина "в глазах" программы?


    Текущее положение дел


    Вот список самых распространненных отчетов об ошибках:


    1. Валидатор возвращает строку, массив/объект строк.
    2. Валидатор возвращает true/false (npm validator).
    3. Валидатор выбрасывает исключение.
    4. Валидатор выводит отчет в консоль (npm prop-types).

    Такие данные непригодны для дальнейшего использования, например для интернационализации или интерпретации, а значит и бесполезны. Как следствие, библиотеки не взаимозаменяемы, а компоненты системы завязываются на уникальное представление. Чтобы передать отчет на клиент, приходится писать собственные обертки.


    Давайте попробуем это исправить и сформируем общие требования к представлению отчета.


    Требования


    Забегая вперед скажу, что этот вариант уже несколько лет успешно работает в продакшене.

    Вот требования к отчету на которые опирался я:


    1. Удобная программная обработка: значения вместо сообщений.
    2. Представление объектов любой структуры: храним полные пути.
    3. Удобная интернационализация: использование ID для правил.
    4. Сохранение читаемости: использование человекопонятных кодов.
    5. Переносимость: отчет не привязан к среде исполнения или конкретному языку.

    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:


    • +16
    • 4.1k
    • 6
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      0

      Простая валидация банально проще. Простите за тавтологию.
      Ваш ValidationReport клёвый, но не бесплатный. На его формирование нужно потратить много больше человекочасов чем на функцию с парой штук if (!someRule) return false.


      Кстати, никто не знает, случаем, валидатора с подобным цепочным синтаксисом:


      superValidator9000( string )
        .hasNumbers()
        .hasLetters()
        .hasUpperCase()
        .longerThan(5)

      С удовольствием использовал бы нечто подобное, особенно если бы оно умело генерировать ValidationReport

        0
        На его формирование нужно потратить много больше человекочасов чем на функцию с парой штук if (!someRule) return false.

        Это как сравнивать голый http.createServer и express. Использование if сохранит вам пару часов в простых случаях, но на реализации комплексных условий вы потратите больше времени.


        Кстати, никто не знает, случаем, валидатора с подобным цепочным синтаксисом:

        TypedProps как раз реализует подобный интерфейс и генерирует ValidationReport.

          0
          Это как сравнивать голый http.createServer и express. Использование if сохранит вам пару часов в простых случаях, но на реализации комплексных условий вы потратите больше времени.

          ValidationReport никак не упрощает валидацию по комплексным условиям.

            0

            Задача VR не упрощать валидацию как таковую, это задача валидатора. VR упрощает создание отчетов, делая различные инструменты совместимыми между собой и как следствие взаимозаменяемыми.

          0
          это плагин к фреймворку, но именно об этом aurelia.io/docs/plugins/validation

          Only users with full accounts can post comments. Log in, please.