javavavr

vavr return from loop if condition fails


I am writing a code to validate category using vavr

private static Validation<ConstraintViolation, List<Category>> isCategoryValid(
        List<Category> categories) {

    java.util.List<Category> categoryList = new ArrayList<>();

    for (Category category : categories) {

        List<Product> detailsRequest = validateList(category.getDetails());

        if (detailsRequest .isEmpty()) {
            return invalid(new ConstraintViolation("Details", "Details cannot be empty"));
        }

        ...more validations
    
        categoryList.add(WCategory.builder().details(List.ofAll(detailsList))
                .type(category.getType()).build());
    }
    return valid(List.ofAll(categoryList));
}

I have to use java.util.List as I cannot achieve it using vavr itself. If I use

categories.map(x -> x..);

I cannot return from the loop if validation fails and will get the output as List<Validations<ConstraintViolation, List<Category>>> which is not what I wanted.

EDIT:

private static Validation<RuntimeException, List<String>> isCategoryValid(
        List<String> categories) {

    java.util.List<String> categoryList = new ArrayList<>();

    for (String category : categories) {

        String detailsRequest = validateList(category);

        if (detailsRequest != "") {
            return invalid(new RuntimeException("Details cannot be empty"));
        }

        ...more validations
    
        categoryList.add(detailsRequest);
    }
    return valid(List.ofAll(categoryList));
}

Solution

  • It depends a bit on the behaviour you'd like to achieve. If you only want to get the failed validation for the first category in the list, which seems to be the case, you could use Either.traverseRight and then convert that to a validation. The traverseRight will only keep the first failed entry or the list of succesful things.

    So the code could look like this:

    private static Validation<RuntimeException, List<String>> isCategoryValid(List<String> categories) {
        return Validation.fromEither(
                Either.traverseRight(categories, Example::validateCategory).map(Seq::toList)
        );
    }
    
    private static Either<RuntimeException, String> validateCategory(String category) {
        // ...more validations
        return category.equals("") ? Either.left(new RuntimeException("Details cannot be empty")) : Either.right(category);
    }
    

    It might also be a good idea, depending on the usecase, to keep more validation errors. If you opt for this approach, you might look into the Validation.traverse to keep as much validation errors as possible. That's where the Validation really shines in my opinion.

    So then the code would look like this:

    private static Validation<Seq<RuntimeException>, List<String>> isCategoryValid(
            List<String> categories) {
    
        return Validation.traverse(categories, Example::validateCategory).map(Seq::toList);
    }
    
    
    private static Validation<List<RuntimeException>, String> validateCategory(String category) {
        // ... combine multiple validations for the same category
        return category.equals("") ? invalid(List.of(new RuntimeException("Details cannot be empty"))) : valid(category);
    }