📜 ⬆️ ⬇️

V for validator

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?


Current state of affairs


Here is a list of the most common error reports:


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

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.


Requirements


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:


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

ValidationReport


This is how the ValidationReport appeared - an array consisting of Issue objects. Each Issue is an object containing the path , rule and details fields.



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.


Example


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


Library functions


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, }); } 

Login validator


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


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

Conclusion


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.


Implementation


Today there is a package for nodejs:



')

Source: https://habr.com/ru/post/348530/


All Articles