Antecedentes
Trabalho como desenvolvedor front-end há um ano. Meu primeiro projeto foi um back-end "inimigo". Acontece que este não é um grande problema quando a comunicação é estabelecida.
Mas no nosso caso não foi assim.
Desenvolvemos o código, que contava com o fato de o back-end nos enviar certos dados, uma certa estrutura e um determinado formato. Enquanto o back-end considerou normal alterar o conteúdo das respostas - sem aviso prévio. Como resultado, levamos horas para determinar por que uma certa parte do site parou de funcionar.
Percebemos que precisávamos verificar o que o back-end retorna antes de confiar nos dados que ele nos enviou. Criamos uma tarefa para pesquisa sobre a questão da validação de dados no front end.
Este estudo foi encomendado a mim.
Fiz uma lista do que quero ser na ferramenta que gostaria de usar para validação de dados.
Os pontos de seleção mais importantes foram os seguintes:
- descrição declarativa (esquema) da validação, que é transformada em uma função validadora que retorna verdadeiro / falso (válido, não válido)
- baixo limiar de entrada;
- semelhança de dados validados com uma descrição da validação;
- facilidade de integração de validações personalizadas;
- facilidade de integração de mensagens de erro personalizadas.
Como resultado, encontrei muitas bibliotecas de validação, depois de revisar o TOP-5 (ajv, joi, roi ...). Eles são todos muito bons. Mas pareceu-me que, para solucionar 5% dos casos complexos - eles condenaram 95% dos casos mais frequentes a serem bastante detalhados e pesados.
Então pensei: por que não desenvolver algo que me convenha?
Quatro meses depois, saiu a sétima versão da minha biblioteca de validação de quartetos .
Era uma versão estável, totalmente testada, com 11k de downloads em npm. Nós o usamos em três projetos em uma campanha por três meses.
Esses três meses tiveram um papel muito útil. O quarteto demonstrou todas as suas vantagens. Não há problemas de dados do back-end. Cada vez que eles mudavam a resposta, imediatamente cometíamos um erro. O tempo gasto na busca das causas dos bugs diminuiu drasticamente. Praticamente não há mais erros de dados.
Mas também foram identificadas falhas.
Portanto, decidi analisá-los e lançar uma nova versão com correções de todos os erros que foram feitos durante o desenvolvimento.
Vou falar sobre esses erros de arquitetura e suas soluções abaixo.
Ancinho arquitetônico
"Stroko" - uma linguagem tipificada do esquema
Vou dar um exemplo da versão antiga do esquema para o objeto da pessoa.
const personSchema = { name: 'string', age: 'number', linkedin: ['string', 'null'] }
Esse esquema valida um objeto com três propriedades: nome - deve ser uma sequência, idade - deve ser um número, um link para uma conta no LinkedIn - deve ser nulo (se não houver uma conta) ou sequência (se houver uma conta).
Esse esquema atende aos meus requisitos de legibilidade, semelhança com dados validados e acho que o limiar de entrada para aprender a escrever esses esquemas não é alto. Além disso, esse esquema pode ser facilmente escrito com uma definição de tipo no texto datilografado:
type Person = { name: string age: number linkedin: string | null }
(Como você pode ver - as mudanças são mais prováveis de cosméticas)
Quando tomei uma decisão, o que deve ser usado para as opções de validação mais frequentes (por exemplo, as usadas acima). Eu escolhi usar - strings, por assim dizer, os nomes dos validadores.
Mas o problema com as strings é que elas não estão disponíveis para o compilador ou analisador de erros. A string 'number' para eles não é muito diferente de 'numder'.
Solução
A nova versão do quarteto 8.0.0. Decidi remover do quarteto - o uso de strings como os nomes dos validadores dentro do esquema.
O diagrama agora se parece com isso:
const personSchema = { name: v.string age: v.number, linkedin: [v.string, null] }
Essa mudança tem duas grandes vantagens:
- compiladores ou analisadores de erros - poderão detectar que o nome do método está escrito com um erro.
- Linhas - não são mais usadas como um elemento do esquema. Isso significa que, para eles, você pode selecionar novas funcionalidades na biblioteca, que serão descritas abaixo.
Suporte TypeScript
Em geral, as sete primeiras versões foram desenvolvidas em Javascript puro. Ao mudar para um projeto com o Typecript, era necessário adaptar a biblioteca de alguma forma. Portanto, as declarações de tipo para a biblioteca foram gravadas.
Mas isso era um sinal de menos - ao adicionar funcionalidade ou ao alterar alguns elementos da biblioteca, sempre era fácil esquecer de atualizar as declarações de tipo.
Havia também apenas pequenos inconvenientes desse tipo:
const checkPerson = v(personSchema)
Quando criamos o validador de objeto na linha (0). Gostaríamos de verificar a resposta real do back-end na linha (1) e lidar com o erro. Na linha (2), para que a person
tipo Pessoa. Mas isso não aconteceu. Infelizmente, essa verificação não era uma guarda de tipo.
Solução
Decidi reescrever a biblioteca inteira do quarteto no Typescript, para que o compilador se dedicasse a verificar a correspondência da biblioteca com os tipos. Ao longo do caminho, adicionamos à função que retorna ao validador compilado um parâmetro de tipo que determinaria qual tipo de proteção é esse tipo de validador.
Um exemplo é assim:
const checkPerson = v<Person>(personSchema)
Agora na linha (2) person
é do tipo Person
.
Legibilidade
Também houve dois casos em que o código foi mal lido: verificação da conformidade com um determinado conjunto de valores (verificação de enumerações) e verificação de outras propriedades do objeto.
a) Verifique enums
Inicialmente, houve uma ideia, na minha opinião, boa. Vamos demonstrá-lo adicionando o campo "gender" ao nosso objeto.
A versão antiga do circuito era assim:
const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum('male', 'female') }
A opção é muito legível. Mas, como sempre, tudo saiu um pouco fora do plano.
Ter uma enumeração declarada no programa, por exemplo, isto:
enum Sex { Male = 'male', Female = 'female' }
Naturalmente, quero usá-lo dentro do circuito. Portanto, ao alterar um dos valores (por exemplo, 'masculino' -> 'm', 'feminino' -> 'f'), o esquema de validação também deve ser alterado.
Portanto, quase sempre a validação enum foi escrita assim:
const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)) }
O que parece bastante volumoso.
b) Validação das propriedades residuais do objeto
Suponha que adicionemos essa característica ao nosso objeto - ele pode ter campos adicionais, mas todos eles devem ser links para redes sociais - isso significa que eles devem ser null
ou ser uma string.
O esquema antigo ficaria assim:
const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)), ...v.rest(['null', 'string'])
Esta entrada destacou as propriedades restantes - daquelas já listadas. É mais provável que o uso de um operador de propagação confunda uma pessoa que deseja entender esse esquema.
Solução
Conforme descrito acima, as strings não fazem mais parte dos esquemas de validação. Apenas três tipos de valores Javascript permaneceram no esquema de validação. Objeto - para descrever o esquema de validação do objeto. Matriz para descrição - várias opções para validade. Função (biblioteca gerada ou personalizada) - para todas as outras opções de validação.
Esta disposição permitiu adicionar funcionalidades, o que permitiu aumentar a legibilidade do circuito muitas vezes.
De fato, e se quisermos comparar o valor com a string 'male'. Realmente precisamos saber mais alguma coisa além do valor em si e da string 'male'.
Portanto, decidiu-se adicionar os valores dos tipos primitivos como um elemento do circuito. Portanto, onde você encontra um valor primitivo no esquema, isso significa que esse é o valor válido que o validador criado de acordo com esse esquema deve verificar. É melhor dar um exemplo:
Se precisarmos verificar o número de igualdade 42-mente. Então escrevemos assim:
const check42 = v(42) check42(42)
Vamos ver como isso afeta o esquema da pessoa (sem levar em conta propriedades adicionais):
const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string],
Usando enums predefinidas, podemos reescrevê-lo assim:
const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex)
Nesse caso, a cerimonialidade desnecessária foi removida na forma de usar o método enum e o operador spread para inserir valores válidos do objeto como parâmetros neste método.
O que é considerado um valor primitivo: números, seqüências de caracteres, caracteres, true
, false
, null
e undefined
.
Ou seja, se precisarmos comparar o valor com eles, simplesmente usaremos esses valores. E uma biblioteca de validação - ele criará um validador que compara estritamente o valor com os especificados no esquema.
Para validar propriedades residuais, foi escolhido usar uma propriedade especial para todos os outros campos do objeto:
const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex), [v.rest]: [null, v.string] }
Assim, o circuito parece mais legível. E mais como anúncios tipográficos.
O validador está relacionado à função que o criou
Nas versões mais antigas, as explicações de erro não faziam parte do validador. Eles foram adicionados a uma matriz dentro da função v
.
Anteriormente, para obter uma explicação dos erros de validação, era necessário ter um validador com você (para verificar) e v (para obter uma explicação da invalidez). Tudo isso parecia da seguinte maneira:
a) Adicionamos explicações ao 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' ) )
Para qualquer elemento do circuito - você pode adicionar uma explicação do erro usando o segundo argumento da função do compilador v.
b) Limpe a matriz de explicações
Antes da validação, era necessário limpar esse conjunto global no qual todas as explicações foram registradas durante a validação.
v.clearContext()
c) Validar
const isPersonValid = checkPerson(person)
Durante essa verificação, se uma validade foi detectada e no estágio de criação do circuito - uma explicação foi dada a ela, essa explicação é colocada no array global v.explanation
.
d) Tratamento de erros
if (!isPersonValid) { throw new TypeError('Invalid person response: ' + v.explanation.join('; ')) }
Como você pode ver aqui, há um grande problema. Porque se queremos usar o validador não no lugar de sua criação. Nós precisaremos passar não apenas os parâmetros, mas também a função que os criou. Porque é nele que a matriz está localizada na qual as explicações serão adicionadas.
Solução
Esse problema foi resolvido da seguinte maneira: as explicações passaram a fazer parte da própria função de validação. O que pode ser entendido por seu tipo:
tipo Validator = (valor: qualquer, explicações?: qualquer []) => booleano
Agora, se você precisar de uma explicação do erro, transmita a matriz na qual deseja adicionar a explicação.
Assim, o validador se torna uma unidade independente. Também foi adicionado um método que pode transformar a função de validação em uma função que retorna nulo se o valor for válido e retorna uma matriz de explicações se o valor não for válido.
Agora a validação com explicações fica assim:
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') })
Posfácio
Eu destaquei três premissas, pelas quais tive que reescrever tudo:
- A esperança de que as pessoas não se enganem ao escrever linhas
- Usando variáveis globais (neste caso, matriz de v.explanation)
- Testes com pequenos exemplos durante o desenvolvimento - não mostraram os problemas que surgem quando usados em casos grandes e reais.
Mas estou feliz por ter realizado uma análise desses problemas e a versão lançada já é usada em nosso projeto. E espero que seja útil para nós não menos que o anterior.
Obrigado a todos pela leitura, espero que minha experiência seja útil para você.