Skip to content

Commit

Permalink
feat(solve-handler): Add .filter(..) operation (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseLion committed Feb 13, 2024
1 parent 9d51c1a commit 0ad8d98
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 16 deletions.
73 changes: 57 additions & 16 deletions src/main/java/io/github/joselion/maybe/SolveHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -215,9 +216,9 @@ public <C extends Throwable, X extends Throwable> SolveHandler<T, X> onErrorSolv
) {
return this.value
.unwrap(
x -> ofType.isInstance(x)
? Maybe.of(x).map(Commons::<C>cast).solve(solver)
: SolveHandler.failure(Commons.cast(x)),
prev -> ofType.isInstance(prev)
? Maybe.of(prev).map(Commons::<C>cast).solve(solver)
: SolveHandler.failure(Commons.cast(prev)),
SolveHandler::from
);
}
Expand Down Expand Up @@ -331,9 +332,9 @@ public <U> SolveHandler<U, E> map(final Function<? super T, ? extends U> 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 <C> the type of error to match
* @param <X> the type of the mapped error
Expand All @@ -347,10 +348,10 @@ public <C extends Throwable, X extends Throwable> SolveHandler<T, X> mapError(
final Function<? super C, ? extends X> mapper
) {
return this.value.unwrap(
e -> {
final var nextError = ofType.isInstance(e)
? mapper.apply(Commons.cast(e))
: Commons.<X>cast(e);
error -> {
final var nextError = ofType.isInstance(error)
? mapper.apply(Commons.cast(error))
: Commons.<X>cast(error);

return SolveHandler.failure(nextError);
},
Expand All @@ -360,9 +361,9 @@ public <C extends Throwable, X extends Throwable> SolveHandler<T, X> 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 <X> the type of the mapped error
* @param mapper a function which takes the error as argument and returns
Expand Down Expand Up @@ -401,6 +402,46 @@ public <U> SolveHandler<U, E> flatMap(final Function<? super T, Maybe<? extends
);
}

/**
* If the value is present and the value matches the given predicate, returns
* a handler with the same value. Otherwise, returns a handler with an error
* mapped by the {@code onFalse} function, which receives the value as input.
* If an error is present, returns a handler containing the same error so it
* can be propagated downstream.
*
* <p>In simpler terms, this operation is a shorcut for the following:
* <pre>{@code
* Maybe.of(value)
* .solve(MyService::processOrThrow)
* .solve(x -> {
* if (someCondition(x)) { // `predicate` param
* return x;
* }
*
* throw new RuntimeException("Invalid value: " + x); // `onFalse` param
* });
* }</pre>
*
* @param <X> 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 <X extends Throwable> SolveHandler<T, X> filter(
final Predicate<T> predicate,
final Function<T, X> onFalse
) {
return this.value
.mapLeft(Commons::<X>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},
Expand All @@ -418,8 +459,8 @@ public <U> SolveHandler<U, ClassCastException> cast(final Class<U> 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 <U> the type of the cast value
* @param <X> the type of the mapped exception
Expand Down Expand Up @@ -607,7 +648,7 @@ public <R extends AutoCloseable, X extends Throwable> CloseableHandler<R, X> sol
.of(prev)
.solve(solver)
.map(CloseableHandler::<R, X>from)
.orElse(x -> CloseableHandler.failure(Commons.cast(x)))
.orElse(error -> CloseableHandler.failure(Commons.cast(error)))
);
}
}
44 changes: 44 additions & 0 deletions src/test/java/io/github/joselion/maybe/SolveHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 0ad8d98

Please sign in to comment.