Mi rastrillo: de trapos a riquezas

Antecedentes


He estado trabajando como desarrollador front-end durante un a帽o. Mi primer proyecto fue un backend "enemigo". Sucede que este no es un gran problema cuando se establece la comunicaci贸n.


Pero en nuestro caso no fue as铆.


Desarrollamos el c贸digo, que se bas贸 en el hecho de que el backend nos env铆a ciertos datos, cierta estructura y cierto formato. Mientras que el backend consider贸 normal cambiar el contenido de las respuestas, sin previo aviso. Como resultado, nos llev贸 horas determinar por qu茅 cierta parte del sitio dej贸 de funcionar.


Nos dimos cuenta de que necesit谩bamos verificar lo que devuelve el backend antes de confiar en los datos que nos envi贸. Creamos una tarea de investigaci贸n sobre el tema de la validaci贸n de datos desde el front-end.


Este estudio me fue encargado.


He hecho una lista de lo que quiero ser en la herramienta que me gustar铆a usar para la validaci贸n de datos.


Los puntos de selecci贸n m谩s importantes fueron los siguientes:


  • Descripci贸n declarativa (esquema) de validaci贸n, que se transforma en una funci贸n de validaci贸n que devuelve verdadero / falso (v谩lido, no v谩lido)
  • umbral de entrada bajo;
  • similitud de datos validados con una descripci贸n de validaci贸n;
  • facilidad de integraci贸n de validaciones personalizadas;
  • facilidad de integraci贸n de mensajes de error personalizados.

Como resultado, encontr茅 muchas bibliotecas de validaci贸n, despu茅s de haber revisado el TOP-5 (ajv, joi, roi ...). Todos son muy buenos. Pero me pareci贸 que para resolver el 5% de los casos complejos, condenaron el 95% de los casos m谩s comunes a ser bastante detallados y voluminosos.


Por lo tanto, pens茅: 驴por qu茅 no desarrollar algo t煤 mismo que me convenga?
Cuatro meses despu茅s, sali贸 la s茅ptima versi贸n de mi biblioteca de validaci贸n de cuarteto .
Era una versi贸n estable, totalmente probada, 11k descargas en npm. Lo usamos en tres proyectos en una campa帽a durante tres meses.


Estos tres meses han jugado un papel muy 煤til. Cuarteto ha demostrado todas sus ventajas. No hay problemas de datos desde el backend. Cada vez que cambiaron la respuesta, inmediatamente arrojamos un error. El tiempo dedicado a encontrar las causas de los errores ha disminuido dram谩ticamente. Pr谩cticamente no quedan errores de datos.


Pero tambi茅n se identificaron fallas.


Por lo tanto, decid铆 analizarlos y lanzar una nueva versi贸n con correcciones de todos los errores que se cometieron durante el desarrollo.
Hablar茅 sobre estos errores arquitect贸nicos y sus soluciones a continuaci贸n.


Rastrillo arquitect贸nico


"Stroko" - un lenguaje tipificado del esquema


Dar茅 un ejemplo de la versi贸n anterior del esquema para el objeto de la persona.


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

Este esquema valida un objeto con tres propiedades: nombre - debe ser una cadena, edad - debe ser un n煤mero, enlace a una cuenta en LinkedIn - debe ser nulo (si no hay cuenta) o cadena (si hay una cuenta).


Este esquema cumple con mis requisitos de legibilidad, similitud con datos validados, y creo que el umbral de entrada para aprender a escribir dichos esquemas no es alto. Adem谩s, dicho esquema se puede escribir f谩cilmente con una definici贸n de tipo en mecanografiado:


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

(Como puede ver, es m谩s probable que los cambios sean cosm茅ticos)


Cuando tom茅 una decisi贸n, qu茅 deber铆a usarse para las opciones de validaci贸n m谩s frecuentes (por ejemplo, las que se usaron anteriormente). Eleg铆 usar - cadenas, por as铆 decirlo, los nombres de los validadores.


Pero el problema con las cadenas es que no est谩n disponibles para el compilador o el analizador de errores. La cadena 'n煤mero' para ellos no es muy diferente de 'numder'.


Soluci贸n


