aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpacien2019-04-13 21:24:40 +0200
committerpacien2019-04-13 21:24:40 +0200
commit7ded9823fb54b81cbd73a133fd1e1fe036f31b04 (patch)
treed6f05d089507290d9cc47f24b6799e81ba31a5d8
parent80f7acccde921fb387a17d2a5712babf3001b974 (diff)
downloadlemonad-7ded9823fb54b81cbd73a133fd1e1fe036f31b04.tar.gz
refactor validation
-rw-r--r--readme.md20
-rw-r--r--src/main/java/org/pacien/lemonad/validation/Validation.java133
-rw-r--r--src/main/java/org/pacien/lemonad/validation/ValidationContainer.java6
-rw-r--r--src/main/java/org/pacien/lemonad/validation/Validator.java80
-rw-r--r--src/test/java/org/pacien/lemonad/validation/ValidationTest.java39
-rw-r--r--src/test/java/org/pacien/lemonad/validation/ValidatorTest.java61
6 files changed, 143 insertions, 196 deletions
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.
28import static org.pacien.lemonad.attempt.Attempt.*; 28import static org.pacien.lemonad.attempt.Attempt.*;
29 29
30(tree.hasLemon() ? success(tree.getLemon()) : failure("No lemon.")) 30(tree.hasLemon() ? success(tree.getLemon()) : failure("No lemon."))
31 .recoverError(__ -> store.buyLemon()) 31 .recoverError(error -> store.buyLemon())
32 .transformResult(this::makeLemonade) 32 .transformResult(this::makeLemonade)
33 .ifSuccess(this::drink); 33 .ifSuccess(this::drink);
34``` 34```
35 35
36### Validation 36### Validation
37 37
38The `Validation` monad represents a validation of a subject which can be either valid or invalid. 38The `Validation` monad represents a validation of a subject which can be successful or failed with errors.
39In the latter case, the monad wraps one or multiple validation errors in addition to the subject of the validation. 39Those errors are aggregated from all the checks that have failed.
40
41The `Validator` functional interface represents a function which performs verification operations on a supplied subject and returns
42a `Validation`.
43`Validator`s can be composed to perform verifications against multiple criteria and obtain an aggregated `Validation`.
44 40
45```java 41```java
46import static org.pacien.lemonad.validation.Validator.*; 42import org.pacien.lemonad.validation.Validation;
47
48var validator = validatingAll(
49 ensuringPredicate(not(Lemon::isRotten), "Bad lemon."),
50 validatingField(Lemon::juiceContent, ensuringPredicate(mL -> mL >= 40, "Not juicy.")));
51 43
52validator.validate(lemon) 44Validation.of(lemon)
45 .validate(not(Lemon::isRotten), "Bad lemon.")
46 .validate(Lemon::juiceContent, mL -> mL >= 40, "Not juicy")
53 .ifValid(this::makeLemonade) 47 .ifValid(this::makeLemonade)
54 .ifInvalid(errors -> makeLifeTakeTheLemonBack()); 48 .ifInvalid(errors -> makeLifeTakeTheLemonBack());
55``` 49```
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;
20 20
21import org.pacien.lemonad.attempt.Attempt; 21import org.pacien.lemonad.attempt.Attempt;
22 22
23import java.util.Arrays; 23import java.util.ArrayList;
24import java.util.Collection;
24import java.util.List; 25import java.util.List;
25import java.util.Objects;
26import java.util.function.BiConsumer; 26import java.util.function.BiConsumer;
27import java.util.function.Consumer; 27import java.util.function.Consumer;
28import java.util.function.Function; 28import java.util.function.Function;
29import java.util.stream.Stream; 29import java.util.function.Predicate;
30 30
31import lombok.NonNull; 31import lombok.NonNull;
32 32
33import static java.util.stream.Collectors.toUnmodifiableList; 33import static java.util.function.Function.identity;
34import static org.pacien.lemonad.attempt.Attempt.failure; 34import static org.pacien.lemonad.attempt.Attempt.failure;
35import static org.pacien.lemonad.attempt.Attempt.success; 35import static org.pacien.lemonad.attempt.Attempt.success;
36 36
@@ -72,58 +72,133 @@ public interface Validation<S, E> {
72 } 72 }
73 73
74 /** 74 /**
75 * @param consumer the consumer called with the validation subject and reported errors if the validation is failed. 75 * @param consumer the consumer called with the validation subject and reported errors if the validation has failed.
76 * @return the current object. 76 * @return the current object.
77 */ 77 */
78 default Validation<S, E> ifInvalid(@NonNull BiConsumer<? super S, ? super List<? super E>> consumer) { 78 default Validation<S, E> ifInvalid(@NonNull BiConsumer<? super S, ? super List<? super E>> consumer) {
79 if (!isValid()) consumer.accept(getSubject(), getErrors()); 79 if (isInvalid()) consumer.accept(getSubject(), getErrors());
80 return this; 80 return this;
81 } 81 }
82 82
83 /** 83 /**
84 * @return an {@link Attempt} with a state corresponding to the one of the validation. 84 * @param predicate the validation predicate testing the validity of a subject.
85 * @param error the error to return if the subject does not pass the test.
86 * @return an updated {@link Validation}.
85 */ 87 */
86 default Attempt<S, List<E>> toAttempt() { 88 default Validation<S, E> validate(@NonNull Predicate<? super S> predicate, @NonNull E error) {
87 return isValid() ? success(getSubject()) : failure(getErrors()); 89 return validate(identity(), predicate, error);
90 }
91
92 /**
93 * @param mapper the field getter mapping the validation subject.
94 * @param predicate the validation predicate testing the validity of a subject.
95 * @param error the error to return if the subject does not pass the test.
96 * @return an updated {@link Validation}.
97 */
98 default <F> Validation<S, E> validate(
99 @NonNull Function<? super S, ? extends F> mapper,
100 @NonNull Predicate<? super F> predicate,
101 E error
102 ) {
103 return validate(mapper, field -> predicate.test(field) ? List.of() : List.of(error));
104 }
105
106 /**
107 * @param validator the validating function to use, returning a potentially empty list of errors.
108 * @return an updated {@link Validation}.
109 */
110 default Validation<S, E> validate(@NonNull Function<? super S, ? extends List<? extends E>> validator) {
111 var errors = validator.apply(getSubject());
112 return errors.isEmpty() ? this : merge(errors);
113 }
114
115 /**
116 * @param mapper the field getter mapping the validation subject.
117 * @param validator the validating function to use, returning a potentially empty list of errors.
118 * @return an updated {@link Validation}.
119 */
120 default <F> Validation<S, E> validate(
121 @NonNull Function<? super S, ? extends F> mapper,
122 @NonNull Function<? super F, ? extends List<? extends E>> validator
123 ) {
124 return validate(validator.compose(mapper));
125 }
126
127 /**
128 * @param validator a subject validating function returning a {@link Validation}.
129 * @return an updated {@link Validation}.
130 */
131 default Validation<S, E> merge(@NonNull Function<? super S, ? extends Validation<?, ? extends E>> validator) {
132 return merge(validator.apply(getSubject()));
133 }
134
135 /**
136 * @param mapper the field getter mapping the validation subject.
137 * @param validator a subject validating function returning a {@link Validation}.
138 * @return an updated {@link Validation}.
139 */
140 default <F> Validation<S, E> merge(
141 @NonNull Function<? super S, ? extends F> mapper,
142 @NonNull Function<? super F, ? extends Validation<?, ? extends E>> validator
143 ) {
144 return merge(validator.compose(mapper));
145 }
146
147 /**
148 * @param validation another validation to merge into the current one.
149 * @return an updated {@link Validation}.
150 */
151 @SuppressWarnings("unchecked")
152 default Validation<S, E> merge(@NonNull Validation<?, ? extends E> validation) {
153 if (validation.isValid()) return this;
154 if (this.isValid()) return Validation.of(this.getSubject(), (List<E>) validation.getErrors());
155 return merge(validation.getErrors());
156 }
157
158 /**
159 * @param errors a potentially empty list of additional errors to take into account.
160 * @return an updated {@link Validation}.
161 */
162 default Validation<S, E> merge(@NonNull Collection<? extends E> errors) {
163 var combinedErrors = new ArrayList<E>(getErrors().size() + errors.size());
164 combinedErrors.addAll(getErrors());
165 combinedErrors.addAll(errors);
166 return new ValidationContainer<>(getSubject(), combinedErrors);
88 } 167 }
89 168
90 /** 169 /**
91 * @param mapper a function transforming a {@link Validation}. 170 * @param mapper a function transforming a {@link Validation}.
92 * @return the transformed {@link Validation}. 171 * @return the transformed {@link Validation}.
93 */ 172 */
94 default <SS, EE> Validation<SS, EE> flatMap(@NonNull Function<? super Validation<? super S, ? super E>, ? extends Validation<? extends SS, ? extends EE>> mapper) { 173 default <SS, EE> Validation<SS, EE> flatMap(
174 @NonNull Function<? super Validation<? super S, ? super E>, ? extends Validation<? extends SS, ? extends EE>> mapper
175 ) {
95 //noinspection unchecked 176 //noinspection unchecked
96 return (Validation<SS, EE>) mapper.apply(this); 177 return (Validation<SS, EE>) mapper.apply(this);
97 } 178 }
98 179
99 /** 180 /**
100 * @param subject an overriding subject. 181 * @return an {@link Attempt} with a state corresponding to the one of the validation.
101 * @param validationResults a {@link Stream} of {@link Validation}s to merge.
102 * @return the merged {@link Validation} containing all errors from the supplied ones.
103 */ 182 */
104 static <S, E> Validation<S, E> merge(S subject, @NonNull Stream<? extends Validation<?, ? extends E>> validationResults) { 183 default Attempt<S, List<E>> toAttempt() {
105 return new ValidationContainer<>( 184 return isValid() ? success(getSubject()) : failure(getErrors());
106 subject,
107 validationResults.flatMap(res -> res.getErrors().stream()).collect(toUnmodifiableList()));
108 } 185 }
109 186
110 /** 187 /**
111 * @param subject the suject of the validation. 188 * @param subject the subject of the validation.
112 * @return a successful {@link Validation}. 189 * @param errors some optional validation errors.
190 * @return a {@link Validation}.
113 */ 191 */
114 static <S, E> Validation<S, E> valid(S subject) { 192 @SafeVarargs static <S, E> Validation<S, E> of(S subject, E... errors) {
115 return new ValidationContainer<>(subject, List.of()); 193 return Validation.of(subject, List.of(errors));
116 } 194 }
117 195
118 /** 196 /**
119 * @param subject the suject of the validation. 197 * @param subject the subject of the validation.
120 * @param error a validation error. 198 * @param errors some optional validation errors.
121 * @param errors additional validation errors. 199 * @return a {@link Validation}.
122 * @return a failed {@link Validation} for the supplied subject.
123 */ 200 */
124 @SafeVarargs static <S, E> Validation<S, E> invalid(S subject, E error, E... errors) { 201 static <S, E> Validation<S, E> of(S subject, @NonNull List<E> errors) {
125 return new ValidationContainer<>( 202 return new ValidationContainer<>(subject, errors);
126 subject,
127 Stream.concat(Stream.of(error), Arrays.stream(errors)).map(Objects::requireNonNull).collect(toUnmodifiableList()));
128 } 203 }
129} 204}
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;
23import lombok.NonNull; 23import lombok.NonNull;
24import lombok.Value; 24import lombok.Value;
25 25
26import static java.util.Collections.unmodifiableList;
27
26/** 28/**
27 * @author pacien 29 * @author pacien
28 */ 30 */
@@ -37,4 +39,8 @@ import lombok.Value;
37 @Override public boolean isInvalid() { 39 @Override public boolean isInvalid() {
38 return !isValid(); 40 return !isValid();
39 } 41 }
42
43 @Override public List<E> getErrors() {
44 return unmodifiableList(errors);
45 }
40} 46}
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 @@
1/*
2 * lemonad - Some functional sweetness for Java
3 * Copyright (C) 2019 Pacien TRAN-GIRARD
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19package org.pacien.lemonad.validation;
20
21import java.util.Arrays;
22import java.util.List;
23import java.util.Objects;
24import java.util.function.Function;
25import java.util.function.Predicate;
26
27import lombok.NonNull;
28import lombok.val;
29
30import static java.util.stream.Collectors.toUnmodifiableList;
31import static org.pacien.lemonad.validation.Validation.invalid;
32import static org.pacien.lemonad.validation.Validation.valid;
33
34/**
35 * A function which applies validation rules on a subject and reports possible errors.
36 *
37 * @param <S> the subject type
38 * @param <E> the error type
39 * @author pacien
40 */
41@FunctionalInterface public interface Validator<S, E> {
42 /**
43 * @param subject the subject to validate, which can potentially be null.
44 * @return the non-null result of the validation of the supplied subject.
45 */
46 Validation<S, E> validate(S subject);
47
48 /**
49 * @param predicate the validation predicate testing the validity of a subject.
50 * @param negativeError an error to return if the subject does not pass the test.
51 * @return a {@link Validator} based on the supplied predicate and error.
52 */
53 static <S, E> Validator<S, E> ensuringPredicate(@NonNull Predicate<? super S> predicate, @NonNull E negativeError) {
54 return subject -> predicate.test(subject) ? valid(subject) : invalid(subject, negativeError);
55 }
56
57 /**
58 * @param validators the {@link Validator}s to combine, to be evaluated in order of listing.
59 * @return a {@link Validator} based on the supplied ones.
60 */
61 @SafeVarargs static <S, E> Validator<S, E> validatingAll(@NonNull Validator<? super S, ? extends E>... validators) {
62 val validatorList = Arrays.stream(validators).map(Objects::requireNonNull).collect(toUnmodifiableList());
63 return subject -> new ValidationContainer<>(
64 subject,
65 validatorList.stream()
66 .flatMap(validator -> validator.validate(subject).getErrors().stream())
67 .collect(toUnmodifiableList()));
68 }
69
70 /**
71 * @param getter the field getter mapping the validation subject.
72 * @param validator the {@link Validator} validating the field.
73 * @return a {@link Validator} validating the parent object.
74 */
75 static <S, F, E> Validator<S, E> validatingField(@NonNull Function<? super S, ? extends F> getter,
76 @NonNull Validator<? super F, ? extends E> validator) {
77 //noinspection unchecked
78 return subject -> new ValidationContainer<>(subject, (List<E>) validator.validate(getter.apply(subject)).getErrors());
79 }
80}
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;
23import org.pacien.lemonad.attempt.Attempt; 23import org.pacien.lemonad.attempt.Attempt;
24 24
25import java.util.List; 25import java.util.List;
26import java.util.stream.Stream;
27 26
28import static org.junit.jupiter.api.Assertions.assertEquals; 27import static org.junit.jupiter.api.Assertions.assertEquals;
29import static org.junit.jupiter.api.Assertions.assertFalse; 28import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -36,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.fail;
36class ValidationTest { 35class ValidationTest {
37 @Test void testValidResult() { 36 @Test void testValidResult() {
38 var subject = "subject"; 37 var subject = "subject";
39 var validation = Validation.valid(subject); 38 var validation = Validation.of(subject);
40 assertTrue(validation.getErrors().isEmpty()); 39 assertTrue(validation.getErrors().isEmpty());
41 assertTrue(validation.isValid()); 40 assertTrue(validation.isValid());
42 assertFalse(validation.isInvalid()); 41 assertFalse(validation.isInvalid());
@@ -48,7 +47,7 @@ class ValidationTest {
48 @Test void testInvalidResult() { 47 @Test void testInvalidResult() {
49 var subject = "subject"; 48 var subject = "subject";
50 var errors = List.of(0, 1); 49 var errors = List.of(0, 1);
51 var validation = Validation.invalid(subject, 0, 1); 50 var validation = Validation.of(subject, 0, 1);
52 assertEquals(errors, validation.getErrors()); 51 assertEquals(errors, validation.getErrors());
53 assertFalse(validation.isValid()); 52 assertFalse(validation.isValid());
54 assertTrue(validation.isInvalid()); 53 assertTrue(validation.isInvalid());
@@ -61,18 +60,32 @@ class ValidationTest {
61 } 60 }
62 61
63 @Test void testFlatMap() { 62 @Test void testFlatMap() {
64 Validation.valid("subject") 63 Validation
65 .ifInvalid((__, ___) -> fail()) 64 .of("subject")
66 .flatMap(res -> Validation.invalid(res.getSubject(), 0)) 65 .ifInvalid((__, ___) -> fail())
67 .ifValid(innerSubject -> fail()); 66 .flatMap(res -> Validation.of(res.getSubject(), 0))
67 .ifValid(innerSubject -> fail());
68 } 68 }
69 69
70 @Test void testMerge() { 70 @Test void testMerge() {
71 var subject = "subject"; 71 var validation = Validation
72 assertEquals(List.of(0, 1, 2, 3), Validation.merge(subject, Stream.of( 72 .of(12345, 0)
73 Validation.valid(subject), 73 .merge(s -> Validation.of(s, 1))
74 Validation.invalid(subject, 0, 1), 74 .merge((Integer s) -> Integer.toString(s), (String s) -> Validation.of(s, 2))
75 Validation.invalid(subject, 2, 3)) 75 .merge(Validation.of(0L, List.of(3)))
76 ).getErrors()); 76 .merge(List.of(4));
77
78 assertEquals(Validation.of(12345, 0, 1, 2, 3, 4), validation);
79 }
80
81 @Test void testValidate() {
82 var validation = Validation
83 .of("subject")
84 .validate(String::isEmpty, 0)
85 .validate(String::length, len -> len > 0, 1)
86 .validate(subject -> List.of(2, 3))
87 .validate(subject -> subject.charAt(0), firstChar -> firstChar == 's' ? List.of() : List.of(4));
88
89 assertEquals(Validation.of("subject", 0, 2, 3), validation);
77 } 90 }
78} 91}
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 @@
1/*
2 * lemonad - Some functional sweetness for Java
3 * Copyright (C) 2019 Pacien TRAN-GIRARD
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19package org.pacien.lemonad.validation;
20
21import org.junit.jupiter.api.Test;
22
23import java.util.List;
24
25import static java.util.function.Predicate.not;
26import static org.junit.jupiter.api.Assertions.assertEquals;
27
28/**
29 * @author pacien
30 */
31class ValidatorTest {
32 @Test void testValidatorEnsuringPredicate() {
33 var emptyError = 0;
34 var validator = Validator.ensuringPredicate(not(String::isEmpty), emptyError);
35 assertEquals(List.of(emptyError), validator.validate("").getErrors());
36 assertEquals(List.of(), validator.validate("test").getErrors());
37 }
38
39 @Test void testValidatorValidatingAll() {
40 var emptyError = 0;
41 var tooLongError = 1;
42 var containsBadLetterError = 2;
43
44 var validator = Validator.validatingAll(
45 Validator.ensuringPredicate(not(String::isEmpty), emptyError),
46 Validator.ensuringPredicate((String str) -> str.length() < 10, tooLongError),
47 Validator.ensuringPredicate((String str) -> !str.contains("e"), containsBadLetterError));
48
49 assertEquals(List.of(emptyError), validator.validate("").getErrors());
50 assertEquals(List.of(tooLongError, containsBadLetterError), validator.validate("test test test").getErrors());
51 assertEquals(List.of(), validator.validate("potato").getErrors());
52 }
53
54 @Test void testValidatingField() {
55 var emptyError = 0;
56 var fieldValidator = Validator.ensuringPredicate((Integer len) -> len > 0, emptyError);
57 var validator = Validator.validatingField(String::length, fieldValidator);
58 assertEquals(List.of(emptyError), validator.validate("").getErrors());
59 assertEquals(List.of(), validator.validate("test").getErrors());
60 }
61}