Mon râteau: des haillons aux richesses

Contexte


Je travaille en tant que développeur front-end depuis un an maintenant. Mon premier projet était un backend «ennemi». Il arrive que ce ne soit pas un gros problème lorsque la communication est établie.


Mais dans notre cas, ce n'était pas le cas.


Nous avons développé le code, qui reposait sur le fait que le backend nous envoie certaines données, une certaine structure et un certain format. Alors que le backend considérait normal de changer le contenu des réponses - sans avertissement. Par conséquent, il nous a fallu des heures pour déterminer pourquoi une certaine partie du site avait cessé de fonctionner.


Nous avons réalisé que nous devions vérifier ce que le backend renvoie avant de nous fier aux données qu'il nous a envoyées. Nous avons créé une tâche de recherche sur la question de la validation des données depuis le front-end.


Cette étude m'a été commandée.


J'ai fait une liste de ce que je veux être dans l'outil que je voudrais utiliser pour la validation des données.


Les points de sélection les plus importants étaient les points suivants:


  • description déclarative (schéma) de validation, qui est transformée en une fonction de validation qui renvoie vrai / faux (valide, non valide)
  • seuil d'entrée bas;
  • similitude des données validées avec une description de la validation;
  • facilité d'intégration des validations personnalisées;
  • facilité d'intégration des messages d'erreur personnalisés.

Du coup, j'ai trouvé de nombreuses bibliothèques de validation, ayant revu le TOP-5 (ajv, joi, roi ...). Ils sont tous très bons. Mais il m'a semblé que pour résoudre 5% des cas complexes - ils ont condamné 95% des cas les plus courants à être assez verbeux et volumineux.


J'ai donc pensé: pourquoi ne pas développer vous-même quelque chose qui me conviendrait.
Quatre mois plus tard, la septième version de ma bibliothèque de validation de quatuor est sortie.
C'était une version stable, entièrement testée, 11 000 téléchargements sur npm. Nous l'avons utilisé sur trois projets dans une campagne de trois mois.


Ces trois mois ont joué un rôle très utile. quatuor a démontré tous ses avantages. Il n'y a aucun problème de données avec le backend. Chaque fois qu'ils ont changé la réponse, nous avons immédiatement jeté une erreur. Le temps passé à rechercher les causes des bogues a considérablement diminué. Il n'y a pratiquement plus de bogues de données.


Mais des défauts ont également été identifiés.


Par conséquent, j'ai décidé de les analyser et de publier une nouvelle version avec des corrections de toutes les erreurs qui ont été faites pendant le développement.
Je parlerai de ces erreurs architecturales et de leurs solutions ci-dessous.


Râteau architectural


"Stroko" - une langue typique du schéma


Je vais donner un exemple de l'ancienne version du schéma pour l'objet de la personne.


const personSchema = { name: 'string', age: 'number', linkedin: ['string', 'null'] } 

