Having met in numerous Javascript development sites with situations where it was necessary to validate the values, it became clear that it was necessary to somehow resolve this issue. For this purpose, the following task was set:
Develop a library that will enable:
The basis of which will be:
To achieve these goals, the quartet validation library was developed.
Most of the systems that are calculated to be applicable in a wide range of tasks are based on the simplest elements : actions, data, and algorithms. As well as the methods of their composition - with the goal of the simplest elements to collect something more difficult to solve more complex problems.
The quartet library is based on the notion of a validator . Validators in this library are the following functions
function validator( value: any, { key: string|int, parent: any }, { key: string|int, parent: any }, ... ): boolean
There are several things in this definition that are worth describing in more detail:
function(...): boolean
- says that the validator - calculates the result of validation, and the result of validation is a boolean value - true or false , respectively, valid or not valid
value: any
- says that the validator - calculates the result of validating the value , which can be any javascript value. The validator either refers this validated value to valid or non-valid.
{ key: string|int, parent: any }, ...
- indicates that the validated value may be in different contexts depending on what level of nesting the value is located at. Let's show it in examples.
Example value without any context
const value = 4; // . // : const isValueValid = validator(4)
Example value in array context
// 0 1 2 3 4 const arr = [1, 2, 3, value, 5] // (k): 3 // : [1, 2, 3, value, 5] // value - const isValueValid = validator(4, { key: 3, parent: [1,2,3,4,5] })
Sample Value in Object Context
const obj = { a: 1, b: 2, c: value, d: 8 } // 'c' // : { a: 1, b: 2, c: 4, d: 8 } // value - // : const isValueValid = validator(4, { key: 'c', parent: { a: 1, b: 2, c: 4, d: 8 } })
Since the structures in the object can have more nesting, it makes sense to talk about a variety of contexts
const arrOfObj = [{ a: 1, b: 2, c: value, d: 8 }, // ... ] // c 'c' // : { a: 1, b: 2, c: 4, d: 8 } // arrOfObj, // 0. // value - const isValueValid = validator( 4, { key: 'c', parent: { a: 1, b: 2, c: 4, d: 8 } } { key: 0, parent: [{ a: 1, b: 2, c: 4, d: 8 }] } )
And so on.
This definition of a validator should remind you of the definition of functions that are passed as an argument to the methods of arrays, such as: map, filter, some, every and so on.
In this case, the validator is a more generalized function — it accepts not only the index of the element in the array and the array, but also the index of the array — in its parent and its parent, and so on.
The bricks described above do not stand out in the midst of other “solution-stones” , which are lying on the javascript crutch “beach” . So let's build something more slender and interesting out of them. For this we have a composition .
Agree, it would be convenient to validate objects in such a way that the description of the validation itself coincides with the description of the object. For this we will use the object composition validators . It looks like this:
// quartet const quartet = require('quartet') // (v - validator) const v = quartet() // , // const objectSchema = { a: a => typeof a ==='string', // 'string' b: b => typeof b === 'number', // 'number' // ... } const compositeObjValidator = v(objectSchema) const obj = { a: 'some text', b: 2 } const isObjValid = compositeObjValidator(obj) console.log(isObjValid) // => true
As you can see, from different validator bricks defined for specific fields, we can assemble an object validator - some "small building", in which it is still quite tight - but better than without it. For this we use validator composer v
. Whenever it encounters an object literal v
at the place of a validator, it will consider it as an object composition, turning it into a validator of an object in its fields.
Sometimes we cannot describe all the fields . For example, when an object is a data dictionary:
const quartet = require('quartet') const v = quartet() const isStringValidator = name => typeof name === 'string' const keyValueValidator = (value, { key }) => value.length === 1 && key.length === 1 const dictionarySchema= { dictionaryName: isStringValidator, ...v.rest(keyValueValidator) } const compositeObjValidator = v(dictionarySchema) const obj = { dictionaryName: 'next letter', b: 'c', c: 'd' } const isObjValid = compositeObjValidator(obj) console.log(isObjValid) // => true const obj2 = { dictionaryName: 'next letter', b: 'a', a: 'invalid value', notValidKey: 'a' } const isObj2Valid = compositeObjValidator(obj2) console.log(isObj2Valid) // => false
As we saw above, there is a need to reuse simple validators. In these examples, we already had to use the "string type validator" two times already.
In order to shorten the record and increase its readability in the quartet library, string synonyms of validators are used. Whenever the composer of validators encounters a line at the place where the validator should be, he searches the dictionary for the corresponding validator and uses it .
By default, the most common validators are already defined in the library.
Consider examples:
v('number')(1) // => true v('number')('1') // => false v('string')('1') // => true v('string')(null) // => false v('null')(null) // => true v('object')(null) // => true v('object!')(null) // => false // ...
and many others described in the documentation .
The validator composer (function v
) is also a factory of validators. In the sense that it contains many useful methods that return
For example, let's look at the validation of an array: most often it consists of checking the type of the array and checking all its elements. Use the v.arrayOf(elementValidator)
method for this. For example, take an array of points with names.
const a = [ {x: 1, y: 1, name: 'A'}, {x: 2, y: 1, name: 'B'}, {x: -1, y: 2, name: 'C'}, {x: 1, y: 3, name: 'D'}, ]
Since an array of points is an array of objects, it makes sense to use object composition to validate the elements of the array.
const namedPointSchema = { x: 'number', // number - y: 'number', name: 'string' // string - }
Now, using the factory method v.arrayOf
, create a validator of the entire array.
const isArrayValid = v.arrayOf({ x: 'number', y: 'number', name: 'string' })
Let's see how this validator works:
isArrayValid(0) // => false isArrayValid(null) // => false isArrayValid([]) // => true isArrayValid([1, 2, 3]) // => false isArrayValid([ {x: 1, y: 1, name: 'A'}, {x: 2, y: 1, name: 'B'}, {x: -1, y: 2, name: 'C'}, {x: 1, y: 3, name: 'D'}, ]) // => true
This is just one of the factory methods, each of which is described in the documentation.
As you saw above, v.rest
also a factory method that returns an object composition that checks all fields not listed in the object composition. So, it can be embedded into another object composition using a spread-operator
.
Here is an example of using several of them:
// quartet const quartet = require('quartet') // (v - validator) const v = quartet() // , const max = { name: 'Maxim', sex: 'male', age: 34, status: 'grandpa', friends: [ { name: 'Dima', friendDuration: '1 year'}, { name: 'Semen', friendDuration: '3 months'} ], workExperience: 2 } // , "" , // "" , "" - const nameSchema = v.and( 'not-empty', 'string', // name => name[0].toUpperCase() === name[0] // - ) const maxSchema = { name: nameSchema, // sex: v.enum('male', 'female'), // - . // "" age: v.and('non-negative', 'safe-integer'), status: v.enum('grandpa', 'non-grandpa'), friends: v.arrayOf({ name: nameSchema, // friendDuration: v.regex(/^[1-9]\d? (years?|months?)$/) }), workExperience: v.and('non-negative', 'safe-integer') } console.log(v(maxSchema)(max)) // => true
It often happens that valid data takes various forms, for example:
id
can be a number, or it can be a string.point
object may or may not contain some coordinates, depending on the dimension.For the organization of the validation of options, a separate type of composition is provided - a variant composition. It is represented by an array of validators of possible options. An object is considered valid when at least one of the validators reported its validity.
Consider an example with validation of identifiers:
const isValidId = v([ v.and('not-empty', 'string'), // v.and('positive', 'safe-integer') // ]) isValidId('') // => false isValidId('asdba32bas321ab321adb321abds546ba98s7') // => true isValidId(0) // => false isValidId(1) // => true isValidId(1123124) // => true
Example with validation points:
const isPointValid = v([ { // - x dimension: v.enum(1), x: 'number', // v.rest false // , - ...v.rest(() => false) }, // - { dimension: v.enum(2), x: 'number', y: 'number', ...v.rest(() => false) }, // - x, y z { dimension: v.enum(3), x: 'number', y: 'number', z: 'number', ...v.rest(() => false) }, ]) // , , , - - isPointValid(1) // => false isPointValid(null) // => false isPointValid({ dimension: 1, x: 2 }) // => true isPointValid({ dimension: 1, x: 2, y: 3 // }) // => false isPointValid({ dimension: 2, x: 2, y: 3 }) // => true isPointValid({ dimension: 3, x: 2, y: 3, z: 4 }) // => true // ...
Thus, whenever a composer sees an array, he will consider it as a composition of validator elements of this array in such a way that when one of them considers the value valid - the validation calculation stops - and the value is considered valid.
As you can see, the composer considers a validator not only a function of a validator, but everything that can lead to a function of a validator.
Validator Type | Example | How is perceived by the composer |
---|---|---|
validation function | x => typeof x === 'bigint' | just called on the required values |
object composition | { a: 'number' } | creates a validator function for an object based on the specified field validators |
Variant composition | ['number', 'string'] | Creates a validator function to validate a value with at least one of the options |
Results of calling factory methods | v.enum('male', 'female') | Most factory methods return validation functions (with the exception of v.rest , which returns an object composition), so they are treated as normal validation functions. |
All these validator variants are valid and can be used anywhere within the circuit in which the validator should be placed.
As a result, the operation scheme is always the following: v(schema)
returns the validation function. This validation function is then invoked on specific values:v(schema)(value[, ...parents])
- not yet one
- There will be!
It so happens that the data is invalid and we need to be able to determine the cause of invalidity.
For this, the quartet library has an explanation mechanism. It consists in the fact that in case the validator, whether internal or external, detects that the data being checked is invalid, he must send an explanatory note .
For these purposes, the second argument of the composer validators v
. It adds a side effect of sending an explanatory note to the v.explanation
array in the event of invalid data.
An example, let us validate an array, and want to find out the numbers of all elements that are invalid and their value:
// - // const getExplanation = (value, { key: index }) => ({ invalidValue: value, index }) // , . // v.explanation // const arrValidator = v.arrayOf( v( 'number', // getExplanation // "", "" ) ) // , "" // , // // , const explainableArrValidator = v(arrValidator, 'this array is not valid') const arr = [1, 2, 3, 4, '5', '6', 7, '8'] explainableArrValidator(arr) // => false v.explanation // [ // { invalidValue: '5', index: 4 }, // { invalidValue: '6', index: 5 }, // { invalidValue: '8', index: 7 }, // 'this array is not valid' // ]
As you can see, the choice of explanation depends on the task. Sometimes it is not even necessary.
Sometimes we need to do something with invalid fields. In such cases, it makes sense to use the name of the invalid field as an explanation :
const objSchema = { a: v('number', 'a'), b: v('number', 'b'), c: v('string', 'c') } const isObjValid = v(objSchema) let invalidObj = { a: 1, b: '1', c: 3 } isObjValid(invalidObj) // => false v.explanation // ['b', 'c'] // console.error(`${v.explanation.join(', ')} is not valid`) // => b, c is not valid // (. ) invalidObj = v.omitInvalidProps(objSchema)(invalidObj) console.log(invalidObj) // => { a: 1 }
With this explanation mechanism, you can implement any behavior associated with validation results.
Anything can be an explanation:
getExplanation => function(invalid): valid
);Correcting validation errors is not a rare task. For these purposes, the library uses validators with a side effect that remembers the location of the error and how to fix it.
v.default(validator, value)
- returns a validator that remembers an invalid value, and at the time of calling v.fix
- sets the default valuev.filter(validator)
- returns a validator that remembers an invalid value, and at the time of calling v.fix
- removes this value from the parentv.addFix(validator, fixFunc)
returns a validator that remembers an invalid value, and at the time v.fix
is called, it calls fixFunc with parameters (value, {key, parent}, ...). fixFunc
- must mutate one of the pairs - to change the value const toPositive = (negativeValue, { key, parent }) => { parent[key] = -negativeValue } const objSchema = { a: v.default('number', 1), b: v.filter('string', ''), c: v.default('array', []), d: v.default('number', invalidValue => Number(invalidValue)), // pos: v.and( v.default('number', 0), // - 0 v.addFix('non-negative', toPositive) // - ) } const invalidObj = { a: 1, b: 2, c: 3, d: '4', pos: -3 } v.resetExplanation() // v() v(objSchema)(invalidObj) // => false // v.hasFixes() => true const validObj = v.fix(invalidObj) console.log(validObj) // => { a: 1, b: '', c: [], d: 4 }
There are also utility methods for validation related actions in this library:
Method | Result |
---|---|
v.throwError | In case of non-validity, throws a TypeError with the specified message. |
v.omitInvalidItems | Returns a new array (or dictionary object) with no invalid elements (fields). |
v.omitInvalidProps | Returns a new object with no invalid fields, given the object validator. |
v.validOr | Returns the value if it is valid, otherwise it replaces it with the specified default value. |
v.example | Checks if these values are appropriate for the schema. If they do not fit, an error is thrown. Provides documentation and testing schemes |
The tasks were solved in the following ways:
Task | Decision |
---|---|
Validation of data types | Default named validators. |
Default values | v.default |
Removing invalid parts | v.filter , v.omitInvalidItems and v.omitInvalidProps . |
Easy to learn | Simple validators, simple ways to compose them into complex validators. |
Code readability | One of the goals of the library was to liken the validation schemes themselves |
validated objects. | |
Ease of modification | Having mastered the elements of compositions and using your own validation functions, changing the code is quite simple. |
Error message | Explanation, in the form of an error message. Or calculation of the error code based on explanations. |
This solution was developed to quickly and easily create validator functions with the ability to embed custom validation functions. Therefore, if any, any changes, criticism, improvement options from reading this article are welcome. Thanks for attention.
Source: https://habr.com/ru/post/429916/
All Articles