Como construir e construir

Antecedentes


Tendo encontrado em vários locais do desenvolvimento Javascript situações em que era necessário validar valores, ficou claro que era necessário resolver de alguma forma esse problema. Para esse fim, a seguinte tarefa foi definida:
Desenvolva uma biblioteca que permita:


  • validar tipos de dados ;
  • defina valores padrão em vez de campos ou elementos inválidos;
  • excluir partes inválidas de um objeto ou matriz;
  • Receba uma mensagem de erro

A base será:


  • Fácil de aprender
  • Legibilidade do código recebido.
  • Facilidade de modificação de código

Para atingir esses objetivos, uma biblioteca de validação de quarteto foi desenvolvida.


Tijolos de validação básica


No coração da maioria dos sistemas projetados para serem aplicáveis ​​a uma ampla gama de tarefas estão os elementos mais simples: ações, dados e algoritmos. Bem como métodos de composição - para montar algo mais complicado a partir de elementos simples para resolver problemas mais complexos.


Validador


A biblioteca do quarteto é baseada no conceito de um validador . Os validadores nesta biblioteca são funções do seguinte formato


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

Há várias coisas nesta definição que devem ser descritas em mais detalhes:


function(...): boolean - diz que o validador - calcula o resultado da validação e o resultado da validação é um valor booleano - verdadeiro ou falso , respectivamente válido ou inválido


value: any - indica que o validador - calcula o resultado da validação de um valor , que pode ser qualquer valor javascript. O validador atribui o valor validado a válido ou inválido.


{ key: string|int, parent: any }, ... - indica que o valor validado pode estar em contextos diferentes, dependendo do nível de aninhamento do valor. Vamos mostrar com exemplos


Valor de exemplo sem qualquer contexto


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

Valor de exemplo em um 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 exemplo no contexto de um 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 } }) 

Como estruturas em um objeto podem ter um aninhamento maior, faz sentido falar sobre uma variedade 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 }] } ) 

E assim por diante


Sobre a semelhança com métodos de matriz

Essa definição de validador deve lembrá-lo da definição de funções que são passadas como argumento para métodos de matriz, como: map, filter, some, every e assim por diante.


  • O primeiro argumento para essas funções é um elemento de matriz.
  • O segundo argumento é o índice do elemento.
  • O terceiro argumento é a própria matriz.

O validador, neste caso, é uma função mais generalizada - ele pega não apenas o índice do elemento na matriz e na matriz, mas também o índice da matriz - em seu pai e pai, e assim por diante.


O que devemos construir uma casa?


Os tijolos descritos acima não se destacam entre outras "soluções de pedra" que estão na "praia" da muleta javascript. Portanto, vamos construir a partir deles, algo mais coerente e interessante. Para isso, temos uma composição .


Como construir um arranha-céu de validação de objeto?


Concordo, seria conveniente validar objetos de tal maneira que a descrição da validação corresponda à descrição do objeto. Para isso, usaremos a composição de objetos dos validadores . É assim:


 //    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 você pode ver, a partir de diferentes blocos de validadores definidos para campos específicos, podemos montar um validador de objetos - algum "pequeno prédio", que ainda está bastante cheio - mas melhor do que sem ele. Para isso, usamos o compositor de validadores v . A cada vez, ao encontrar o literal do objeto v no lugar do validador, ele será considerado uma composição de objeto, transformando-o em um validador de objeto em seus campos.


Às vezes, não podemos descrever todos os campos . Por exemplo, quando um objeto é um dicionário de dados:


 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 

Como reutilizar soluções de construção?


Como vimos acima, é necessário reutilizar validadores simples. Nestes exemplos, já tivemos que usar o "validador de tipo de string" duas vezes.


Para encurtar o registro e aumentar sua legibilidade, a biblioteca do quarteto usa sinônimos de string de validadores. Sempre que um compositor de validador encontra uma sequência no local em que o validador deve estar, ele pesquisa seu validador no dicionário e o usa .


Por padrão, os validadores mais comuns já estão definidos na biblioteca.


Considere os seguintes exemplos:


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

e muitos outros descritos na documentação .


Cada arco tem seu próprio tipo de tijolos?


O compositor validador (função v ) também é uma fábrica de validadores. No sentido de que ele contém muitos métodos úteis que retornam


  • validadores de função
  • valores que o compositor perceberá como esquemas para criar validadores

Por exemplo, vejamos a validação da matriz: na maioria das vezes consiste em verificar o tipo da matriz e verificar todos os seus elementos. Usaremos o v.arrayOf(elementValidator) para isso. Por exemplo, considere uma matriz de pontos com nomes.


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

Como uma matriz de pontos é uma matriz de objetos, faz sentido usar a composição de objetos para validar os elementos da matriz.


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

Agora, usando o método de fábrica v.arrayOf , crie um validador para toda a matriz.


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

Vamos ver como esse validador funciona:


 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 é apenas um dos métodos de fábrica, cada um dos quais é descrito na documentação.


Como você viu acima, v.rest também v.rest um método de fábrica que retorna uma composição de objeto que verifica todos os campos não especificados na composição do objeto. Isso significa que ele pode ser incorporado em outra composição de objeto usando o spread-operator .


Vamos citar como exemplo o uso de vários deles:


 //    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 ou não ser?


Muitas vezes acontece que dados válidos assumem várias formas, por exemplo:


  • id pode ser um número ou uma string.
  • O objeto de point pode ou não conter algumas coordenadas, dependendo da dimensão.
  • E muitos outros casos.

