Wie man baut und baut

Hintergrund


Nachdem wir uns an zahlreichen Stellen in der Javascript-Entwicklung mit Situationen getroffen hatten, in denen es notwendig war, Werte zu validieren, wurde klar, dass es notwendig war, dieses Problem irgendwie zu lösen. Zu diesem Zweck wurde folgende Aufgabe gestellt:
Entwickeln Sie eine Bibliothek, die Folgendes ermöglicht:


  • Datentypen validieren;
  • Standardwerte anstelle ungültiger Felder oder Elemente festlegen ;
  • ungültige Teile eines Objekts oder Arrays löschen ;
  • Erhalten Sie eine Fehlermeldung

Die Basis davon wird sein:


  • Leicht zu lernen
  • Lesbarkeit des empfangenen Codes.
  • Einfache Codeänderung

Um diese Ziele zu erreichen, wurde eine Validierungsbibliothek für Quartette entwickelt.


Grundlegende Validierungsbausteine


Das Herzstück der meisten Systeme, die für eine Vielzahl von Aufgaben ausgelegt sind, sind die einfachsten Elemente : Aktionen, Daten und Algorithmen. Sowie Methoden ihrer Zusammensetzung - um aus einfachen Elementen etwas Komplizierteres zusammenzusetzen, um komplexere Probleme zu lösen.


Validator


Die Quartettbibliothek basiert auf dem Konzept eines Validators . Die Validatoren in dieser Bibliothek sind Funktionen der folgenden Form


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

In dieser Definition gibt es mehrere Dinge, die genauer beschrieben werden sollten:


function(...): boolean Wert - sagt, dass der Validator - das Ergebnis der Validierung berechnet und das Ergebnis der Validierung ein boolescher Wert ist - wahr oder falsch , jeweils gültig oder ungültig


value: any - gibt an, dass der Validator - das Ergebnis der Validierung eines Werts berechnet , bei dem es sich um einen beliebigen Javascript-Wert handeln kann. Der Validator weist den validierten Wert entweder gültig oder ungültig zu.


{ key: string|int, parent: any }, ... - gibt an, dass sich der validierte Wert in verschiedenen Kontexten befinden kann, je nachdem, auf welcher Verschachtelungsebene der Wert liegt. Lassen Sie es uns anhand von Beispielen zeigen


Beispielwert ohne Kontext


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

Beispielwert in einem Array-Kontext


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

Beispielwert im Kontext eines Objekts


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

Da Strukturen in einem Objekt eine größere Verschachtelung aufweisen können, ist es sinnvoll, über eine Vielzahl von Kontexten zu sprechen


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

Usw.


Über die Ähnlichkeit mit Array-Methoden

Diese Definition eines Validators sollte Sie an die Definition von Funktionen erinnern, die als Argument an Array-Methoden übergeben werden, z. B.: Map, Filter, einige, alle usw.


  • Das erste Argument für diese Funktionen ist ein Array-Element.
  • Das zweite Argument ist der Index des Elements.
  • Das dritte Argument ist das Array selbst.

Der Validator ist in diesem Fall eine allgemeinere Funktion - er verwendet nicht nur den Index des Elements im Array und des Arrays, sondern auch den Index des Arrays - in seinem übergeordneten und seinem übergeordneten Element und so weiter.


Was sollen wir ein Haus bauen?


Die oben beschriebenen Steine ​​heben sich nicht von anderen "Steinlösungen" ab , die am "Strand" der Javascript-Krücke liegen. Lassen Sie uns deshalb etwas Kohärenteres und Interessanteres daraus machen. Dafür haben wir eine Komposition .


Wie baue ich einen Wolkenkratzer für die Objektvalidierung?


Stimmen Sie zu, es wäre zweckmäßig, Objekte so zu validieren, dass die Beschreibung der Validierung mit der Beschreibung des Objekts übereinstimmt. Hierfür verwenden wir die Objektzusammensetzung von Validatoren . Es sieht so aus:


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

Wie Sie sehen können, können wir aus verschiedenen für bestimmte Felder definierten Validierungsbausteinen einen Objektvalidator zusammenstellen - ein "kleines Gebäude", das immer noch ziemlich überfüllt ist - aber besser als ohne. Dazu verwenden wir den Composer of Validators v . Jedes Mal, wenn das Objektliteral v anstelle des Validators erfüllt wird, wird es als Objektzusammensetzung betrachtet und in seinen Feldern in einen Objektvalidator umgewandelt.


Manchmal können wir nicht alle Felder beschreiben . Wenn ein Objekt beispielsweise ein Datenwörterbuch ist:


 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 

Wie kann man Baulösungen wiederverwenden?


Wie wir oben gesehen haben, müssen einfache Validatoren wiederverwendet werden. In diesen Beispielen mussten wir den "String Type Validator" bereits zweimal verwenden.