Ce schéma valide un objet avec trois propriétés: nom - doit être une chaîne, âge - doit être un nombre, lien vers un compte dans LinkedIn - doit être soit nul (s'il n'y a pas de compte) ou chaîne (s'il y a un compte).


Ce schéma répond à mes exigences de lisibilité, de similitude avec les données validées, et je pense que le seuil d'entrée pour apprendre à écrire de tels schémas n'est pas élevé. De plus, un tel schéma peut être facilement écrit avec une définition de type en tapuscrit:


 type Person = { name: string age: number linkedin: string | null } 

(Comme vous pouvez le voir - les changements sont plus probablement cosmétiques)


Lorsque j'ai pris une décision, ce qui devrait être utilisé pour les options de validation les plus fréquentes (par exemple, celles utilisées ci-dessus). J'ai choisi d'utiliser - des chaînes, pour ainsi dire, les noms des validateurs.


Mais le problème avec les chaînes est qu'elles ne sont pas disponibles pour le compilateur ou l'analyseur d'erreur. La chaîne 'nombre' pour eux n'est pas très différente de 'numder'.


Solution


La nouvelle version du quatuor 8.0.0. J'ai décidé de retirer du quatuor - l'utilisation de chaînes comme noms de validateurs dans le schéma.


Le diagramme ressemble maintenant à ceci:


 const personSchema = { name: v.string age: v.number, linkedin: [v.string, null] } 

Ce changement présente deux grands avantages:


  • compilateurs ou analyseurs d'erreurs - pourront détecter que le nom de la méthode est orthographié avec une erreur.
  • Les lignes - ne sont plus utilisées comme élément de schéma. Cela signifie que pour eux, vous pouvez sélectionner de nouvelles fonctionnalités dans la bibliothèque, qui seront décrites ci-dessous.

Prise en charge de TypeScript


En général, les sept premières versions ont été développées en pur Javascript. Lors du passage à un projet avec Typescript, il était nécessaire d'adapter la bibliothèque en quelque sorte. Par conséquent, des déclarations de type pour la bibliothèque ont été écrites.


Mais c'était un inconvénient - lors de l'ajout de fonctionnalités ou de la modification de certains éléments de la bibliothèque, il était toujours facile d'oublier de mettre à jour les déclarations de type.


Il y avait aussi juste des inconvénients mineurs de ce genre:


 const checkPerson = v(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

Lorsque nous avons créé le validateur d'objet en ligne (0). Nous aimerions après avoir vérifié la vraie réponse du backend en ligne (1) et traité l'erreur. En ligne (2) pour que cette person type Personne. Mais cela ne s'est pas produit. Malheureusement, un tel contrôle n'était pas un type de garde.


Solution


J'ai décidé de réécrire toute la bibliothèque du quatuor sur Typescript afin que le compilateur soit engagé dans la vérification de la correspondance de la bibliothèque avec les types. En cours de route, nous ajoutons à la fonction qui retourne le validateur compilé un paramètre de type qui déterminerait quel type garde ce type de validateur.


Un exemple ressemble à ceci:


 const checkPerson = v<Person>(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

Maintenant en ligne (2) person est de type Person .


Lisibilité


Il y avait également deux cas où le code était mal lu: vérification de la conformité avec un certain ensemble de valeurs (vérification des énumérations) et vérification d'autres propriétés de l'objet.


a) Vérifier les énumérations
Au départ, il y avait une idée, à mon avis une bonne idée. Nous allons le démontrer en ajoutant le champ "genre" à notre objet.
L'ancienne version du circuit ressemblait à ceci:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum('male', 'female') } 

L'option est très lisible. Mais comme d'habitude, tout s'est un peu déréglé.
Avoir une énumération déclarée dans le programme, par exemple, ceci:


 enum Sex { Male = 'male', Female = 'female' } 

Naturellement, je veux l'utiliser à l'intérieur du circuit. De sorte que lorsque vous modifiez l'une des valeurs (par exemple, «mâle» -> «m», «femelle» -> «f»), le schéma de validation doit également changer.


Par conséquent, la validation d'énumération était presque toujours écrite comme ceci:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)) } 

Ce qui semble assez volumineux.


b) Validation des propriétés résiduelles de l'objet


Supposons que nous ajoutions une telle caractéristique à notre objet - il peut avoir des champs supplémentaires, mais tous doivent être des liens vers des réseaux sociaux - cela signifie qu'ils doivent être soit null soit une chaîne.


L'ancien schéma ressemblerait à ceci:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)), ...v.rest(['null', 'string']) // Rest props are string | null } 

Cette entrée a mis en évidence les propriétés restantes - de celles déjà répertoriées. L'utilisation d'un opérateur de diffusion est plus susceptible de dérouter une personne qui veut comprendre ce schéma.


Solution


Comme décrit ci-dessus, les chaînes ne font plus partie des schémas de validation. Seuls trois types de valeurs Javascript restaient un schéma de validation. Objet - pour décrire le schéma de validation de l'objet. Tableau pour la description - plusieurs options de validité. Fonction (générée par la bibliothèque ou personnalisée) - pour toutes les autres options de validation.


Cette disposition a permis d'ajouter des fonctionnalités, ce qui a permis d'augmenter la lisibilité du circuit plusieurs fois.


En fait, que se passe-t-il si nous voulons comparer la valeur avec la chaîne «male». Avons-nous vraiment besoin de savoir autre chose que la valeur elle-même et la chaîne «male».


Par conséquent, il a été décidé d'ajouter les valeurs des types primitifs comme élément du circuit. Par conséquent, lorsque vous rencontrez une valeur primitive dans le schéma, cela signifie qu'il s'agit de la valeur valide que le validateur créé conformément à ce schéma doit vérifier. Je ferais mieux de donner un exemple:


Si nous devons vérifier le nombre d'égalité 42-esprit. Ensuite, nous l'écrivons comme ceci:


 const check42 = v(42) check42(42) // => true check42(41) // => false check42(43) // => false check42('42') // => false 

Voyons comment cela affecte le schéma de la personne (sans tenir compte des propriétés supplémentaires):


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], // null is primitive value sex: ['male', 'female'] // 'male', 'female' are primitive values } 

En utilisant des énumérations prédéfinies, nous pouvons le réécrire comme ceci:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex) // same as ['male', 'female'] } 

