From 7ded9823fb54b81cbd73a133fd1e1fe036f31b04 Mon Sep 17 00:00:00 2001 From: pacien Date: Sat, 13 Apr 2019 21:24:40 +0200 Subject: refactor validation --- readme.md | 20 ++-- .../org/pacien/lemonad/validation/Validation.java | 133 ++++++++++++++++----- .../lemonad/validation/ValidationContainer.java | 6 + .../org/pacien/lemonad/validation/Validator.java | 80 ------------- .../pacien/lemonad/validation/ValidationTest.java | 39 ++++-- .../pacien/lemonad/validation/ValidatorTest.java | 61 ---------- 6 files changed, 143 insertions(+), 196 deletions(-) delete mode 100644 src/main/java/org/pacien/lemonad/validation/Validator.java delete mode 100644 src/test/java/org/pacien/lemonad/validation/ValidatorTest.java diff --git a/readme.md b/readme.md index 8a1a720..cad76ee 100644 --- a/readme.md +++ b/readme.md @@ -28,28 +28,22 @@ the use of which being problematic in performance-sensitive contexts. import static org.pacien.lemonad.attempt.Attempt.*; (tree.hasLemon() ? success(tree.getLemon()) : failure("No lemon.")) - .recoverError(__ -> store.buyLemon()) + .recoverError(error -> store.buyLemon()) .transformResult(this::makeLemonade) .ifSuccess(this::drink); ``` ### Validation -The `Validation` monad represents a validation of a subject which can be either valid or invalid. -In the latter case, the monad wraps one or multiple validation errors in addition to the subject of the validation. - -The `Validator` functional interface represents a function which performs verification operations on a supplied subject and returns -a `Validation`. -`Validator`s can be composed to perform verifications against multiple criteria and obtain an aggregated `Validation`. +The `Validation` monad represents a validation of a subject which can be successful or failed with errors. +Those errors are aggregated from all the checks that have failed. ```java -import static org.pacien.lemonad.validation.Validator.*; - -var validator = validatingAll( - ensuringPredicate(not(Lemon::isRotten), "Bad lemon."), - validatingField(Lemon::juiceContent, ensuringPredicate(mL -> mL >= 40, "Not juicy."))); +import org.pacien.lemonad.validation.Validation; -validator.validate(lemon) +Validation.of(lemon) + .validate(not(Lemon::isRotten), "Bad lemon.") + .validate(Lemon::juiceContent, mL -> mL >= 40, "Not juicy") .ifValid(this::makeLemonade) .ifInvalid(errors -> makeLifeTakeTheLemonBack()); ``` diff --git a/src/main/java/org/pacien/lemonad/validation/Validation.java b/src/main/java/org/pacien/lemonad/validation/Validation.java index 04c1ed0..98a4496 100644 --- a/src/main/java/org/pacien/lemonad/validation/Validation.java +++ b/src/main/java/org/pacien/lemonad/validation/Validation.java @@ -20,17 +20,17 @@ package org.pacien.lemonad.validation; import org.pacien.lemonad.attempt.Attempt; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Stream; +import java.util.function.Predicate; import lombok.NonNull; -import static java.util.stream.Collectors.toUnmodifiableList; +import static java.util.function.Function.identity; import static org.pacien.lemonad.attempt.Attempt.failure; import static org.pacien.lemonad.attempt.Attempt.success; @@ -72,58 +72,133 @@ public interface Validation { } /** - * @param consumer the consumer called with the validation subject and reported errors if the validation is failed. + * @param consumer the consumer called with the validation subject and reported errors if the validation has failed. * @return the current object. */ default Validation ifInvalid(@NonNull BiConsumer> consumer) { - if (!isValid()) consumer.accept(getSubject(), getErrors()); + if (isInvalid()) consumer.accept(getSubject(), getErrors()); return this; } /** - * @return an {@link Attempt} with a state corresponding to the one of the validation. + * @param predicate the validation predicate testing the validity of a subject. + * @param error the error to return if the subject does not pass the test. + * @return an updated {@link Validation}. */ - default Attempt> toAttempt() { - return isValid() ? success(getSubject()) : failure(getErrors()); + default Validation validate(@NonNull Predicate predicate, @NonNull E error) { + return validate(identity(), predicate, error); + } + + /** + * @param mapper the field getter mapping the validation subject. + * @param predicate the validation predicate testing the validity of a subject. + * @param error the error to return if the subject does not pass the test. + * @return an updated {@link Validation}. + */ + default Validation validate( + @NonNull Function mapper, + @NonNull Predicate predicate, + E error + ) { + return validate(mapper, field -> predicate.test(field) ? List.of() : List.of(error)); + } + + /** + * @param validator the validating function to use, returning a potentially empty list of errors. + * @return an updated {@link Validation}. + */ + default Validation validate(@NonNull Function> validator) { + var errors = validator.apply(getSubject()); + return errors.isEmpty() ? this : merge(errors); + } + + /** + * @param mapper the field getter mapping the validation subject. + * @param validator the validating function to use, returning a potentially empty list of errors. + * @return an updated {@link Validation}. + */ + default Validation validate( + @NonNull Function mapper, + @NonNull Function> validator + ) { + return validate(validator.compose(mapper)); + } + + /** + * @param validator a subject validating function returning a {@link Validation}. + * @return an updated {@link Validation}. + */ + default Validation merge(@NonNull Function> validator) { + return merge(validator.apply(getSubject())); + } + + /** + * @param mapper the field getter mapping the validation subject. + * @param validator a subject validating function returning a {@link Validation}. + * @return an updated {@link Validation}. + */ + default Validation merge( + @NonNull Function mapper, + @NonNull Function> validator + ) { + return merge(validator.compose(mapper)); + } + + /** + * @param validation another validation to merge into the current one. + * @return an updated {@link Validation}. + */ + @SuppressWarnings("unchecked") + default Validation merge(@NonNull Validation validation) { + if (validation.isValid()) return this; + if (this.isValid()) return Validation.of(this.getSubject(), (List) validation.getErrors()); + return merge(validation.getErrors()); + } + + /** + * @param errors a potentially empty list of additional errors to take into account. + * @return an updated {@link Validation}. + */ + default Validation merge(@NonNull Collection errors) { + var combinedErrors = new ArrayList(getErrors().size() + errors.size()); + combinedErrors.addAll(getErrors()); + combinedErrors.addAll(errors); + return new ValidationContainer<>(getSubject(), combinedErrors); } /** * @param mapper a function transforming a {@link Validation}. * @return the transformed {@link Validation}. */ - default Validation flatMap(@NonNull Function, ? extends Validation> mapper) { + default Validation flatMap( + @NonNull Function, ? extends Validation> mapper + ) { //noinspection unchecked return (Validation) mapper.apply(this); } /** - * @param subject an overriding subject. - * @param validationResults a {@link Stream} of {@link Validation}s to merge. - * @return the merged {@link Validation} containing all errors from the supplied ones. + * @return an {@link Attempt} with a state corresponding to the one of the validation. */ - static Validation merge(S subject, @NonNull Stream> validationResults) { - return new ValidationContainer<>( - subject, - validationResults.flatMap(res -> res.getErrors().stream()).collect(toUnmodifiableList())); + default Attempt> toAttempt() { + return isValid() ? success(getSubject()) : failure(getErrors()); } /** - * @param subject the suject of the validation. - * @return a successful {@link Validation}. + * @param subject the subject of the validation. + * @param errors some optional validation errors. + * @return a {@link Validation}. */ - static Validation valid(S subject) { - return new ValidationContainer<>(subject, List.of()); + @SafeVarargs static Validation of(S subject, E... errors) { + return Validation.of(subject, List.of(errors)); } /** - * @param subject the suject of the validation. - * @param error a validation error. - * @param errors additional validation errors. - * @return a failed {@link Validation} for the supplied subject. + * @param subject the subject of the validation. + * @param errors some optional validation errors. + * @return a {@link Validation}. */ - @SafeVarargs static Validation invalid(S subject, E error, E... errors) { - return new ValidationContainer<>( - subject, - Stream.concat(Stream.of(error), Arrays.stream(errors)).map(Objects::requireNonNull).collect(toUnmodifiableList())); + static Validation of(S subject, @NonNull List errors) { + return new ValidationContainer<>(subject, errors); } } diff --git a/src/main/java/org/pacien/lemonad/validation/ValidationContainer.java b/src/main/java/org/pacien/lemonad/validation/ValidationContainer.java index 03f77be..a9a1614 100644 --- a/src/main/java/org/pacien/lemonad/validation/ValidationContainer.java +++ b/src/main/java/org/pacien/lemonad/validation/ValidationContainer.java @@ -23,6 +23,8 @@ import java.util.List; import lombok.NonNull; import lombok.Value; +import static java.util.Collections.unmodifiableList; + /** * @author pacien */ @@ -37,4 +39,8 @@ import lombok.Value; @Override public boolean isInvalid() { return !isValid(); } + + @Override public List getErrors() { + return unmodifiableList(errors); + } } diff --git a/src/main/java/org/pacien/lemonad/validation/Validator.java b/src/main/java/org/pacien/lemonad/validation/Validator.java deleted file mode 100644 index e04cde8..0000000 --- a/src/main/java/org/pacien/lemonad/validation/Validator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * lemonad - Some functional sweetness for Java - * Copyright (C) 2019 Pacien TRAN-GIRARD - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.pacien.lemonad.validation; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Predicate; - -import lombok.NonNull; -import lombok.val; - -import static java.util.stream.Collectors.toUnmodifiableList; -import static org.pacien.lemonad.validation.Validation.invalid; -import static org.pacien.lemonad.validation.Validation.valid; - -/** - * A function which applies validation rules on a subject and reports possible errors. - * - * @param the subject type - * @param the error type - * @author pacien - */ -@FunctionalInterface public interface Validator { - /** - * @param subject the subject to validate, which can potentially be null. - * @return the non-null result of the validation of the supplied subject. - */ - Validation validate(S subject); - - /** - * @param predicate the validation predicate testing the validity of a subject. - * @param negativeError an error to return if the subject does not pass the test. - * @return a {@link Validator} based on the supplied predicate and error. - */ - static Validator ensuringPredicate(@NonNull Predicate predicate, @NonNull E negativeError) { - return subject -> predicate.test(subject) ? valid(subject) : invalid(subject, negativeError); - } - - /** - * @param validators the {@link Validator}s to combine, to be evaluated in order of listing. - * @return a {@link Validator} based on the supplied ones. - */ - @SafeVarargs static Validator validatingAll(@NonNull Validator... validators) { - val validatorList = Arrays.stream(validators).map(Objects::requireNonNull).collect(toUnmodifiableList()); - return subject -> new ValidationContainer<>( - subject, - validatorList.stream() - .flatMap(validator -> validator.validate(subject).getErrors().stream()) - .collect(toUnmodifiableList())); - } - - /** - * @param getter the field getter mapping the validation subject. - * @param validator the {@link Validator} validating the field. - * @return a {@link Validator} validating the parent object. - */ - static Validator validatingField(@NonNull Function getter, - @NonNull Validator validator) { - //noinspection unchecked - return subject -> new ValidationContainer<>(subject, (List) validator.validate(getter.apply(subject)).getErrors()); - } -} diff --git a/src/test/java/org/pacien/lemonad/validation/ValidationTest.java b/src/test/java/org/pacien/lemonad/validation/ValidationTest.java index fed74a3..c19c694 100644 --- a/src/test/java/org/pacien/lemonad/validation/ValidationTest.java +++ b/src/test/java/org/pacien/lemonad/validation/ValidationTest.java @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test; import org.pacien.lemonad.attempt.Attempt; import java.util.List; -import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -36,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.fail; class ValidationTest { @Test void testValidResult() { var subject = "subject"; - var validation = Validation.valid(subject); + var validation = Validation.of(subject); assertTrue(validation.getErrors().isEmpty()); assertTrue(validation.isValid()); assertFalse(validation.isInvalid()); @@ -48,7 +47,7 @@ class ValidationTest { @Test void testInvalidResult() { var subject = "subject"; var errors = List.of(0, 1); - var validation = Validation.invalid(subject, 0, 1); + var validation = Validation.of(subject, 0, 1); assertEquals(errors, validation.getErrors()); assertFalse(validation.isValid()); assertTrue(validation.isInvalid()); @@ -61,18 +60,32 @@ class ValidationTest { } @Test void testFlatMap() { - Validation.valid("subject") - .ifInvalid((__, ___) -> fail()) - .flatMap(res -> Validation.invalid(res.getSubject(), 0)) - .ifValid(innerSubject -> fail()); + Validation + .of("subject") + .ifInvalid((__, ___) -> fail()) + .flatMap(res -> Validation.of(res.getSubject(), 0)) + .ifValid(innerSubject -> fail()); } @Test void testMerge() { - var subject = "subject"; - assertEquals(List.of(0, 1, 2, 3), Validation.merge(subject, Stream.of( - Validation.valid(subject), - Validation.invalid(subject, 0, 1), - Validation.invalid(subject, 2, 3)) - ).getErrors()); + var validation = Validation + .of(12345, 0) + .merge(s -> Validation.of(s, 1)) + .merge((Integer s) -> Integer.toString(s), (String s) -> Validation.of(s, 2)) + .merge(Validation.of(0L, List.of(3))) + .merge(List.of(4)); + + assertEquals(Validation.of(12345, 0, 1, 2, 3, 4), validation); + } + + @Test void testValidate() { + var validation = Validation + .of("subject") + .validate(String::isEmpty, 0) + .validate(String::length, len -> len > 0, 1) + .validate(subject -> List.of(2, 3)) + .validate(subject -> subject.charAt(0), firstChar -> firstChar == 's' ? List.of() : List.of(4)); + + assertEquals(Validation.of("subject", 0, 2, 3), validation); } } diff --git a/src/test/java/org/pacien/lemonad/validation/ValidatorTest.java b/src/test/java/org/pacien/lemonad/validation/ValidatorTest.java deleted file mode 100644 index 55927b5..0000000 --- a/src/test/java/org/pacien/lemonad/validation/ValidatorTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * lemonad - Some functional sweetness for Java - * Copyright (C) 2019 Pacien TRAN-GIRARD - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.pacien.lemonad.validation; - -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static java.util.function.Predicate.not; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * @author pacien - */ -class ValidatorTest { - @Test void testValidatorEnsuringPredicate() { - var emptyError = 0; - var validator = Validator.ensuringPredicate(not(String::isEmpty), emptyError); - assertEquals(List.of(emptyError), validator.validate("").getErrors()); - assertEquals(List.of(), validator.validate("test").getErrors()); - } - - @Test void testValidatorValidatingAll() { - var emptyError = 0; - var tooLongError = 1; - var containsBadLetterError = 2; - - var validator = Validator.validatingAll( - Validator.ensuringPredicate(not(String::isEmpty), emptyError), - Validator.ensuringPredicate((String str) -> str.length() < 10, tooLongError), - Validator.ensuringPredicate((String str) -> !str.contains("e"), containsBadLetterError)); - - assertEquals(List.of(emptyError), validator.validate("").getErrors()); - assertEquals(List.of(tooLongError, containsBadLetterError), validator.validate("test test test").getErrors()); - assertEquals(List.of(), validator.validate("potato").getErrors()); - } - - @Test void testValidatingField() { - var emptyError = 0; - var fieldValidator = Validator.ensuringPredicate((Integer len) -> len > 0, emptyError); - var validator = Validator.validatingField(String::length, fieldValidator); - assertEquals(List.of(emptyError), validator.validate("").getErrors()); - assertEquals(List.of(), validator.validate("test").getErrors()); - } -} -- cgit v1.2.3