Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(solve-handler): Add .filter(..) operation #237

Merged
merged 1 commit into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading