When we are faced with the choice of tools for the validation of user data, then we are more often talking about the interface for setting rules. Today, there are a great number of such tools from declarative to objective. 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 for interfaces there is only benefit from diversity, then the result is the opposite. In general, let's take a look at the problem.
Caution! After reading the article, you might want to throw away your favorite validator.
Today, validation tools are diverse, but poor in capabilities. You can often see an error message in the form: . This is a classic example of a bad error report design. Take the go compiler message that encountered 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 will replace it with a message:
test.go: file should contain valid code
How do you like that !? Why we expect a detailed report from the tool and return a piece of information to the user. How does the source code differ from the value of the login "in the eyes" of the program?
Here is a list of the most common error reports:
true/false
(npm validator).Such data are 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 representation. To send a report to the client, you have to write your own wrappers.
Let's try to fix this and formulate the general requirements for the presentation of the report.
Looking ahead, I’ll say that this option has been successfully in production for several years.
Here are the requirements for the report on which I relied:
This is how the ValidationReport appeared - an array consisting of Issue objects. Each Issue is an object containing the path
, rule
and details
fields.
path
is an array of strings or numbers. The field path inside the object. May be empty if the value being validated is a primitive.rule
is a string. Error code.details
- object. An arbitrary object containing data 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 into any other presentation, it is detailed and clear. Now instead of a it becomes possible to display:
'$': 1
. When validating nested structures, it is easy to manage paths.
Specific error codes can be represented as URIs.
As an example, we implement some library functions, a validator for login and JavaScript implementation in a functional style. Ready code on jsbin .
Here two methods will be implemented for creating Issue (createIssue) and for adding a prefix to the Issue.path (pathRefixer) value:
function createIssue(path, rule, details = {}) { return {path, rule, details}; } function pathPrefixer(...prefix) { return ({path, rule, details}) => ({ path: [...prefix, ...path], rule, details, }); }
Actually the same validator login.
const LETTER = /[az]/; 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 at the application level. Add the function of checking the model and the 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'));
Using ValidationReport will allow you to combine different libraries for validation and manage the process at your discretion: for example, perform time-consuming checks in parallel, and then concatenate the result. Reports from different programs are presented in the same type and allow you to reuse the code of their handlers.
Today there is a package for nodejs:
Source: https://habr.com/ru/post/348530/
All Articles