I'm having some trouble understanding the validation library, io.vavr.control.Validation
. At the risk of asking too broad a question, I do have several sub-questions—however I believe they are closely related and would piece together to help me understand the proper way to use this validation mechanism.
I started with the example here: https://softwaremill.com/javaslang-data-validation.
Validation<String, ValidRegistrationRequest> validate(RegistrationRequest request) {
return combine(
validateCardId(request.getCardId()),
validateTicketType(request.getTicketType()),
validateGuestId(request.getGuestId())
)
.ap(ValidRegistrationRequest::new)
.mapError(this::errorsAsJson);
}
private Validation<String, Card> validateCardId(String cardId) {
// validate cardId
// if correct then return an instance of entity the cardId corresponds to
}
private Validation<String, TicketType> validateTicketType(String ticketType) {
// validate ticketType
// if known then return enumeration representing the ticket
}
private Validation<String, Guest> validateGuest(String guestId) {
// validate guestId
// if correct then return an instance of entity the questId corresponds to
}
At first, I didn't understand where the generic parameters for Validation<String, ValidRegistrationRequest>
came from. I now understand that they are linked to the return types of the methods passed to mapError
and ap
, respectively. But:
How does combine
know to return Validation<String, ValidRegistrationRequest>
? I feel the only way this is possible, is if combine
is actually a Validation<String, ValidRegistrationRequest>::combine
, so that the ap
and mapError
are defined from this template. But I don't believe that the compiler should be able to imply that that combine
refers to a static implementation in the class of the return type. What's happening here?
[Minor] What is the use case for using a ValidRegistrationRequest
as opposed to just RegistrationRequest
again? I'm tempted to do the latter in my coding, until I see an example.
A second example I was reading about is here: http://www.vavr.io/vavr-docs/#_validation.
class PersonValidator {
private static final String VALID_NAME_CHARS = "[a-zA-Z ]";
private static final int MIN_AGE = 0;
public Validation<Seq<String>, Person> validatePerson(String name, int age) {
return Validation.combine(validateName(name), validateAge(age)).ap(Person::new);
}
private Validation<String, String> validateName(String name) {
return CharSeq.of(name).replaceAll(VALID_NAME_CHARS, "").transform(seq -> seq.isEmpty()
? Validation.valid(name)
: Validation.invalid("Name contains invalid characters: '"
+ seq.distinct().sorted() + "'"));
}
private Validation<String, Integer> validateAge(int age) {
return age < MIN_AGE
? Validation.invalid("Age must be at least " + MIN_AGE)
: Validation.valid(age);
}
}
Where did Seq
come from? Is that the default when no mapError
is supplied? But I'm looking at the decompiled .class file for Validation.class, and the only reference to Seq
is here:
static <E, T> Validation<List<E>, Seq<T>> sequence(Iterable<? extends Validation<List<E>, T>> values) {
Objects.requireNonNull(values, "values is null");
List<E> errors = List.empty();
List<T> list = List.empty();
Iterator var3 = values.iterator();
while(var3.hasNext()) {
Validation<List<E>, T> value = (Validation)var3.next();
if (value.isInvalid()) {
errors = errors.prependAll(((List)value.getError()).reverse());
} else if (errors.isEmpty()) {
list = list.prepend(value.get());
}
}
return errors.isEmpty() ? valid(list.reverse()) : invalid(errors.reverse());
}
Which, I don't think is relevant. Perhaps I'm using an outdated Validation
? (It is after all javaslang.control.Validation
in my imports, not io.vavr.control.Validation
.)
I had this question for both examples: How does combine
know which parameters to pass to the constructor (ap
), and in what order? Is the answer, "All its parameters, in the order given"?
Thanks in advance.
You have the same questions and doubts I had when was looking for the first time into validation mechanism of Vavr.
Here are my responses to the first two questions:
combine(...)
method returns with an instance of a validation builder, in this case, this is a Builder3
class holding three results of validate*(...)
functions. The ap(...)
method is a method of this builder and triggers building of Validation
instance.When it is called, validation results are applied, one by one, to a curried version of a function provided as an argument:
v3.ap(v2.ap(v1.ap(Validation.valid(f.curried()))))
In the example, f
is a constructor of ValidRegistrationRequest
class. In the end, we have a validation holding the valid request instance.
On the other hand, if any of the results are invalid, the method creates an invalid result with a list of error messages. And calling mapError(this::errorsAsJson)
(on Validation
instance this time!) transforms it into a JSON format.
ValidRegistrationRequest
?I have used Vavr's validation in one of my projects. I had a request coming with some identifiers of entities. To validate the correctness of it, I had to query a database to check whether there is something for each id.
So, if validation returned with the original request, I would have to fetch those objects from the database once again. Thus, I decided to return ValidRegistrationRequest
holding domain objects. With calling database once only, request processing is significantly faster.
And answers to the second pair of questions:
Validation.combine(...).ap(...)
returns with an instance of Invalid
class, holding a list of error messages, returned from validation methods.If you look into sources, to Validation.ap(...)
method, you can see that invalid results are gathered into a Vavr's List
. Because it inherits from Seq
, you can see this type in the validatePerson
example Seq<String>
.
The order of arguments in combine
must be the same as the order of arguments taken by the function provided to ap(...)
method.
With sources downloaded, it is way easier to track internals of Vavr.