Introdução à API de validação do Vavr
1. Visão geral
A validação é uma tarefa que ocorre com freqüência em aplicativos Java e, portanto, muito esforço foi colocado no desenvolvimento de bibliotecas de validação.
Vavr (anteriormente conhecido como Javaslang) fornece umvalidation API completo. Ele nos permite validar dados de maneira direta, usando um estilo de programação funcional para objetos. Se você quiser dar uma olhada no que esta biblioteca oferece fora da caixa, sinta-se à vontade para verificarthis article.
Neste tutorial, damos uma olhada em profundidade na API de validação da biblioteca e aprendemos como usar seus métodos mais relevantes.
2. A interfaceValidation
A interface de validação do Vavr é baseada em um conceito de programação funcional conhecido comoapplicative functor. Ele executa uma sequência de funções enquanto acumula os resultados, mesmo que algumas ou todas essas funções falhem durante a cadeia de execução.
O functor aplicativo da biblioteca é construído sobre os implementadores de sua interfaceValidation. Essa interface fornece métodos para acumular erros de validação e dados validados, permitindo, portanto, processar os dois como um lote.
3. Validando a entrada do usuário
A validação da entrada do usuário (por exemplo, dados coletados de uma camada da web) é fácil usando a API de validação, pois tudo se resume à criação de uma classe de validação personalizada que valida os dados enquanto acumula erros resultantes, se houver.
Vamos validar o nome e o e-mail de um usuário, que foram enviados por meio de um formulário de login. Primeiro, precisamos incluirVavr’s Maven artifact no arquivopom.xml:
io.vavr
vavr
0.9.0
A seguir, vamos criar uma classe de domínio que modela objetos de usuário:
public class User {
private String name;
private String email;
// standard constructors, setters and getters, toString
}
Finalmente, vamos definir nosso validador personalizado:
public class UserValidator {
private static final String NAME_PATTERN = ...
private static final String NAME_ERROR = ...
private static final String EMAIL_PATTERN = ...
private static final String EMAIL_ERROR = ...
public Validation, User> validateUser(
String name, String email) {
return Validation
.combine(
validateField(name, NAME_PATTERN, NAME_ERROR),
validateField(email, EMAIL_PATTERN, EMAIL_ERROR))
.ap(User::new);
}
private Validation validateField
(String field, String pattern, String error) {
return CharSeq.of(field)
.replaceAll(pattern, "")
.transform(seq -> seq.isEmpty()
? Validation.valid(field)
: Validation.invalid(error));
}
}
A classeUserValidator valida o nome fornecido e o e-mail individualmente com o métodovalidateField(). Nesse caso, esse método executa uma correspondência típica de padrão com base em expressões regulares.
A essência neste exemplo é o uso dos métodosvalid(),invalid()ecombine().
4. Os métodosvalid(),invalid() ecombine()
Se o nome e o e-mail fornecidos corresponderem às expressões regulares fornecidas, o métodovalidateField() chamavalid(). Este método retorna uma instância deValidation.Valid. Por outro lado, se os valores forem inválidos, o método da contraparteinvalid() retorna uma instância deValidation.Invalid.
Este mecanismo simples, baseado na criação de diferentes instâncias deValidation dependendo dos resultados da validação, deve nos dar pelo menos uma ideia básica sobre como processar os resultados (mais sobre isso na seção 5).
A faceta mais relevante do processo de validação é o métodocombine(). Internamente, este método usa a classeValidation.Builder, que permite combinar até 8 instâncias diferentes deValidation que podem ser calculadas com métodos diferentes:
static Builder combine(
Validation validation1, Validation validation2) {
Objects.requireNonNull(validation1, "validation1 is null");
Objects.requireNonNull(validation2, "validation2 is null");
return new Builder<>(validation1, validation2);
}
A classeValidation.Builder mais simples leva duas instâncias de validação:
final class Builder {
private Validation v1;
private Validation v2;
// standard constructors
public Validation, R> ap(Function2 f) {
return v2.ap(v1.ap(Validation.valid(f.curried())));
}
public Builder3 combine(
Validation v3) {
return new Builder3<>(v1, v2, v3);
}
}
Validation.Builder, junto com o métodoap(Function), retorna um único resultado com os resultados da validação. Se todos os resultados forem válidos, o métodoap(Function) mapeia os resultados em um único valor. Este valor é armazenado em uma instânciaValid usando a função especificada em sua assinatura.
Em nosso exemplo, se o nome e o email fornecidos forem válidos, um novo objetoUser é criado. Obviamente, é possível fazer algo totalmente diferente com um resultado válido, ou seja, armazená-lo em um banco de dados, enviá-lo por email e assim por diante.
5. Processando resultados de validação
É muito fácil implementar diferentes mecanismos para processar os resultados da validação. Mas como validamos os dados em primeiro lugar? Nessa medida, usamos a classeUserValidator:
UserValidator userValidator = new UserValidator();
Validation, User> validation = userValidator
.validateUser("John", "[email protected]");
Depois que uma instância deValidation é obtida, podemos aproveitar a flexibilidade da API de validação e os resultados do processo de várias maneiras.
Vamos elaborar as abordagens mais comumente encontradas.
5.1. As instânciasValideInvalid
Essa abordagem é de longe a mais simples. Consiste em verificar os resultados da validação com as instânciasValideInvalid:
@Test
public void
givenInvalidUserParams_whenValidated_thenInvalidInstance() {
assertThat(
userValidator.validateUser(" ", "no-email"),
instanceOf(Invalid.class));
}
@Test
public void
givenValidUserParams_whenValidated_thenValidInstance() {
assertThat(
userValidator.validateUser("John", "[email protected]"),
instanceOf(Valid.class));
}
Em vez de verificar a validade dos resultados com as instânciasValideInvalid, devemos apenas ir um passo adiante e usar os métodosisValid()eisInvalid().
5.2. As APIsisValid() eisInvalid()
Usar o tandemisValid() /isInvalid() é análogo à abordagem anterior, com a diferença de que esses métodos retornamtrue oufalse, dependendo dos resultados da validação:
@Test
public void
givenInvalidUserParams_whenValidated_thenIsInvalidIsTrue() {
assertTrue(userValidator
.validateUser("John", "no-email")
.isInvalid());
}
@Test
public void
givenValidUserParams_whenValidated_thenIsValidMethodIsTrue() {
assertTrue(userValidator
.validateUser("John", "[email protected]")
.isValid());
}
A instânciaInvalid contém todos os erros de validação. Eles podem ser obtidos com o métodogetError():
@Test
public void
givenInValidUserParams_withGetErrorMethod_thenGetErrorMessages() {
assertEquals(
"Name contains invalid characters, Email must be a well-formed email address",
userValidator.validateUser("John", "no-email")
.getError()
.intersperse(", ")
.fold("", String::concat));
}
Por outro lado, se os resultados forem válidos, uma instânciaUser pode ser capturada com o métodoget():
@Test
public void
givenValidUserParams_withGetMethod_thenGetUserInstance() {
assertThat(userValidator.validateUser("John", "[email protected]")
.get(), instanceOf(User.class));
}
Essa abordagem funciona conforme o esperado, mas o código ainda parece bem detalhado e demorado. Podemos compactá-lo ainda mais usando o métodotoEither().
5.3. A APItoEither()
O métodotoEither() constrói instânciasLefteRight da interfaceEither. Essa interface complementar possui vários métodos de conveniência que podem ser usados para reduzir o processamento dos resultados da validação.
Se os resultados forem válidos, o resultado é armazenado na instânciaRight. Em nosso exemplo, isso equivaleria a um objetoUser válido. Por outro lado, se os resultados forem inválidos, os erros serão armazenados na instânciaLeft:
@Test
public void
givenValidUserParams_withtoEitherMethod_thenRightInstance() {
assertThat(userValidator.validateUser("John", "[email protected]")
.toEither(), instanceOf(Right.class));
}
O código agora parece muito mais conciso e simplificado. Mas ainda não terminamos. A interfaceValidation fornece o métodofold(), que aplica uma função personalizada que se aplica a resultados válidos e outra aos inválidos.
5.4. A APIfold()
Vamos ver como usar o métodofold() para processar os resultados da validação:
@Test
public void
givenValidUserParams_withFoldMethod_thenEqualstoParamsLength() {
assertEquals(2, (int) userValidator.validateUser(" ", " ")
.fold(Seq::length, User::hashCode));
}
O uso defold() reduz o processamento dos resultados da validação a apenas uma linha.
Vale ressaltar que os tipos de retorno das funções passados como argumentos para o método devem ser os mesmos. Além disso, as funções devem ser suportadas pelos parâmetros de tipo definidos na classe de validação, ou seja,Seq<String>eUser.
6. Conclusão
Neste artigo, exploramos em profundidade a API de validação do Vavr e aprendemos como usar alguns de seus métodos mais relevantes. Para uma lista completa, verifique oofficial docs API.
O controle de validação do Vavr fornece uma alternativa muito atraente para implementações mais tradicionais deJava Beans Validation,, comoHibernate Validator.
Como de costume, todos os exemplos mostrados no artigo estão disponíveisover on GitHub.