Cómo construir y construir

Antecedentes


Habiéndose reunido en numerosos lugares en el desarrollo de Javascript con situaciones en las que era necesario validar los valores, quedó claro que era necesario resolver este problema de alguna manera. Para este propósito, se estableció la siguiente tarea:
Desarrolle una biblioteca que permita:


  • validar tipos de datos ;
  • establecer valores predeterminados en lugar de campos o elementos no válidos;
  • eliminar partes inválidas de un objeto o matriz;
  • Recibe un mensaje de error

La base de la cual será:


  • Fácil de aprender
  • Legibilidad del código recibido.
  • Facilidad de modificación de código

Para lograr estos objetivos, se ha desarrollado una biblioteca de validación de cuarteto .


Ladrillos básicos de validación


En el corazón de la mayoría de los sistemas que están diseñados para ser aplicables en una amplia gama de tareas están los elementos más simples: acciones, datos y algoritmos. Además de los métodos de su composición , para ensamblar algo más complicado a partir de elementos simples para resolver problemas más complejos.


Validador


La biblioteca de cuarteto se basa en el concepto de un validador . Los validadores en esta biblioteca son funciones de la siguiente forma


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

Hay varias cosas en esta definición que deberían describirse con más detalle:


function(...): boolean - dice que el validador - calcula el resultado de la validación, y el resultado de la validación es un valor booleano - verdadero o falso , respectivamente válido o no válido


value: any : indica que el validador: calcula el resultado de validar un valor , que puede ser cualquier valor de JavaScript. El validador asigna el valor validado a válido o inválido.


{ key: string|int, parent: any }, ... - indica que el valor validado puede estar en diferentes contextos según el nivel de anidamiento del valor. Vamos a mostrarlo con ejemplos.


Valor de ejemplo sin contexto


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

Valor de ejemplo en un contexto de matriz


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

Valor de ejemplo en el contexto de un objeto


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

Dado que las estructuras en un objeto pueden tener un mayor anidamiento, tiene sentido hablar sobre una variedad de contextos


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

Y así sucesivamente.


Sobre la similitud con los métodos de matriz

Esta definición de un validador debería recordarle la definición de funciones que se pasan como argumento a los métodos de matriz, tales como: mapa, filtro, algunos, todos , etc.


  • El primer argumento para estas funciones es un elemento de matriz.
  • El segundo argumento es el índice del elemento.
  • El tercer argumento es la matriz misma.

El validador en este caso es una función más generalizada: toma no solo el índice del elemento en la matriz y la matriz, sino también el índice de la matriz, en su elemento primario y su elemento primario, y así sucesivamente.


¿Qué debemos construir una casa?


Los ladrillos descritos anteriormente no se destacan entre otras "soluciones de piedra" que se encuentran en la "playa" de la muleta de JavaScript. Por lo tanto, construyamos a partir de ellos, algo más coherente e interesante. Para esto tenemos una composición .


¿Cómo construir un rascacielos de validación de objetos?


De acuerdo, sería conveniente validar los objetos de tal manera que la descripción de la validación coincida con la descripción del objeto. Para esto utilizaremos la composición de objetos de validadores . Se ve así:


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

Como puede ver, a partir de diferentes ladrillos de validación definidos para campos específicos, podemos ensamblar un validador de objetos, un "pequeño edificio" en el que todavía es bastante estrecho, pero mejor que sin él. Para esto, utilizamos el compositor de validadores v . Cada vez, al encontrar el objeto literal v en el lugar del validador, lo considerará como una composición de objeto, convirtiéndolo en un validador de objetos de acuerdo con sus campos.


A veces no podemos describir todos los campos . Por ejemplo, cuando un objeto es un diccionario de datos:


 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 

¿Cómo reutilizar las soluciones de construcción?


Como vimos anteriormente, existe la necesidad de reutilizar validadores simples. En estos ejemplos, ya teníamos que usar el "validador de tipo de cadena" dos veces.


Para acortar el registro y aumentar su legibilidad, la biblioteca de cuarteto utiliza sinónimos de validadores de cadena. Cada vez que un validador compositor encuentra una cadena en el lugar donde debería estar el validador, busca en el diccionario su validador y lo utiliza .


Por defecto, los validadores más comunes ya están definidos en la biblioteca.


Considere los siguientes ejemplos:


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

y muchos otros descritos en la documentación .


Cada arco tiene su propio tipo de ladrillos?


El validador compositor (función v ) también es una fábrica de validadores. En el sentido de que contiene muchos métodos útiles que devuelven


  • validadores de funciones
  • valores que el compositor percibirá como esquemas para crear validadores

Por ejemplo, veamos la validación de la matriz: la mayoría de las veces consiste en verificar el tipo de matriz y verificar todos sus elementos. Usaremos el v.arrayOf(elementValidator) para esto. Por ejemplo, tome una matriz de puntos con nombres.


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

Dado que una matriz de puntos es una matriz de objetos, tiene sentido usar la composición de objetos para validar los elementos de la matriz.


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

Ahora, utilizando el método de fábrica v.arrayOf , cree un validador para toda la matriz.


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

Veamos cómo funciona este validador:


 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 

Este es solo uno de los métodos de fábrica, cada uno de los cuales se describe en la documentación.


Como viste anteriormente, v.rest también v.rest un método de fábrica que devuelve una composición de objeto que verifica todos los campos no especificados en la composición del objeto. Eso significa que se puede incrustar en otra composición de objeto utilizando el spread-operator .


Citemos como ejemplo el uso de varios de ellos:


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

¿Ser o no ser?


