Comment construire et construire

Contexte


Ayant rencontré à de nombreux endroits dans le développement Javascript des situations où il était nécessaire de valider des valeurs, il est devenu clair qu'il était nécessaire de résoudre ce problème. À cet effet, la tâche suivante a été définie:
Développer une bibliothèque qui permettra:


  • valider les types de données ;
  • définir des valeurs par défaut au lieu de champs ou d'éléments non valides;
  • supprimer des parties non valides d'un objet ou d'un tableau;
  • Recevoir un message d'erreur

Dont la base sera:


  • Facile à apprendre
  • Lisibilité du code reçu.
  • Facilité de modification du code

Pour atteindre ces objectifs, une bibliothèque de validation de quatuor a été développée.


Briques de validation de base


Au cœur de la plupart des systèmes conçus pour être applicables à un large éventail de tâches se trouvent les éléments les plus simples: actions, données et algorithmes. Ainsi que les méthodes de leur composition - afin d'assembler quelque chose à partir des éléments les plus simples les plus complexes pour résoudre des problèmes plus complexes.


Validateur


La bibliothèque du quatuor est basée sur le concept d'un validateur . Les validateurs de cette bibliothèque sont des fonctions du formulaire suivant


function validator( value: any, { key: string|int, parent: any }, { key: string|int, parent: any }, ... ): boolean 

Il y a plusieurs choses dans cette définition qui devraient être décrites plus en détail:


function(...): boolean - dit que le validateur - calcule le résultat de la validation, et le résultat de la validation est une valeur booléenne - vrai ou faux , respectivement valide ou non valide


value: any - indique que le validateur - calcule le résultat de la validation d'une valeur , qui peut être n'importe quelle valeur javascript. Le validateur attribue la valeur validée à valide ou non valide.


{ key: string|int, parent: any }, ... - indique que la valeur validée peut être dans des contextes différents selon le niveau d'imbrication de la valeur. Montrons-le avec des exemples


Exemple de valeur sans aucun contexte


 const value = 4; //         . //         : const isValueValid = validator(4) 

Exemple de valeur dans un contexte de tableau


 //  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] }) 

Exemple de valeur dans le contexte d'un objet


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

Étant donné que les structures d'un objet peuvent avoir une imbrication plus importante, il est logique de parler de divers contextes


 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 }] } ) 

Et ainsi de suite.


À propos de la similitude avec les méthodes de tableau

Cette définition d'un validateur devrait vous rappeler la définition des fonctions qui sont passées en argument aux méthodes de tableau, telles que: carte, filtre, certains, tous , etc.


  • Le premier argument de ces fonctions est un élément de tableau.
  • Le deuxième argument est l' indice de l'élément.
  • Le troisième argument est le tableau lui-même.

Le validateur dans ce cas est une fonction plus généralisée - il prend non seulement l'index de l'élément dans le tableau et le tableau, mais aussi l'index du tableau - dans son parent et son parent, et ainsi de suite.


Que devons-nous construire une maison?


Les briques décrites ci-dessus ne se distinguent pas des autres "solutions de pierre" qui reposent sur la "plage" de la béquille javascript. Par conséquent, construisons à partir d'eux quelque chose de plus cohérent et intéressant. Pour cela, nous avons une composition .


Comment construire un gratte-ciel de validation d'objet?


D'accord, il serait pratique de valider les objets de telle manière que la description de la validation corresponde à la description de l'objet. Pour cela, nous utiliserons la composition d'objet des validateurs . Cela ressemble à ceci:


 //    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 

Comme vous pouvez le voir, à partir de différentes briques de validateur définies pour des champs spécifiques, nous pouvons assembler un validateur d'objet - un "petit bâtiment", qui est encore assez encombré - mais mieux que sans lui. Pour cela, nous utilisons le compositeur de validateurs v . A chaque fois, rencontrant le littéral objet v à la place du validateur, il le considérera comme une composition objet, le transformant en valideur objet dans ses domaines.


Parfois, nous ne pouvons pas décrire tous les domaines . Par exemple, lorsqu'un objet est un dictionnaire de données:


 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 

Comment réutiliser les solutions de construction?


