Back to Home

V for validator

validators · validation · bug reports · validator report

V for validator

    When we are faced with the choice of a tool for validating user data, we are more often talking about the interface for setting rules. Today, there are a great many such instruments from declarative to object. Each validator tries to be expressive and easy to use. But I want to draw your attention to the result of the validator - reports. Each developer strives to make his own decision, and if diversity is only useful for interfaces, then the opposite is true for the result. In general, let's take a look at the problem.


    Caution! After reading the article, you might want to throw out your favorite validator.

    Today, validation tools are diverse, but poor in capabilities. You can often find the error message in the form: логин должен содержать цифры или буквы. This is a classic example of a poor design for a bug report. Take the message from the go compiler that encounters an invalid character:


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

    The compiler indicates the location and cause of the error. Now imagine that the compiler replaces it with a message:


    test.go: file should contain valid code

    How do you like that !? Why do we expect a detailed report from the tool, and return a piece of information to the user. How is the source code different from the login value "in the eyes" of the program?


    Current state of affairs


    Here is a list of the most common error reports:


    1. The validator returns a string, an array / string object.
    2. The validator returns true/false(npm validator).
    3. The validator throws an exception.
    4. The validator displays the report in the console (npm prop-types).

    Such data is unsuitable for further use, for example, for internationalization or interpretation, and therefore useless. As a result, libraries are not interchangeable, and system components are tied to a unique view. To send a report to a client, you have to write your own wrappers.


    Let's try to fix this and formulate general reporting requirements.


    Requirements


    Looking ahead, I’ll say that this option has been successfully working in production for several years.

    Here are the report requirements that I relied on:


    1. Convenient software processing: values ​​instead of messages.
    2. Representation of objects of any structure: store full paths.
    3. Convenient internationalization: using IDs for rules.
    4. Maintaining readability: using human-readable codes.
    5. Portability: the report is not tied to a runtime or a specific language.

    ValidationReport


    So ValidationReport appeared - an array consisting of Issue objects. Each Issue is an object containing fields path, ruleand details.


    • path- an array of strings or numbers. The path of the field inside the object. It can be empty if the validated value is primitive.
    • rule- line. Error code.
    • details- an object. An arbitrary view object containing information about the cause of the error.

    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}

    Such a report is easily converted to any other representation, it is detailed and visual. Now, instead логин должен содержать цифры или буквы, it is possible to deduce Логин содержит недопустимый символ '$': позиция 1. When validating nested structures, it’s easy to manage paths.


    Specific error codes can be represented as URIs.


    Example


    As an example, we implement some library functions, a validator for login, and implementation in JavaScript in a functional style. Ready code on jsbin .


    Library functions


    Two methods will be implemented here for creating an Issue (createIssue) and for adding a prefix to the value of Issue.path (pathRefixer):


    function createIssue(path, rule, details = {}) {
        return {path, rule, details};
    }
    function pathPrefixer(...prefix) {
        return ({path, rule, details}) => ({
            path: [...prefix, ...path],
            rule,
            details,
        });
    }

    Login validator


    Actually the same login validator.


    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}.`;
    }

    Implementation


    Application level implementation. Add the function of checking the model and abstract query using the model:


    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'));

    Conclusion


    Using ValidationReport will allow you to combine different libraries for validation and manage the process at your discretion: for example, perform laborious checks in parallel, and then concatenate the result. Reports from different programs are presented the same way and allow you to reuse the code of their handlers.


    Implementation


    Today there is a package for nodejs:


    Read Next