La nueva versi贸n del cuarteto 8.0.0. Decid铆 eliminar del cuarteto: el uso de cadenas como nombres de validadores dentro del esquema.


El diagrama ahora se ve as铆:


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

Este cambio tiene dos grandes ventajas:


  • compiladores o analizadores de errores: podr谩n detectar que el nombre del m茅todo est谩 escrito con un error.
  • L铆neas: ya no se utilizan como elemento de esquema. Esto significa que para ellos puede seleccionar una nueva funcionalidad en la biblioteca, que se describir谩 a continuaci贸n.

Soporte de TypeScript


En general, las primeras siete versiones se desarrollaron en Javascript puro. Al cambiar a un proyecto con Typecript, era necesario adaptar de alguna manera la biblioteca. Por lo tanto, se escribieron declaraciones de tipo para la biblioteca.


Pero esto era un inconveniente: al agregar funcionalidad o al cambiar algunos elementos de la biblioteca, siempre era f谩cil olvidarse de actualizar las declaraciones de tipo.


Tambi茅n hubo inconvenientes menores de este tipo:


 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) 

Cuando creamos el validador de objetos en la l铆nea (0). Nos gustar铆a despu茅s de verificar la respuesta real del backend en la l铆nea (1) y manejar el error. En la l铆nea (2) para que esa person tipo Persona. Pero esto no sucedi贸. Desafortunadamente, tal verificaci贸n no era un tipo de guardia.


Soluci贸n


Decid铆 reescribir toda la biblioteca del cuarteto en Typecript para que el compilador se ocupara de verificar la correspondencia de la biblioteca con los tipos. En el camino, agregamos a la funci贸n que devuelve al validador compilado un par谩metro de tipo que determinar铆a qu茅 tipo de protecci贸n es este tipo de validador.


Un ejemplo se ve as铆:


 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) 

Ahora en l铆nea (2) person es del tipo Person .


Legibilidad


Tambi茅n hubo dos casos en los que el c贸digo se ley贸 mal: verificar el cumplimiento de un determinado conjunto de valores (verificaciones de enumeraciones) y verificar otras propiedades del objeto.


a) Verificar enumeraciones
Inicialmente, hab铆a una idea, en mi opini贸n, una buena. Lo demostraremos agregando el campo "g茅nero" a nuestro objeto.
La versi贸n anterior del circuito se ve铆a as铆:


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

La opci贸n es muy legible. Pero como de costumbre, todo sali贸 un poco fuera de lo planeado.
Tener una enumeraci贸n declarada en el programa, por ejemplo, esto:


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

Naturalmente quiero usarlo dentro del circuito. De modo que al cambiar uno de los valores (por ejemplo, 'masculino' -> 'm', 'femenino' -> 'f'), el esquema de validaci贸n tambi茅n debe cambiar.


Por lo tanto, casi siempre la validaci贸n de enumeraci贸n se escribi贸 as铆:


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

Lo cual se ve bastante voluminoso.


b) Validaci贸n de las propiedades residuales del objeto.


Supongamos que agregamos una caracter铆stica de este tipo a nuestro objeto (puede tener campos adicionales, pero todos deben ser enlaces a redes sociales), lo que significa que deben ser null o una cadena.


El viejo esquema se ver铆a as铆:


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

Esta entrada resalt贸 las propiedades restantes, de las que ya figuran. Es m谩s probable que el uso de un operador de propagaci贸n confunda a una persona que quiere comprender este esquema.


Soluci贸n


Como se describi贸 anteriormente, las cadenas ya no son parte de los esquemas de validaci贸n. Solo quedaron tres tipos de valores de Javascript como esquema de validaci贸n. Objeto: para describir el esquema de validaci贸n del objeto. Matriz para la descripci贸n: varias opciones de validez. Funci贸n (biblioteca generada o personalizada): para todas las dem谩s opciones de validaci贸n.


Esta disposici贸n permiti贸 agregar funcionalidad, lo que permiti贸 aumentar la legibilidad del circuito muchas veces.


De hecho, 驴qu茅 pasa si queremos comparar el valor con la cadena 'masculino'? 驴Realmente necesitamos saber algo m谩s adem谩s del valor en s铆 y la cadena 'masculino'?


