Pitch: govalid: validação de structs em Go sem transformar tags em uma mini linguagem
Recentemente comecei a desenvolver o govalid, uma biblioteca de validação para Go que nasceu principalmente de uma preferência pessoal que fui criando enquanto estudava outras soluções: eu não queria precisar aprender mais um pequeno "dialeto" dentro das tags das structs.
Em muitas bibliotecas, é comum encontrar algo parecido com:
type User struct {
Name string `validate:"required,min=3"`
Email string `validate:"required,email"`
Age uint `validate:"gte=18,lte=130"`
}
Não há necessariamente algo errado com essa abordagem. Ela é compacta, bastante utilizada e existem bibliotecas maduras seguindo esse modelo.
O que começou a me incomodar é que, conforme as validações crescem, também cresce a quantidade de regras e sintaxes que preciso conhecer dentro de uma string.
Minha ideia com o govalid foi experimentar outra abordagem: manter as validações como código Go explícito.
Por exemplo:
err := govalid.New().Validate(
user,
govalid.Field(
"Name",
govalid.Required(),
govalid.StringMinLength(3),
),
govalid.Field(
"Age",
govalid.IntBetween(18, 130),
),
govalid.Field(
"Profile.Email",
govalid.StringEmail(),
),
)
As regras são funções Go e podem ser compostas:
govalid.Field(
"Scores",
govalid.Optional(
govalid.CollectionEach(
govalid.IntBetween(0, 100),
),
),
)
Além das regras prontas, também é possível escrever uma Rule normalmente:
isSlug := govalid.Rule(func(ctx govalid.RuleContext) error {
value, ok := ctx.ValueAny().(string)
if !ok || value == "" {
return errors.New("should be a slug")
}
return nil
})
Atualmente existem regras para strings, inteiros com e sem sinal, floats, booleanos, bytes, maps e collections, além de validações condicionais.
Outro ponto que tentei manter foi a composição. Por exemplo, uma regra de inteiro pode ser utilizada diretamente em um campo ou aplicada aos elementos de uma collection.
Por que não tags?
A intenção não é dizer que validação por tags é ruim.
O objetivo é oferecer uma alternativa para quem, assim como eu, prefere escrever:
govalid.IntBetween(18, 130)
em vez de precisar lembrar qual seria a representação equivalente dentro de uma tag.
Isso tem um custo: as validações ficam mais verbosas.
Por outro lado, elas ficam explícitas no código, navegáveis pela IDE e podem ser compostas utilizando Go normalmente.
E para projetos maiores?
Essa é uma parte em que ainda tenho dúvidas.
Hoje vejo o projeto principalmente como uma alternativa para aplicações pequenas e médias. A biblioteca já possui testes com estruturas e payloads relativamente grandes, mas isso é diferente de afirmar que ela está preparada para todos os requisitos encontrados em sistemas grandes.
Inclusive, esse é um dos motivos de estar compartilhando o projeto.
Quero entender quais problemas essa abordagem começaria a encontrar conforme a aplicação cresce e quais casos de uso ainda não estou considerando.
GitHub:
https://github.com/rickferrdev/govalid
Quem trabalha com validação em Go: vocês preferem manter essas regras explícitas em código ou acham que a concisão das tags compensa ter essa sintaxe adicional?