A menudo sucede que los datos válidos toman varias formas, por ejemplo:


  • id puede ser un número o puede ser una cadena.
  • El objeto de point puede o no contener algunas coordenadas, dependiendo de la dimensión.
  • Y muchos otros casos.

Para organizar la validación de variantes, se proporciona un tipo separado de composición: composición de variantes. Está representado por una serie de validadores de posibles opciones. Un objeto se considera válido cuando al menos uno de los validadores informa su validez.


Considere un ejemplo con validación de identificador:


  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 

Ejemplo de validación de punto:


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

Por lo tanto, cada vez que un compositor ve una matriz, la considerará una composición de los elementos validadores de esta matriz de tal manera que cuando uno de ellos considere que el valor es válido, el cálculo de la validación se detendrá y el valor será reconocido como válido.


Como vemos, el compositor considera no solo la función de validación como un validador, sino también todo lo que puede conducir a una función de validación.


Tipo de validadorEjemploSegún lo percibido por el compositor
función de validaciónx => typeof x === 'bigint'acabo de invocar los valores necesarios
composición de objetos{ a: 'number' }crea una función de validación para un objeto basada en los validadores de campo especificados
Composición variante['number', 'string']Crea una función de validación para validar un valor con al menos una de las opciones
Resultados de llamadas al método de fábricav.enum('male', 'female')La mayoría de los métodos de fábrica devuelven funciones de validación (con la excepción de v.rest , que devuelve la composición del objeto), por lo que se tratan como funciones de validación regulares

Todas estas opciones del validador son válidas y se pueden usar en cualquier lugar dentro del esquema donde debería estar el validador.


Como resultado, el esquema de operación siempre es así: v(schema) devuelve una función de validación. A continuación, esta función de validación se llama a valores específicos:
v(schema)(value[, ...parents])


¿Has tenido algún accidente en el sitio de construcción?


- Todavía no, ni uno
- ¡Lo harán!


Ocurre que los datos no son válidos y necesitamos poder determinar la causa del no válido.


Para esto, la biblioteca de cuarteto proporciona un mecanismo de explicación . Consiste en el hecho de que, en el caso de que el validador, ya sea interno o externo, detecte la validez de los datos verificados, debe enviar una nota explicativa .


Para estos fines, v el segundo argumento del compositor de validadores v . Agrega el efecto secundario de enviar una nota explicativa a la matriz v.explanation en caso de datos no válidos.


Ejemplo, validemos una matriz y queramos averiguar los números de todos los elementos que no son válidos y su valor:


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

Como puede ver, la elección de la explicación depende de la tarea. A veces ni siquiera es necesario.


A veces necesitamos hacer algo con campos no válidos. En tales casos, tiene sentido usar el nombre del campo no válido como explicación :


 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 } 

Con este mecanismo de explicación, puede implementar cualquier comportamiento asociado con los resultados de la validación.


Una explicación puede ser cualquier cosa:


  • un objeto que contiene la información necesaria;
  • función que corrige el error. ( getExplanation => function(invalid): valid );
  • nombre del campo no válido o índice del elemento no válido;
  • código de error
  • y todo eso es suficiente para tu imaginación.

¿Qué hacer cuando las cosas no se están construyendo?


Corregir errores de validación no es una tarea rara. Para estos fines, la biblioteca utiliza validadores con un efecto secundario que recuerda el lugar del error y cómo solucionarlo.


  • v.default(validator, value) : devuelve un validador que recuerda un valor no válido y, en el momento de llamar a v.fix , establece el valor predeterminado
  • v.filter(validator) : devuelve un validador que recuerda un valor no válido y, en el momento de llamar a v.fix , elimina este valor del padre
  • v.addFix(validator, fixFunc) : devuelve un validador que recuerda un valor no válido y, en el momento de llamar a v.fix , llama a fixFunc con parámetros (valor, {clave, padre}, ...). fixFunc - debe mutar a uno de los socios - para cambiar el valor

 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 } 

Las tareas aún son útiles.


También hay métodos de utilidad para acciones de validación en esta biblioteca:


MétodoResultado
v.throwErrorSi no es válido, arroja un TypeError con el mensaje dado.
v.omitInvalidItemsDevuelve una nueva matriz (u objeto de diccionario) sin elementos no válidos (campos).
v.omitInvalidPropsDevuelve un nuevo objeto sin campos no válidos, de acuerdo con el validador de objeto especificado.
v.validOrDevuelve el valor si es válido; de lo contrario, lo reemplaza con el valor predeterminado especificado.
v.exampleComprueba si los valores dados se ajustan al esquema. Si no encajan, se produce un error. Sirve como documentación y pruebas de circuito.

Resultados


Las tareas se resolvieron de las siguientes maneras:


DesafíoSolución
Validación de tipo de datosValidadores con nombre predeterminados.
Valores por defectov.default
Eliminar partes inválidasv.filter , v.omitInvalidItems y v.omitInvalidProps .
Fácil de aprenderValidadores simples, formas simples de componerlos en validadores complejos.
Legibilidad de códigoUno de los objetivos de la biblioteca era comparar los esquemas de validación.
objetos validados
Facilidad de modificaciónHabiendo dominado los elementos de las composiciones y usando sus propias funciones de validación, cambiar el código es bastante simple.
Mensaje de errorExplicación en forma de mensaje de error. O cálculo de código de error basado en explicaciones.

Epílogo


Esta solución fue diseñada para crear rápida y convenientemente funciones de validación con la capacidad de incrustar funciones de validación personalizadas. Por lo tanto, si hay alguna, cualquier corrección, crítica, opciones de mejora de quienes leen este artículo son bienvenidas. Gracias por su atencion

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


All Articles