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(effect): Add EffectHandler .solve(..) operation #232

Merged
merged 1 commit into from
Feb 11, 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
4 changes: 2 additions & 2 deletions src/main/java/io/github/joselion/maybe/CloseableHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Optional<E> error() {
*
* @param <S> the type of the value returned by the {@code solver}
* @param <X> the type of exception the {@code solver} may throw
* @param solver the checked function operation to solve
* @param solver the throwing function operation to solve
* @return a {@link SolveHandler} with either the value solved or the thrown
* exception to be handled
*/
Expand Down Expand Up @@ -126,7 +126,7 @@ public <S, X extends Throwable> SolveHandler<S, X> solve(
* resource nor the error is present.
*
* @param <X> the type of exception the {@code effect} may throw
* @param effect the checked consumer operation to execute
* @param effect the throwing consumer operation to execute
* @return an {@link EffectHandler} with either the thrown exception to be
* handled or empty
*/
Expand Down
43 changes: 34 additions & 9 deletions src/main/java/io/github/joselion/maybe/EffectHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

import io.github.joselion.maybe.helpers.Commons;
import io.github.joselion.maybe.util.function.ThrowingConsumer;
import io.github.joselion.maybe.util.function.ThrowingFunction;
import io.github.joselion.maybe.util.function.ThrowingRunnable;
import io.github.joselion.maybe.util.function.ThrowingSupplier;

/**
* EffectHandler is an API to handle the posible error of a {@link Maybe}'s
Expand Down Expand Up @@ -67,7 +69,7 @@ Optional<E> error() {
/**
* Runs an effect if the operation succeeds.
*
* @param effect a runnable function
* @param effect the runnable to run on success
* @return the same handler to continue chainning operations
*/
public EffectHandler<E> doOnSuccess(final Runnable effect) {
Expand All @@ -85,7 +87,7 @@ public EffectHandler<E> doOnSuccess(final Runnable effect) {
*
* @param <X> the type of the error to match
* @param ofType a class instance of the error type to match
* @param effect a consumer function that recieves the caught error
* @param effect a consumer that recieves the caught error
* @return the same handler to continue chainning operations
*/
public <X extends Throwable> EffectHandler<E> doOnError(final Class<X> ofType, final Consumer<? super X> effect) {
Expand All @@ -101,7 +103,7 @@ public <X extends Throwable> EffectHandler<E> doOnError(final Class<X> ofType, f
* Run an effect if the error is present. The error is passed in the argument
* of the {@code effect} consumer.
*
* @param effect a consumer function that recieves the caught error
* @param effect a consumer that recieves the caught error
* @return the same handler to continue chainning operations
*/
public EffectHandler<E> doOnError(final Consumer<Throwable> effect) {
Expand All @@ -118,7 +120,7 @@ public EffectHandler<E> doOnError(final Consumer<Throwable> effect) {
*
* @param <X> the type of the error to catch
* @param ofType thetype of the error to catch
* @param handler a consumer function that receives the caught error
* @param handler a consumer that receives the caught error
* @return an empty handler if an error instance of the provided type was
* caught. The same handler instance otherwise
*/
Expand All @@ -139,7 +141,7 @@ public <X extends Throwable> EffectHandler<E> catchError(final Class<X> ofType,
* the operations returns an empty {@link EffectHandler}. Otherwise, the same
* instance is returned.
*
* @param handler a consumer function that recieves the caught error
* @param handler a consumer that recieves the caught error
* @return an empty handler if the error is present. The same handler
* instance otherwise
*/
Expand All @@ -157,8 +159,8 @@ public EffectHandler<E> catchError(final Consumer<Throwable> handler) {
* previous effect in two different callbacks.
*
* @param <X> the type of the error the new effect may throw
* @param onSuccess a runnable checked function to run in case of succeess
* @param onError a runnable checked function to run in case of error
* @param onSuccess a throwing runnable to run in case of succeess
* @param onError a throwing runnable to run in case of error
* @return a new {@link EffectHandler} representing the result of one of the
* invoked callback
*/
Expand All @@ -184,19 +186,42 @@ public <X extends Throwable> EffectHandler<X> effect(
* either empty or has a different error cause by the next effect.
*
* @param <X> the type of the error the new effect may throw
* @param effect a runnable checked function to run in case of succeess
* @param effect a throwing runnable to run in case of succeess
* @return a new {@link EffectHandler} that is either empty or with the
* thrown error
*/
public <X extends Throwable> EffectHandler<X> effect(final ThrowingRunnable<? extends X> effect) {
return this.effect(effect, err -> { });
}

/**
* Chain a solver covering both cases of success or error of the
* previous effect in two different callbacks.
*
* <p>The second callback receives the caught error. Both callbacks should
* solve a value of the same type {@code T}, but only one of the callbacks is
* invoked. It depends on whether the previous effect threw an error or not.
*
* @param <T> the type of the value to be solved
* @param <X> the type of exception the callbacks may throw
* @param onSuccess a throwing supplier that solves a value
* @param onError a throwing function that receives the error and solves a value
* @return a {@link SolveHandler} with either the solved value or the error
*/
public <T, X extends Throwable> SolveHandler<T, X> solve(
final ThrowingSupplier<T, X> onSuccess,
final ThrowingFunction<Throwable, T, X> onError
) {
return this.error
.map(Maybe.partial(onError))
.orElseGet(() -> Maybe.from(onSuccess));
}

/**
* Terminal operation to handle the error if present. The error is passed in
* the argument of the {@code effect} consumer.
*
* @param effect a consumer function that receives the caught error
* @param effect a consumer that receives the caught error
*/
public void orElse(final Consumer<Throwable> effect) {
this.error.ifPresent(effect);
Expand Down
32 changes: 16 additions & 16 deletions src/main/java/io/github/joselion/maybe/SolveHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Optional<E> error() {
* Run an effect if the operation solved successfully. The solved value
* is passed in the argument of the {@code effect} consumer.
*
* @param effect a function that receives the solved value
* @param effect a consumer that receives the solved value
* @return the same handler to continue chainning operations
*/
public SolveHandler<T, E> doOnSuccess(final Consumer<? super T> effect) {
Expand All @@ -104,7 +104,7 @@ public SolveHandler<T, E> doOnSuccess(final Consumer<? super T> effect) {
*
* @param <X> the type of the error to match
* @param ofType a class instance of the error type to match
* @param effect a consumer function that receives the caught error
* @param effect a consumer that receives the caught error
* @return the same handler to continue chainning operations
*/
public <X extends Throwable> SolveHandler<T, E> doOnError(final Class<X> ofType, final Consumer<? super X> effect) {
Expand All @@ -121,7 +121,7 @@ public <X extends Throwable> SolveHandler<T, E> doOnError(final Class<X> ofType,
* Run an effect if the error is present. The error is passed in the argument
* of the {@code effect} consumer.
*
* @param effect a consumer function that receives the caught error
* @param effect a consumer that receives the caught error
* @return the same handler to continue chainning operations
*/
public SolveHandler<T, E> doOnError(final Consumer<Throwable> effect) {
Expand Down Expand Up @@ -233,10 +233,10 @@ public <C extends Throwable, X extends Throwable> SolveHandler<T, X> onErrorSolv
*
* @param <S> the type of value returned by the next operation
* @param <X> the type of exception the new solver may throw
* @param onSuccess a checked function that receives the current value
* and solves another
* @param onError a checked function that receives the error and
* solves another value
* @param onSuccess a throwing function that receives the current value
* and solves another
* @param onError a throwing function that receives the error and solves
* another value
* @return a new handler with either the solved value or the error
*/
public <S, X extends Throwable> SolveHandler<S, X> solve(
Expand All @@ -255,8 +255,8 @@ public <S, X extends Throwable> SolveHandler<S, X> solve(
*
* @param <S> the type of value returned by the next operation
* @param <X> the type of exception the new solver may throw
* @param solver a checked function that receives the current value and
* solves another
* @param solver a throwing function that receives the current value and
* solves another
* @return a new handler with either the solved value or an error
*/
public <S, X extends Throwable> SolveHandler<S, X> solve(
Expand All @@ -275,8 +275,8 @@ public <S, X extends Throwable> SolveHandler<S, X> solve(
* error cases in two different callbacks.
*
* @param <X> the type of the error the effect may throw
* @param onSuccess a consumer checked function to run in case of succeess
* @param onError a consumer checked function to run in case of error
* @param onSuccess a throwing consumer to run in case of succeess
* @param onError a throwing consumer to run in case of error
* @return an {@link EffectHandler} representing the result of one of the
* invoked callback
*/
Expand All @@ -296,7 +296,7 @@ public <X extends Throwable> EffectHandler<X> effect(
* downstream.
*
* @param <X> the type of the error the effect may throw
* @param effect a consume checked function to run in case of succeess
* @param effect a throwing consume to run in case of succeess
* @return a new {@link EffectHandler} representing the result of the success
* callback or containg the error
*/
Expand Down Expand Up @@ -389,14 +389,14 @@ public T orElse(final Function<Throwable, ? extends T> mapper) {
}

/**
* Returns the solved value if present. Otherwise, the result produced by
* the supplying function as another value.
* Returns the solved value if present. Otherwise, another value produced by
* the {@code supplier}.
*
* @apiNote Use this method instead of {@link #orElse(Object)} to do lazy
* evaluation of the produced value. That means that the "else"
* value won't be evaluated if the error is not present.
* @param supplier the supplying function that produces another value if the
* operation failed to solve
* @param supplier the suplier that produces another value if the operation
* failed to solve
* @return the solved value if present. Another value otherwise
*/
public T orElseGet(final Supplier<? extends T> supplier) {
Expand Down
76 changes: 61 additions & 15 deletions src/test/java/io/github/joselion/maybe/EffectHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
throw FAILURE;
};

private final ThrowingRunnable<RuntimeException> noOp = () -> { };
private final ThrowingRunnable<RuntimeException> noop = () -> { };

@Nested class empty {
@Test void returns_an_empty_handler() {
Expand Down Expand Up @@ -62,7 +62,7 @@
@Test void calls_the_effect_callback() {
final var runnableSpy = Spy.runnable(() -> { });

Maybe.from(noOp).doOnSuccess(runnableSpy);
Maybe.from(noop).doOnSuccess(runnableSpy);

verify(runnableSpy).run();
}
Expand Down Expand Up @@ -126,7 +126,7 @@

Maybe
.from(throwingOp)
.effect(() -> { })
.effect(noop)
.doOnError(consumerSpy);

verify(consumerSpy).accept(FAILURE);
Expand All @@ -140,7 +140,7 @@
final var runtimeSpy = Spy.<RuntimeException>consumer(error -> { });
final var throwableSpy = Spy.<Throwable>consumer(error -> { });

Maybe.from(noOp)
Maybe.from(noop)
.doOnError(RuntimeException.class, runtimeSpy)
.doOnError(throwableSpy);

Expand Down Expand Up @@ -198,7 +198,7 @@
final var consumerSpy = Spy.<Throwable>consumer(e -> { });
final var handler = Maybe
.from(throwingOp)
.effect(() -> { })
.effect(noop)
.catchError(consumerSpy);

assertThat(handler.error()).isEmpty();
Expand All @@ -214,8 +214,8 @@
final var runtimeSpy = Spy.<RuntimeException>consumer(e -> { });
final var throwableSpy = Spy.<Throwable>consumer(e -> { });
final var handlers = List.of(
Maybe.from(noOp).catchError(RuntimeException.class, runtimeSpy),
Maybe.from(noOp).catchError(throwableSpy)
Maybe.from(noop).catchError(RuntimeException.class, runtimeSpy),
Maybe.from(noop).catchError(throwableSpy)
);

assertThat(handlers).isNotEmpty().allSatisfy(handler -> {
Expand All @@ -234,7 +234,7 @@
final var effectSpy = Spy.lambda(throwingOp);
final var successSpy = Spy.lambda(throwingOp);
final var errorSpy = Spy.throwingConsumer((Throwable err) -> throwingOp.run());
final var handler = Maybe.from(noOp);
final var handler = Maybe.from(noop);
final var newHandlers = List.of(
handler.effect(effectSpy),
handler.effect(successSpy, errorSpy)
Expand All @@ -255,7 +255,7 @@
@Nested class and_the_error_callback_is_provided {
@Nested class and_the_error_matches_the_type_of_the_arg {
@Test void calls_only_the_error_callback_and_returns_a_handler_with_the_error() throws FileSystemException {
final var successSpy = Spy.throwingRunnable(() -> { });
final var successSpy = Spy.throwingRunnable(noop);
final var errorSpy = Spy.throwingConsumer((Throwable err) -> throwingOp.run());
final var handler = Maybe.from(throwingOp).effect(successSpy, errorSpy);

Expand All @@ -268,9 +268,9 @@

@Nested class and_the_error_does_not_match_the_type_of_the_arg {
@Test void calls_only_the_error_callback_and_returns_a_handler_with_the_error() throws FileSystemException {
final var successSpy = Spy.throwingRunnable(() -> { });
final var successSpy = Spy.throwingRunnable(noop);
final var errorSpy = Spy.throwingConsumer((Throwable err) -> throwingOp.run());
final var handler = Maybe.from(throwingOp).effect(() -> { }).effect(successSpy, errorSpy);
final var handler = Maybe.from(throwingOp).effect(noop).effect(successSpy, errorSpy);

assertThat(handler.error()).contains(FAILURE);

Expand All @@ -293,6 +293,52 @@
}
}

@Nested class solve {
@Nested class when_the_error_is_not_present {
@Test void calls_only_the_success_callback_and_returns_a_SolveHandler() {
final var successSpy = Spy.throwingSupplier(() -> "OK");
final var errorSpy = Spy.throwingFunction((Throwable e) -> "FAIL");
final var handler = Maybe.from(noop).solve(successSpy, errorSpy);

assertThat(handler).isInstanceOf(SolveHandler.class);
assertThat(handler.success()).contains("OK");

verify(successSpy).get();
verify(errorSpy, never()).apply(any());
}
}

@Nested class when_the_error_is_present {
@Nested class and_the_error_matches_the_type_of_the_arg {
@Test void calls_only_the_error_callback_and_returns_a_handler_SolveHandler() {
final var successSpy = Spy.throwingSupplier(() -> "OK");
final var errorSpy = Spy.throwingFunction((Throwable e) -> "FAIL");
final var handler = Maybe.from(throwingOp).solve(successSpy, errorSpy);

assertThat(handler).isInstanceOf(SolveHandler.class);
assertThat(handler.success()).contains("FAIL");

verify(successSpy, never()).get();
verify(errorSpy).apply(FAILURE);
}
}

@Nested class and_the_error_does_not_match_the_type_of_the_arg {
@Test void calls_only_the_error_callback_and_returns_a_handler_SolveHandler() {
final var successSpy = Spy.throwingSupplier(() -> "OK");
final var errorSpy = Spy.throwingFunction((Throwable e) -> "FAIL");
final var handler = Maybe.from(throwingOp).effect(noop).solve(successSpy, errorSpy);

assertThat(handler).isInstanceOf(SolveHandler.class);
assertThat(handler.success()).contains("FAIL");

verify(successSpy, never()).get();
verify(errorSpy).apply(FAILURE);
}
}
}
}

@Nested class orElse {
@Nested class when_the_error_is_present {
@Nested class and_the_error_matches_the_type_of_the_arg {
Expand All @@ -309,7 +355,7 @@
@Nested class and_the_error_does_not_match_the_type_of_the_arg {
@Test void calls_the_effect_callback() {
final var consumerSpy = Spy.<Throwable>consumer(e -> { });
final var handler = Maybe.from(throwingOp).effect(() -> { });
final var handler = Maybe.from(throwingOp).effect(noop);

handler.orElse(consumerSpy);

Expand All @@ -321,7 +367,7 @@
@Nested class when_the_error_is_not_present {
@Test void never_calls_the_effect_callback() {
final var consumerSpy = Spy.<Throwable>consumer(e -> { });
final var handler = Maybe.from(noOp);
final var handler = Maybe.from(noop);

handler.orElse(consumerSpy);

Expand Down Expand Up @@ -357,7 +403,7 @@
@Test void throws_the_error() {
final var anotherError = new RuntimeException("OTHER");
final var functionSpy = Spy.function((Throwable err) -> anotherError);
final var handler = Maybe.from(throwingOp).effect(() -> { });
final var handler = Maybe.from(throwingOp).effect(noop);

assertThatCode(() -> handler.orThrow(functionSpy)).isEqualTo(anotherError);

Expand All @@ -370,7 +416,7 @@
@Nested class when_the_error_is_not_present {
@Test void no_exception_is_thrown() {
final var functionSpy = Spy.function((Throwable err) -> FAILURE);
final var handler = Maybe.from(noOp);
final var handler = Maybe.from(noop);

assertThatCode(handler::orThrow).doesNotThrowAnyException();
assertThatCode(() -> handler.orThrow(functionSpy)).doesNotThrowAnyException();
Expand Down
Loading
Loading