Um den Datensatz zu verkürzen und seine Lesbarkeit zu verbessern, verwendet die Quartettbibliothek Stringsynonyme von Validatoren. Immer wenn ein Validator-Komponist an der Stelle, an der sich der Validator befinden sollte, auf eine Zeichenfolge stößt, durchsucht er das Wörterbuch nach seinem Validator und verwendet ihn .


Standardmäßig sind die gängigsten Validatoren bereits in der Bibliothek definiert.


Betrachten Sie die folgenden Beispiele:


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

und viele andere in der Dokumentation beschrieben .


Jeder Bogen hat seine eigene Art von Ziegeln?


Der Validator Composer (Funktion v ) ist ebenfalls eine Validator Factory. In dem Sinne, dass es viele nützliche Methoden enthält, die zurückkehren


  • Funktionsprüfer
  • Werte, die der Komponist als Schemata zum Erstellen von Validatoren wahrnimmt

Schauen wir uns zum Beispiel die Array-Validierung an: Meistens besteht sie darin, den Typ des Arrays und alle seine Elemente zu überprüfen. Wir werden hierfür die Methode v.arrayOf(elementValidator) verwenden. Nehmen Sie zum Beispiel ein Array von Punkten mit Namen.


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

Da ein Array von Punkten ein Array von Objekten ist, ist es sinnvoll, die Objektzusammensetzung zu verwenden, um die Elemente des Arrays zu validieren.


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

Erstellen Sie nun mit der Factory-Methode v.arrayOf einen Validator für das gesamte Array.


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

Mal sehen, wie dieser Validator funktioniert:


 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 

Dies ist nur eine der Factory-Methoden, von denen jede in der Dokumentation beschrieben ist.


Wie Sie oben gesehen haben, ist v.rest auch eine Factory-Methode, die eine Objektzusammensetzung zurückgibt, die alle Felder überprüft, die nicht in der Objektzusammensetzung angegeben sind. Das heißt, es kann mit dem spread-operator in eine andere Objektzusammensetzung eingebettet spread-operator .


Nennen wir als Beispiel die Verwendung mehrerer von ihnen:


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

Sein oder Nichtsein?


Es kommt häufig vor, dass gültige Daten verschiedene Formen annehmen, zum Beispiel:


  • id kann eine Zahl oder eine Zeichenfolge sein.
  • Das Punktobjekt kann je nach Dimension einige Koordinaten enthalten oder nicht.
  • Und viele andere Fälle.

Um die Validierung von Varianten zu organisieren, wird ein separater Kompositionstyp bereitgestellt - Variantenkomposition. Es wird durch eine Reihe von Validatoren möglicher Optionen dargestellt. Ein Objekt gilt als gültig, wenn mindestens einer der Validatoren seine Gültigkeit meldet.


Betrachten Sie ein Beispiel mit der Bezeichnervalidierung:


  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 

Beispiel für die Punktvalidierung:


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

Wenn ein Komponist ein Array sieht, betrachtet er es als eine Zusammensetzung der Validatorelemente dieses Arrays, sodass die Berechnung der Validierung stoppt und der Wert als gültig erkannt wird, wenn einer von ihnen den Wert für gültig hält.


Wie wir sehen, betrachtet der Komponist nicht nur die Validatorfunktion als Validator, sondern auch alles, was zu einer Validatorfunktion führen kann.


ValidatortypBeispielWie vom Komponisten wahrgenommen
Validierungsfunktionx => typeof x === 'bigint'habe gerade die notwendigen Werte aufgerufen
Objektzusammensetzung{ a: 'number' }Erstellt eine Validierungsfunktion für ein Objekt basierend auf den angegebenen Feldvalidatoren
Variantenzusammensetzung['number', 'string']Erstellt eine Validierungsfunktion, um einen Wert mit mindestens einer der Optionen zu validieren
Factory Method Call-Ergebnissev.enum('male', 'female')Die meisten Factory-Methoden geben Validierungsfunktionen zurück (mit Ausnahme von v.rest , das die Objektzusammensetzung zurückgibt), sodass sie wie reguläre Validierungsfunktionen behandelt werden

Alle diese Validatoroptionen sind gültig und können überall im Schema verwendet werden, wo sich der Validator befinden sollte.


Infolgedessen sieht das Arbeitsschema immer so aus: v(schema) gibt eine Validierungsfunktion zurück. Als nächstes wird diese Validierungsfunktion für bestimmte Werte aufgerufen:
v(schema)(value[, ...parents])


Haben Sie auf der Baustelle Unfälle gehabt?


- Noch nicht, keiner
- Sie werden!


Es kommt vor, dass die Daten ungültig sind und wir in der Lage sein müssen, die Ursache für die Ungültigkeit zu ermitteln.