Dans ce cas, la cérémonialité inutile a été supprimée sous la forme de l'utilisation de la méthode enum et de l'utilisation de l'opérateur d'étalement pour insérer des valeurs valides de l'objet comme paramètres dans cette méthode.


Ce qui est considéré comme une valeur primitive: nombres, chaînes, caractères, true , false , null et undefined .


Autrement dit, si nous devons comparer la valeur avec eux, nous utilisons simplement ces valeurs elles-mêmes. Et une bibliothèque de validation - elle créera un validateur qui compare strictement la valeur à celles spécifiées dans le schéma.


Pour valider les propriétés résiduelles, il a été choisi d'utiliser une propriété spéciale pour tous les autres champs de l'objet:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex), [v.rest]: [null, v.string] } 

Ainsi, le circuit semble plus lisible. Et plus comme des annonces Typescript.


Le validateur est lié à la fonction qui l'a créé


Dans les anciennes versions, les explications d'erreur ne faisaient pas partie du validateur. Ils ont été ajoutés à un tableau à l'intérieur de la fonction v .


Auparavant, pour obtenir une explication des erreurs de validation, vous deviez avoir un validateur avec vous (pour vérifier) ​​et v (pour obtenir une explication d'invalidité). Tout cela ressemblait à ceci:

a) Nous ajoutons des explications au schéma


 const checkPerson = v({ name: v('string', 'wrong name') age: v('number', 'wrong age'), linkedin: v(['null', 'string'], 'wrong linkedin'), sex: v( v.enum(...Object.values(Sex)), 'wrong sex value' ), ...v.rest( v( ['null', 'string'], 'wrong social networks link' ) ) // Rest props are string | null }) 

À n'importe quel élément du circuit - vous pouvez ajouter une explication de l'erreur en utilisant le deuxième argument de la fonction de compilateur v.


b) Effacer le tableau d'explication


Avant la validation, il était nécessaire d'effacer ce tableau global dans lequel toutes les explications étaient enregistrées lors de la validation.


 v.clearContext() // same as v.explanations = [] 

c) Valider


 const isPersonValid = checkPerson(person) 

Lors de cette vérification, si une validité a été trouvée, et au stade de la création du circuit - une explication lui a été donnée, cette explication est placée dans le v.explanation global v.explanation .


d) Traitement des erreurs


 if (!isPersonValid) { throw new TypeError('Invalid person response: ' + v.explanation.join('; ')) } // ex. Throws 'Invalid person response: wrong name; wrong age' 

Comme vous pouvez le voir ici, il y a un gros problème. Car si on veut utiliser le validateur pas à la place de sa création. Nous devrons le transmettre non seulement aux paramètres, mais aussi à la fonction qui l'a créé. Parce que c'est en elle que se trouve le tableau dans lequel les explications seront ajoutées.


Solution


Ce problème a été résolu comme suit: les explications sont devenues une partie de la fonction de validation elle-même. Que peut-on comprendre de son type:
type Validator = (valeur: any, explications?: any []) => booléen


Maintenant, si vous avez besoin d'une explication de l'erreur, vous passez le tableau dans lequel vous souhaitez ajouter l'explication.


Ainsi, le validateur devient une unité indépendante. Une méthode a également été ajoutée qui peut transformer la fonction de validation en une fonction qui renvoie null si la valeur est valide et renvoie un tableau d'explications si la valeur n'est pas valide.


Maintenant, la validation avec explications ressemble à ceci:


 const checkPerson = v<Person>({ name: v(v.string, 'wrong name'), age: v(v.number, 'wrong age'), linkedin: v([null, v.string], 'wrong linkedin') sex: v(Object.values(Sex), 'wrong sex') [v.rest]: v([null, v.string], 'wrong social network') }) // ... const explanations = [] if (!checkPerson(person, explanation)) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } // OR const getExplanation = v.explain(checkPerson) const explanations = getExplanation(person) if (explanations) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } 

Postface


J'ai souligné trois prémisses à cause desquelles j'ai dû tout réécrire:


  • L'espoir que les gens ne se trompent pas en écrivant des lignes
  • Utilisation de variables globales (dans ce cas, tableau v.explanation)
  • Tester avec de petits exemples au cours du développement - n'a pas montré les problèmes qui se posent lorsqu'il est utilisé dans de très gros cas.

Mais je suis heureux d'avoir effectué une analyse de ces problèmes, et la version publiée est déjà utilisée dans notre projet. Et j'espère qu'il nous sera utile non moins que le précédent.


Merci à tous pour la lecture, j'espère que mon expérience vous sera utile.

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


All Articles