Comme nous l'avons vu ci-dessus, il est nécessaire de réutiliser des validateurs simples. Dans ces exemples, nous avons déjà dû utiliser le "validateur de type de chaîne" deux fois déjà.


Afin de raccourcir l'enregistrement et d'augmenter sa lisibilité, la bibliothèque de quatuors utilise des synonymes de chaînes de validateurs. Chaque fois qu'un compositeur de validateur rencontre une chaîne à l'endroit où le validateur doit être, il recherche le validateur dans le dictionnaire et l'utilise .


Par défaut, les valideurs les plus courants sont déjà définis dans la bibliothèque.


Considérez les exemples suivants:


 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 // ... 

et bien d'autres décrits dans la documentation .


Chaque arc a son propre type de briques?


Le compositeur de validateur (fonction v ) est également une fabrique de validateurs. En ce sens qu'il contient de nombreuses méthodes utiles qui renvoient


  • validateurs de fonction
  • valeurs que le compositeur percevra comme des schémas de création de validateurs

Par exemple, regardons la validation du tableau: le plus souvent, elle consiste à vérifier le type du tableau et à vérifier tous ses éléments. Nous utiliserons pour cela la méthode v.arrayOf(elementValidator) . Par exemple, prenez un tableau de points avec des noms.


  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'}, ] 

Puisqu'un tableau de points est un tableau d'objets, il est judicieux d'utiliser la composition d'objets pour valider les éléments du tableau.


 const namedPointSchema = { x: 'number', // number -       y: 'number', name: 'string' // string -       } 

Maintenant, en utilisant la méthode d'usine v.arrayOf , créez un validateur pour l'ensemble du tableau.


 const isArrayValid = v.arrayOf({ x: 'number', y: 'number', name: 'string' }) 

Voyons comment fonctionne ce validateur:


 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 

Ceci n'est qu'une des méthodes d'usine, chacune étant décrite dans la documentation.


Comme vous l'avez vu ci-dessus, v.rest également une méthode d'usine qui renvoie une composition d'objet qui vérifie tous les champs non spécifiés dans la composition d'objet. Cela signifie qu'il peut être intégré dans une autre composition d'objet à l'aide de l' spread-operator .


Citons comme exemple l'utilisation de plusieurs d'entre elles:


 //    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 

Être ou ne pas être?


Il arrive souvent que des données valides prennent diverses formes, par exemple:


  • id peut être un nombre ou une chaîne.
  • L'objet point peut contenir ou non certaines coordonnées, selon la dimension.
  • Et bien d'autres cas.

Pour organiser la validation des variantes, un type de composition distinct est fourni - la composition des variantes. Il est représenté par un tableau de validateurs d'options possibles. Un objet est considéré comme valide quand au moins l'un des validateurs signale sa validité.


Prenons un exemple de validation d'identifiant:


  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 

Exemple de validation de point:


 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 // ... 

Ainsi, chaque fois qu'un compositeur voit un tableau, il le considérera comme une composition des éléments de validation de ce tableau de telle manière que lorsque l'un d'entre eux considère la valeur comme valide, le calcul de validation s'arrêtera et la valeur sera reconnue comme valide.


Comme on le voit, le compositeur considère non seulement la fonction validateur comme un validateur, mais aussi tout ce qui peut conduire à une fonction validateur.


Type de validateurExempleTel que perçu par le compositeur
fonction de validationx => typeof x === 'bigint'vient d'appeler sur les valeurs nécessaires
composition d'objet{ a: 'number' }crée une fonction de validation pour un objet en fonction des validateurs de champ spécifiés
Composition des variantes['number', 'string']Crée une fonction de validation pour valider une valeur avec au moins une des options
Résultats des appels de méthode d'usinev.enum('male', 'female')La plupart des méthodes d'usine renvoient des fonctions de validation (à l'exception de v.rest , qui retourne la composition des objets), elles sont donc traitées comme des fonctions de validation normales

Toutes ces options de validateur sont valides et peuvent être utilisées n'importe où dans le schéma où le validateur doit être.


Par conséquent, le schéma de travail est toujours le suivant: v(schema) renvoie une fonction de validation. Ensuite, cette fonction de validation est appelée sur des valeurs spécifiques:
v(schema)(value[, ...parents])


Avez-vous eu des accidents sur le chantier?


- Pas encore, pas un
- Ils le feront!


Il arrive que les données soient invalides et nous devons être en mesure de déterminer la cause de l'invalide.


Pour cela, la bibliothèque de quatuors fournit un mécanisme d' explication . Elle consiste en ce que dans le cas où le validateur, qu'il soit interne ou externe, détecte la validité des données vérifiées, il doit envoyer une note explicative .


À ces fins, le deuxième argument du compositeur de validateurs v . Il ajoute l'effet secondaire de l'envoi d'une note explicative au tableau v.explanation en cas de données non valides.


Par exemple, laissez-nous valider un tableau et souhaitons connaître le nombre de tous les éléments invalides et leur valeur:


  //   -     //   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' // ] 

Comme vous pouvez le voir, le choix de l'explication dépend de la tâche. Parfois, ce n'est même pas nécessaire.


Parfois, nous devons faire quelque chose avec des champs invalides. Dans de tels cas, il est logique d'utiliser le nom du champ non valide comme explication :


 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 } 

Ayant ce mécanisme d'explication, vous pouvez implémenter tout comportement associé aux résultats de la validation.


Une explication peut être n'importe quoi:


  • un objet contenant les informations nécessaires;
  • fonction qui corrige l'erreur. ( getExplanation => function(invalid): valid );
  • nom du champ non valide ou index de l'élément non valide;
  • code d'erreur
  • et tout cela suffit à votre imagination.

Que faire quand les choses ne se construisent pas?


La correction des erreurs de validation n'est pas une tâche rare. À ces fins, la bibliothèque utilise des validateurs avec un effet secondaire qui se souvient du lieu de l'erreur et de la façon de la corriger.


  • v.default(validator, value) - retourne un validateur qui se souvient d'une valeur invalide, et au moment d'appeler v.fix - définit la valeur par défaut
  • v.filter(validator) - retourne un validateur qui se souvient d'une valeur invalide, et au moment d'appeler v.fix - supprime cette valeur du parent
  • v.addFix(validator, fixFunc) - renvoie un validateur qui se souvient d'une valeur non valide, et au moment d'appeler v.fix - appelle fixFunc avec des paramètres (valeur, {clé, parent}, ...). fixFunc - doit muter l'un des partenaires - pour changer la valeur

 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 } 