Hierfür bietet die Quartettbibliothek einen Erklärungsmechanismus . Es besteht darin, dass der Validator, ob intern oder extern, die Gültigkeit der verifizierten Daten feststellt , eine Erläuterung senden muss.


Für diese Zwecke wird das zweite Argument des Komponisten der Validatoren v . Es fügt den Nebeneffekt hinzu, dass bei ungültigen Daten eine Erläuterung an das Array v.explanation .


Lassen Sie uns beispielsweise ein Array validieren und die Anzahl aller ungültigen Elemente und ihren Wert herausfinden:


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

Wie Sie sehen können, hängt die Wahl der Erklärung von der Aufgabe ab. Manchmal ist es gar nicht nötig.


Manchmal müssen wir etwas mit ungültigen Feldern tun. In solchen Fällen ist es sinnvoll, den Namen des ungültigen Feldes als Erklärung zu verwenden :


 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 } 

Mit diesem Erklärungsmechanismus können Sie jedes Verhalten implementieren, das mit den Validierungsergebnissen verbunden ist.


Eine Erklärung kann alles sein:


  • ein Objekt, das die erforderlichen Informationen enthält;
  • Funktion, die den Fehler korrigiert. ( getExplanation => function(invalid): valid );
  • Name des ungültigen Feldes oder Index des ungültigen Elements;
  • Fehlercode
  • und das alles reicht für deine Fantasie.

Was tun, wenn keine Dinge gebaut werden?


Das Korrigieren von Validierungsfehlern ist keine seltene Aufgabe. Zu diesem Zweck verwendet die Bibliothek Validatoren mit einem Nebeneffekt, der sich an den Ort des Fehlers und dessen Behebung erinnert.


  • v.default(validator, value) - v.default(validator, value) einen Validator zurück, der sich an einen ungültigen Wert erinnert, und legt zum Zeitpunkt des Aufrufs von v.fix den Standardwert fest
  • v.filter(validator) - v.filter(validator) einen Validator zurück, der sich an einen ungültigen Wert erinnert, und entfernt diesen Wert zum Zeitpunkt des Aufrufs von v.fix vom übergeordneten Wert
  • v.addFix(validator, fixFunc) - gibt einen Validator zurück, der sich an einen ungültigen Wert erinnert, und ruft zum Zeitpunkt des Aufrufs von v.fix fixFunc mit Parametern (value, {key, parent}, ...) auf. fixFunc - muss einen der Partner mutieren - um den Wert zu ändern

 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 } 

Aufgaben sind immer noch nützlich


In dieser Bibliothek gibt es auch Dienstprogrammmethoden für Validierungsaktionen:


MethodeErgebnis
v.throwErrorWenn ungültig, wird ein TypeError mit der angegebenen Nachricht ausgelöst.
v.omitInvalidItemsGibt ein neues Array (oder Wörterbuchobjekt) ohne ungültige Elemente (Felder) zurück.
v.omitInvalidPropsGibt ein neues Objekt ohne ungültige Felder gemäß dem angegebenen Objektvalidator zurück.
v.validOrGibt den Wert zurück, wenn er gültig ist, andernfalls wird er durch den angegebenen Standardwert ersetzt.
v.exampleÜberprüft, ob die angegebenen Werte zum Schema passen. Wenn sie nicht passen, wird ein Fehler ausgegeben. Dient als Dokumentation und Schaltungstest

Ergebnisse


Die Aufgaben wurden folgendermaßen gelöst:


HerausforderungLösung
DatentypüberprüfungStandardmäßig benannte Validatoren.
Standardwertev.default
Ungültige Teile entfernenv.filter , v.omitInvalidItems und v.omitInvalidProps .
Leicht zu lernenEinfache Validatoren, einfache Möglichkeiten, sie zu komplexen Validatoren zusammenzusetzen.
Lesbarkeit des CodesEines der Ziele der Bibliothek war es, die Validierungsschemata selbst zu vergleichen
validierte Objekte.
Einfache ÄnderungWenn Sie die Elemente von Kompositionen beherrschen und Ihre eigenen Validierungsfunktionen verwenden, ist das Ändern des Codes ganz einfach.
FehlermeldungErläuterung in Form einer Fehlermeldung. Oder Fehlercodeberechnung basierend auf Erklärungen.

Nachwort


Diese Lösung wurde entwickelt, um schnell und bequem Validierungsfunktionen mit der Möglichkeit zu erstellen, benutzerdefinierte Validierungsfunktionen einzubetten. Daher sind Korrekturen, Kritik und Verbesserungsmöglichkeiten von Personen, die diesen Artikel lesen, willkommen. Vielen Dank für Ihre Aufmerksamkeit.

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


All Articles