diff --git a/src/main/java/io/github/joselion/maybe/SolveHandler.java b/src/main/java/io/github/joselion/maybe/SolveHandler.java index f9e5c99..6e43e31 100644 --- a/src/main/java/io/github/joselion/maybe/SolveHandler.java +++ b/src/main/java/io/github/joselion/maybe/SolveHandler.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import org.eclipse.jdt.annotation.Nullable; @@ -215,9 +216,9 @@ public SolveHandler onErrorSolv ) { return this.value .unwrap( - x -> ofType.isInstance(x) - ? Maybe.of(x).map(Commons::cast).solve(solver) - : SolveHandler.failure(Commons.cast(x)), + prev -> ofType.isInstance(prev) + ? Maybe.of(prev).map(Commons::cast).solve(solver) + : SolveHandler.failure(Commons.cast(prev)), SolveHandler::from ); } @@ -331,9 +332,9 @@ public SolveHandler map(final Function mapper) /** * If an error is present and matches the specified {@code ofType} class, map * it to another throwable using the {@code mapper} function which receives - * the mtching error in its argument. If the error is not present or it does - * not match the specified type, the {@code mapper} is never applied and the - * next handler will just contain the solved value. + * the matching error as input. If the error is not present or it does not + * match the specified type, the {@code mapper} is never applied and the next + * handler will just contain the solved value. * * @param the type of error to match * @param the type of the mapped error @@ -347,10 +348,10 @@ public SolveHandler mapError( final Function mapper ) { return this.value.unwrap( - e -> { - final var nextError = ofType.isInstance(e) - ? mapper.apply(Commons.cast(e)) - : Commons.cast(e); + error -> { + final var nextError = ofType.isInstance(error) + ? mapper.apply(Commons.cast(error)) + : Commons.cast(error); return SolveHandler.failure(nextError); }, @@ -360,9 +361,9 @@ public SolveHandler mapError( /** * If an error is present, map it to another throwable using the {@code mapper} - * function which receives the previous error in its argument. If the error - * is not present, the {@code mapper} is never applied and the next handler - * will just contain the solved value. + * function which receives the previous error as input. If the error is not + * present, the {@code mapper} is never applied and the next handler will + * just contain the solved value. * * @param the type of the mapped error * @param mapper a function which takes the error as argument and returns @@ -401,6 +402,46 @@ public SolveHandler flatMap(final FunctionIn simpler terms, this operation is a shorcut for the following: + *
{@code
+   * Maybe.of(value)
+   *  .solve(MyService::processOrThrow)
+   *  .solve(x -> {
+   *    if (someCondition(x)) { // `predicate` param
+   *      return x;
+   *    }
+   *
+   *    throw new RuntimeException("Invalid value: " + x); // `onFalse` param
+   *  });
+   * }
+ * + * @param the type of the supplied error + * @param predicate a predicate to apply to the value, if present + * @param onFalse a function that receives the value as input an returns an + * error in case te predicate evaluates to false + * @return a handler with either the same value or the mapped error + */ + public SolveHandler filter( + final Predicate predicate, + final Function onFalse + ) { + return this.value + .mapLeft(Commons::cast) + .unwrap( + SolveHandler::failure, + prev -> predicate.test(prev) + ? SolveHandler.from(prev) + : SolveHandler.failure(onFalse.apply(prev)) + ); + } + /** * If the value is present, casts the value to the provided {@code type} * class. If the error is present or the value not assignable to {@code type}, @@ -418,8 +459,8 @@ public SolveHandler cast(final Class type) { * If the value is present, casts the value to the provided {@code type} * class. If the value is not assignable to {@code type}, maps the error with * the provided {@code onError} function, which receives the produced - * {@link ClassCastException} on its argument. If the error is present, - * returns a handler with the same error. + * {@link ClassCastException} as input. If the error is present, returns a + * handler with the same error so it can be propagated downstream. * * @param the type of the cast value * @param the type of the mapped exception @@ -607,7 +648,7 @@ public CloseableHandler sol .of(prev) .solve(solver) .map(CloseableHandler::from) - .orElse(x -> CloseableHandler.failure(Commons.cast(x))) + .orElse(error -> CloseableHandler.failure(Commons.cast(error))) ); } } diff --git a/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java b/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java index 3850eb6..1c6e8f3 100644 --- a/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.INPUT_STREAM; import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -600,6 +601,49 @@ } } + @Nested class filter { + @Nested class when_the_value_is_present { + @Nested class and_the_value_matches_the_predicate { + @Test void returns_a_handler_with_the_same_value() { + final var onFalseSpy = Spy.function((String x) -> new RuntimeException("Invalid: " + x)); + final var handler = SolveHandler.from(OK).filter(x -> x.length() <= 2, onFalseSpy); + + assertThat(handler.success()).containsSame(OK); + assertThat(handler.error()).isEmpty(); + + verify(onFalseSpy, never()).apply(anyString()); + } + } + + @Nested class and_the_value_does_not_match_the_predicate { + @Test void returns_a_handler_with_the_mapped_error() { + final var onFalseSpy = Spy.function((String x) -> new RuntimeException("Invalid: " + x)); + final var handler = SolveHandler.from(OK).filter(x -> x.length() > 2, onFalseSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()) + .get(THROWABLE) + .isInstanceOf(RuntimeException.class) + .hasMessage("Invalid: OK"); + + verify(onFalseSpy).apply(OK); + } + } + } + + @Nested class when_the_error_is_present { + @Test void returns_a_handler_with_the_error() { + final var onFalseSpy = Spy.function((Object x) -> new RuntimeException("Invalid: " + x)); + final var handler = SolveHandler.failure(FAILURE).filter(OK::equals, onFalseSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).get().isEqualTo(FAILURE); + + verify(onFalseSpy, never()).apply(any()); + } + } + } + @Nested class cast { @Nested class when_the_value_is_present { @Nested class and_the_value_is_an_instance_of_the_type {