diff options
6 files changed, 143 insertions, 196 deletions
@@ -28,28 +28,22 @@ the use of which being problematic in performance-sensitive contexts. | |||
28 | import static org.pacien.lemonad.attempt.Attempt.*; | 28 | import 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 | ||
38 | The `Validation` monad represents a validation of a subject which can be either valid or invalid. | 38 | The `Validation` monad represents a validation of a subject which can be successful or failed with errors. |
39 | In the latter case, the monad wraps one or multiple validation errors in addition to the subject of the validation. | 39 | Those errors are aggregated from all the checks that have failed. |
40 | |||
41 | The `Validator` functional interface represents a function which performs verification operations on a supplied subject and returns | ||
42 | a `Validation`. | ||
43 | `Validator`s can be composed to perform verifications against multiple criteria and obtain an aggregated `Validation`. | ||
44 | 40 | ||
45 | ```java | 41 | ```java |
46 | import static org.pacien.lemonad.validation.Validator.*; | 42 | import org.pacien.lemonad.validation.Validation; |
47 | |||
48 | var validator = validatingAll( | ||
49 | ensuringPredicate(not(Lemon::isRotten), "Bad lemon."), | ||
50 | validatingField(Lemon::juiceContent, ensuringPredicate(mL -> mL >= 40, "Not juicy."))); | ||
51 | 43 | ||
52 | validator.validate(lemon) | 44 | Validation.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 | ||
21 | import org.pacien.lemonad.attempt.Attempt; | 21 | import org.pacien.lemonad.attempt.Attempt; |
22 | 22 | ||
23 | import java.util.Arrays; | 23 | import java.util.ArrayList; |
24 | import java.util.Collection; | ||
24 | import java.util.List; | 25 | import java.util.List; |
25 | import java.util.Objects; | ||
26 | import java.util.function.BiConsumer; | 26 | import java.util.function.BiConsumer; |
27 | import java.util.function.Consumer; | 27 | import java.util.function.Consumer; |
28 | import java.util.function.Function; | 28 | import java.util.function.Function; |
29 | import java.util.stream.Stream; | 29 | import java.util.function.Predicate; |
30 | 30 | ||
31 | import lombok.NonNull; | 31 | import lombok.NonNull; |
32 | 32 | ||
33 | import static java.util.stream.Collectors.toUnmodifiableList; | 33 | import static java.util.function.Function.identity; |
34 | import static org.pacien.lemonad.attempt.Attempt.failure; | 34 | import static org.pacien.lemonad.attempt.Attempt.failure; |
35 | import static org.pacien.lemonad.attempt.Attempt.success; | 35 | import 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 |