Para organizar a validação de variantes, é fornecido um tipo separado de composição - composição de variantes. É representado por uma matriz de validadores de opções possíveis. Um objeto é considerado válido quando pelo menos um dos validadores relata sua validade.


Considere um exemplo com validação 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 

Exemplo de validação de ponto:


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

Assim, sempre que um compositor vê uma matriz, ele a considera uma composição dos elementos validadores dessa matriz, de modo que, quando um deles considera o valor válido, o cálculo da validação para e o valor é reconhecido como válido.


Como vemos, o compositor considera não apenas a função validadora como validadora, mas também tudo o que pode levar a uma função validadora.


Tipo de validadorExemploComo percebido pelo compositor
função de validaçãox => typeof x === 'bigint'apenas chamou os valores necessários
composição do objeto{ a: 'number' }cria uma função validadora para um objeto com base nos validadores de campo especificados
Composição das variantes['number', 'string']Cria uma função validadora para validar um valor com pelo menos uma das opções
Resultados da chamada do método de fábricav.enum('male', 'female')A maioria dos métodos de fábrica retorna funções de validação (com exceção do v.rest , que retorna a composição do objeto), portanto, eles são tratados como funções de validação regulares

Todas essas opções do validador são válidas e podem ser usadas em qualquer lugar do esquema onde o validador deve estar.


Como resultado, o esquema de trabalho é sempre assim: v(schema) retorna uma função de validação. Em seguida, esta função de validação é chamada em valores específicos:
v(schema)(value[, ...parents])


Você já teve algum acidente no canteiro de obras?


- Ainda não, nem um
- Eles vão!


Acontece que os dados são inválidos e precisamos ser capazes de determinar a causa do inválido.


Para isso, a biblioteca de quartetos fornece um mecanismo de explicação . Consiste no fato de que, no caso em que o validador, interno ou externo, detecta a validade dos dados verificados, deve enviar uma nota explicativa .


Para esses fins, v o segundo argumento do compositor de validadores v . Ele adiciona o efeito colateral de enviar uma nota explicativa à matriz v.explanation em caso de dados inválidos.


Por exemplo, vamos validar uma matriz e queremos descobrir os números de todos os elementos inválidos e seu 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 você pode ver, a escolha da explicação depende da tarefa. Às vezes nem é necessário.


Às vezes, precisamos fazer algo com campos inválidos. Nesses casos, faz sentido usar o nome do campo inválido como uma explicação :


 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 } 

Com esse mecanismo de explicação, você pode implementar qualquer comportamento associado aos resultados da validação.


Uma explicação pode ser qualquer coisa:


  • um objeto contendo as informações necessárias;
  • função que corrige o erro. ( getExplanation => function(invalid): valid );
  • nome do campo inválido ou índice do elemento inválido;
  • código de erro
  • e tudo o que é suficiente para a sua imaginação.

O que fazer quando as coisas não estão sendo construídas?


Corrigir erros de validação não é uma tarefa rara. Para esses fins, a biblioteca usa validadores com um efeito colateral que lembra o local do erro e como corrigi-lo.


  • v.default(validator, value) - retorna um validador que lembra um valor inválido e, no momento da chamada de v.fix - define o valor padrão
  • v.filter(validator) - retorna um validador que lembra um valor inválido e, no momento da chamada de v.fix - remove esse valor do pai
  • v.addFix(validator, fixFunc) - retorna um validador que lembra um valor inválido e, no momento da chamada de v.fix - chama fixFunc com parâmetros (valor, {key, parent}, ...). fixFunc - deve alterar um dos parceiros - para alterar o 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 } 

As tarefas ainda são úteis


Também existem métodos utilitários para ações de validação nesta biblioteca:


MétodoResultado
v.throwErrorSe inválido, lança um TypeError com a mensagem fornecida.
v.omitInvalidItemsRetorna uma nova matriz (ou objeto de dicionário) sem elementos inválidos (campos).
v.omitInvalidPropsRetorna um novo objeto sem campos inválidos, de acordo com o validador de objeto especificado.
v.validOrRetorna o valor se for válido, caso contrário, ele será substituído pelo valor padrão especificado.
v.exampleVerifica se os valores fornecidos se ajustam ao esquema. Se eles não couberem, um erro será gerado. Serve como documentação e teste de circuitos

Resultados


As tarefas foram resolvidas das seguintes maneiras:


DesafioSolução
Validação do tipo de dadosValidadores nomeados padrão.
Valores padrãov.default
Remoção de peças inválidasv.filter , v.omitInvalidItems e v.omitInvalidProps .
Fácil de aprenderValidadores simples, maneiras simples de compor em validadores complexos.
Legibilidade do códigoUm dos objetivos da biblioteca era comparar os próprios esquemas de validação
objetos validados.
Facilidade de modificaçãoTendo dominado os elementos das composições e usando suas próprias funções de validação - alterar o código é bastante simples.
Mensagem de erroExplicação na forma de uma mensagem de erro. Ou cálculo de código de erro com base em explicações.

Posfácio


Esta solução foi projetada para criar rápida e convenientemente funções de validação com a capacidade de incorporar funções de validação personalizadas. Portanto, se houver, quaisquer correções, críticas e opções de melhoria daqueles que lêem este artigo são bem-vindas. Obrigado pela atenção.

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


All Articles