Por lo tanto, se decidi贸 agregar los valores de los tipos primitivos como un elemento del circuito. Por lo tanto, cuando cumple un valor primitivo en el esquema, esto significa que este es el valor v谩lido que el validador creado de acuerdo con este esquema debe verificar. Ser谩 mejor que d茅 un ejemplo:


Si necesitamos verificar el n煤mero para la igualdad 42-mente. Luego lo escribimos as铆:


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

Veamos c贸mo esto afecta el esquema de persona (sin tener en cuenta las propiedades adicionales):


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

Usando enumeraciones predefinidas, podemos reescribirlo as铆:


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

En este caso, se elimin贸 la ceremonialidad innecesaria en la forma de usar el m茅todo enum y usar el operador de propagaci贸n para insertar valores v谩lidos del objeto como par谩metros en este m茅todo.


Lo que se considera un valor primitivo: n煤meros, cadenas, caracteres, true , false , null e undefined .


Es decir, si necesitamos comparar el valor con ellos, simplemente usamos estos valores ellos mismos. Y una biblioteca de validaci贸n: crear谩 un validador que compara estrictamente el valor con los especificados en el esquema.


Para validar las propiedades residuales, se eligi贸 usar una propiedad especial para todos los dem谩s campos del objeto:


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

Por lo tanto, el circuito parece m谩s legible. Y m谩s como anuncios de Typecript.


El validador est谩 relacionado con la funci贸n que lo cre贸.


En versiones anteriores, las explicaciones de error no formaban parte del validador. Se agregaron a una matriz dentro de la funci贸n v .


Anteriormente, para obtener una explicaci贸n de los errores de validaci贸n, ten铆a que tener un validador con usted (para verificar) yv (para obtener una explicaci贸n de invalidez). Todo esto parec铆a lo siguiente:

a) Agregamos explicaciones al diagrama


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

Para cualquier elemento del circuito, puede agregar una explicaci贸n del error utilizando el segundo argumento de la funci贸n del compilador v.


b) Borrar la matriz de explicaci贸n


Antes de la validaci贸n, era necesario borrar esta matriz global en la que se registraron todas las explicaciones durante la validaci贸n.


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

c) Validar


 const isPersonValid = checkPerson(person) 

Durante esta verificaci贸n, si se detect贸 una validez, y en la etapa de creaci贸n del circuito, se le dio una explicaci贸n, esta explicaci贸n se coloca en la v.explanation global v.explanation .


d) Manejo de errores


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

Como puede ver aqu铆 hay un gran problema. Porque si queremos usar el validador no en el lugar de su creaci贸n. Necesitaremos pasarlo no solo a los par谩metros, sino tambi茅n a la funci贸n que lo cre贸. Porque es all铆 donde se ubica la matriz en la que se agregar谩n las explicaciones.


Soluci贸n


Este problema se resolvi贸 de la siguiente manera: las explicaciones se convirtieron en parte de la funci贸n de validaci贸n. Lo que se puede entender de su tipo:
tipo Validator = (valor: any, explicaciones?: any []) => boolean


Ahora, si necesita una explicaci贸n del error, pasa la matriz a la que desea agregar la explicaci贸n.


Por lo tanto, el validador se convierte en una unidad independiente. Tambi茅n se ha agregado un m茅todo que puede transformar la funci贸n de validaci贸n en una funci贸n que devuelve nulo si el valor es v谩lido y devuelve una matriz de explicaciones si el valor no es v谩lido.


Ahora la validaci贸n con explicaciones se ve as铆:


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

Ep铆logo


Destaqu茅 tres premisas por las cuales tuve que reescribir todo:


  • La esperanza de que las personas no se equivoquen al escribir l铆neas
  • Uso de variables globales (en este caso, v.explanation array)
  • Las pruebas con peque帽os ejemplos durante el desarrollo no mostraron los problemas que surgen cuando se usan en casos realmente grandes.

Pero me alegro de haber realizado un an谩lisis de estos problemas, y la versi贸n lanzada ya se utiliza en nuestro proyecto. Y espero que nos sea 煤til no menos que el anterior.


Gracias a todos por leer, espero que mi experiencia les sea 煤til.

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


All Articles