Les tâches sont toujours utiles


Il existe également des méthodes utilitaires pour les actions de validation dans cette bibliothèque:


La méthodeRésultat
v.throwErrorS'il n'est pas valide, lance une TypeError avec le message donné.
v.omitInvalidItemsRenvoie un nouveau tableau (ou objet dictionnaire) sans éléments (champs) non valides.
v.omitInvalidPropsRenvoie un nouvel objet sans champs non valides, selon le validateur d'objet spécifié.
v.validOrRenvoie la valeur si elle est valide, sinon elle la remplace par la valeur par défaut spécifiée.
v.exampleVérifie si les valeurs données correspondent au schéma. S'ils ne correspondent pas, une erreur est générée. Sert de documentation et de test de circuit

Résultats


Les tâches ont été résolues de la manière suivante:


DéfiSolution
Validation du type de donnéesValidateurs nommés par défaut.
Valeurs par défautv.default
Suppression de pièces invalidesv.filter , v.omitInvalidItems et v.omitInvalidProps .
Facile à apprendreValidateurs simples, façons simples de les composer en validateurs complexes.
Lisibilité du codeL'un des objectifs de la bibliothèque était de comparer les schémas de validation eux-mêmes
objets validés.
Facilité de modificationAprès avoir maîtrisé les éléments des compositions et utilisé vos propres fonctions de validation - changer le code est assez simple.
Message d'erreurExplication sous forme de message d'erreur. Ou calcul du code d'erreur basé sur des explications.

Postface


Cette solution a été conçue pour créer rapidement et facilement des fonctions de validation avec la possibilité d'intégrer des fonctions de validation personnalisées. Par conséquent, s'il y en a, toutes les corrections, critiques, options d'amélioration de la part de ceux qui ont lu cet article sont les bienvenues. Merci de votre attention.

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


All Articles