Skip to content

Commit

Permalink
feat(either): Missing flatmaps and map operations (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseLion committed Nov 8, 2023
1 parent a367d0f commit 82a79ee
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 47 deletions.
69 changes: 69 additions & 0 deletions src/main/java/io/github/joselion/maybe/util/Either.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,75 @@ default <T> Either<L, T> mapRight(final Function<R, T> mapper) {
);
}

/**
* Shortcut method which does a {@link #mapLeft(Function)} and a
* {@link #mapRight(Function)} in a single operation. The first argument
* maps the left value if present. Otherwise, the second argument maps the
* right value.
*
* @param <T> the type the left value will be mapped to
* @param <S> the type the right value will be mapped to
* @param leftMapper a function that receives the left value and returns another
* @param rigthMapper a function that receives the right value and returns another
* @return an {@code Either} instance with the mapped left or right value
*/
default <T, S> Either<T, S> map(final Function<L, T> leftMapper, final Function<R, S> rigthMapper) {
return unwrap(
left -> Either.ofLeft(leftMapper.apply(left)),
right -> Either.ofRight(rigthMapper.apply(right))
);
}

/**
* Map the {@code Left} value to another if present. Does nothing otherwise.
*
* This method is similar to {@link #mapLeft(Function)}, but the
* mapping function can return another {@code Either} without wrapping the
* left value within an additional {@code Either}.
*
* @param <T> the type the left value will be mapped to
* @param mapper a function that receives the left value and returns an {@code Either}
* @return an {@code Either} instance with the mapped left value
*/
default <T> Either<T, R> flatMapLeft(final Function<L, Either<T, R>> mapper) {
return unwrap(mapper, Either::ofRight);
}

/**
* Map the {@code Right} value to another if present. Does nothing otherwise.
*
* This method is similar to {@link #mapRight(Function)}, but the
* mapping function can return another {@code Either} without wrapping the
* right value within an additional {@code Either}.
*
* @param <T> the type the right value will be mapped to
* @param mapper a function that receives the right value and returns an {@code Either}
* @return an {@code Either} instance with the mapped right value
*/
default <T> Either<L, T> flatMapRight(final Function<R, Either<L, T>> mapper) {
return unwrap(Either::ofLeft, mapper);
}

/**
* Shortcut method which does a {@link #flatMapLeft(Function)} and a
* {@link #flatMapRight(Function)} in a single operation. The first argument
* maps the left value if present. Otherwise, the second argument maps the
* right value. In both cases, the mapped left/right values are never wrapped
* within an additional {@code Either}.
*
* @param <T> the type the left value will be mapped to
* @param <S> the type the right value will be mapped to
* @param leftMapper a function that receives the left value and returns an {@code Either}
* @param rigthMapper a function that receives the right value and returns an {@code Either}
* @return an {@code Either} instance with the mapped left or right value
*/
default <T, S> Either<T, S> flatMap(
final Function<L, Either<T, S>> leftMapper,
final Function<R, Either<T, S>> rigthMapper
) {
return unwrap(leftMapper, rigthMapper);
}

/**
* Terminal operator. Returns the {@code Left} value if present. Otherwise,
* it returns the provided fallback value.
Expand Down
69 changes: 69 additions & 0 deletions src/main/java17/io/github/joselion/maybe/util/Either.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,75 @@ default <T> Either<L, T> mapRight(final Function<R, T> mapper) {
);
}

/**
* Shortcut method which does a {@link #mapLeft(Function)} and a
* {@link #mapRight(Function)} in a single operation. The first argument
* maps the left value if present. Otherwise, the second argument maps the
* right value.
*
* @param <T> the type the left value will be mapped to
* @param <S> the type the right value will be mapped to
* @param leftMapper a function that receives the left value and returns another
* @param rigthMapper a function that receives the right value and returns another
* @return an {@code Either} instance with the mapped left or right value
*/
default <T, S> Either<T, S> map(final Function<L, T> leftMapper, final Function<R, S> rigthMapper) {
return unwrap(
left -> Either.ofLeft(leftMapper.apply(left)),
right -> Either.ofRight(rigthMapper.apply(right))
);
}

/**
* Map the {@code Left} value to another if present. Does nothing otherwise.
*
* This method is similar to {@link #mapLeft(Function)}, but the
* mapping function can return another {@code Either} without wrapping the
* left value within an additional {@code Either}.
*
* @param <T> the type the left value will be mapped to
* @param mapper a function that receives the left value and returns an {@code Either}
* @return an {@code Either} instance with the mapped left value
*/
default <T> Either<T, R> flatMapLeft(final Function<L, Either<T, R>> mapper) {
return unwrap(mapper, Either::ofRight);
}

/**
* Map the {@code Right} value to another if present. Does nothing otherwise.
*
* This method is similar to {@link #mapRight(Function)}, but the
* mapping function can return another {@code Either} without wrapping the
* right value within an additional {@code Either}.
*
* @param <T> the type the right value will be mapped to
* @param mapper a function that receives the right value and returns an {@code Either}
* @return an {@code Either} instance with the mapped right value
*/
default <T> Either<L, T> flatMapRight(final Function<R, Either<L, T>> mapper) {
return unwrap(Either::ofLeft, mapper);
}

/**
* Shortcut method which does a {@link #flatMapLeft(Function)} and a
* {@link #flatMapRight(Function)} in a single operation. The first argument
* maps the left value if present. Otherwise, the second argument maps the
* right value. In both cases, the mapped left/right values are never wrapped
* within an additional {@code Either}.
*
* @param <T> the type the left value will be mapped to
* @param <S> the type the right value will be mapped to
* @param leftMapper a function that receives the left value and returns an {@code Either}
* @param rigthMapper a function that receives the right value and returns an {@code Either}
* @return an {@code Either} instance with the mapped left or right value
*/
default <T, S> Either<T, S> flatMap(
final Function<L, Either<T, S>> leftMapper,
final Function<R, Either<T, S>> rigthMapper
) {
return unwrap(leftMapper, rigthMapper);
}

/**
* Terminal operator. Returns the {@code Left} value if present. Otherwise,
* it returns the provided fallback value.
Expand Down
30 changes: 14 additions & 16 deletions src/test/java/io/github/joselion/maybe/EffectHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystemException;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand All @@ -35,7 +33,7 @@
@Nested class doOnSuccess {
@Nested class when_the_value_is_present {
@Test void calls_the_effect_callback() {
final var runnableSpy = Spy.<Runnable>lambda(() -> { });
final var runnableSpy = Spy.runnable(() -> { });

Maybe.fromEffect(noOp).doOnSuccess(runnableSpy);

Expand All @@ -45,7 +43,7 @@

@Nested class when_the_value_is_not_present {
@Test void never_calls_the_effect_callback() {
final var runnableSpy = Spy.<Runnable>lambda(() -> { });
final var runnableSpy = Spy.runnable(() -> { });

Maybe.fromEffect(throwingOp).doOnSuccess(runnableSpy);

Expand All @@ -59,7 +57,7 @@
@Nested class and_the_error_type_is_provided {
@Nested class and_the_error_is_an_instance_of_the_provided_type {
@Test void calls_the_effect_callback() {
final var consumerSpy = Spy.<Consumer<FileSystemException>>lambda(error -> { });
final var consumerSpy = Spy.<FileSystemException>consumer(error -> { });

Maybe.fromEffect(throwingOp)
.doOnError(FileSystemException.class, consumerSpy);
Expand All @@ -70,7 +68,7 @@

@Nested class and_the_error_is_not_an_instance_of_the_provided_type {
@Test void never_calls_the_effect_callback() {
final var consumerSpy = Spy.<Consumer<RuntimeException>>lambda(error -> { });
final var consumerSpy = Spy.<RuntimeException>consumer(error -> { });

Maybe.fromEffect(throwingOp)
.doOnError(RuntimeException.class, consumerSpy);
Expand All @@ -82,7 +80,7 @@

@Nested class and_the_error_type_is_not_provided {
@Test void calls_the_effect_callback() {
final var consumerSpy = Spy.<Consumer<FileSystemException>>lambda(error -> { });
final var consumerSpy = Spy.<FileSystemException>consumer(error -> { });

Maybe.fromEffect(throwingOp)
.doOnError(consumerSpy);
Expand All @@ -94,7 +92,7 @@

@Nested class when_the_error_is_not_present {
@Test void never_calls_the_effect_callback() {
final var cunsumerSpy = Spy.<Consumer<RuntimeException>>lambda(error -> { });
final var cunsumerSpy = Spy.<RuntimeException>consumer(error -> { });

Maybe.fromEffect(noOp)
.doOnError(RuntimeException.class, cunsumerSpy)
Expand All @@ -110,7 +108,7 @@
@Nested class and_the_error_type_is_provided {
@Nested class and_the_error_is_an_instance_of_the_provided_type {
@Test void calls_the_handler_function() {
final var consumerSpy = Spy.<Consumer<FileSystemException>>lambda(e -> { });
final var consumerSpy = Spy.<FileSystemException>consumer(e -> { });
final var handler = Maybe.fromEffect(throwingOp)
.catchError(FileSystemException.class, consumerSpy);

Expand All @@ -122,7 +120,7 @@

@Nested class and_the_error_is_not_an_instance_of_the_provided_type {
@Test void never_calls_the_handler_function() {
final var consumerSpy = Spy.<Consumer<AccessDeniedException>>lambda(e -> { });
final var consumerSpy = Spy.<AccessDeniedException>consumer(e -> { });
final var handler = Maybe.fromEffect(throwingOp)
.catchError(AccessDeniedException.class, consumerSpy);

Expand All @@ -135,7 +133,7 @@

@Nested class and_the_error_type_is_not_provided {
@Test void calls_the_handler_function() {
final var consumerSpy = Spy.<Consumer<FileSystemException>>lambda(e -> { });
final var consumerSpy = Spy.<FileSystemException>consumer(e -> { });
final var handler = Maybe.fromEffect(throwingOp)
.catchError(consumerSpy);

Expand All @@ -148,7 +146,7 @@

@Nested class when_the_error_is_not_present {
@Test void never_calls_the_handler_function() {
final var consumerSpy = Spy.<Consumer<RuntimeException>>lambda(e -> { });
final var consumerSpy = Spy.<RuntimeException>consumer(e -> { });
final var handlers = List.of(
Maybe.fromEffect(noOp).catchError(RuntimeException.class, consumerSpy),
Maybe.fromEffect(noOp).catchError(consumerSpy)
Expand Down Expand Up @@ -224,7 +222,7 @@
@Nested class orElse {
@Nested class when_the_error_is_present {
@Test void calls_the_effect_callback() {
final var consumerSpy = Spy.<Consumer<FileSystemException>>lambda(e -> { });
final var consumerSpy = Spy.<FileSystemException>consumer(e -> { });
final var handler = Maybe.fromEffect(throwingOp);

handler.orElse(consumerSpy);
Expand All @@ -235,7 +233,7 @@

@Nested class when_the_error_is_not_present {
@Test void never_calls_the_effect_callback() {
final var consumerSpy = Spy.<Consumer<RuntimeException>>lambda(e -> { });
final var consumerSpy = Spy.<RuntimeException>consumer(e -> { });
final var handler = Maybe.fromEffect(noOp);

handler.orElse(consumerSpy);
Expand All @@ -249,7 +247,7 @@
@Nested class when_the_error_is_present {
@Test void throws_the_error() {
final var anotherError = new RuntimeException("OTHER");
final var functionSpy = Spy.<Function<FileSystemException, RuntimeException>>lambda(err -> anotherError);
final var functionSpy = Spy.function((FileSystemException err) -> anotherError);
final var handler = Maybe.fromEffect(throwingOp);

assertThatThrownBy(handler::orThrow).isEqualTo(FAIL_EXCEPTION);
Expand All @@ -261,7 +259,7 @@

@Nested class when_the_error_is_not_present {
@Test void no_exception_is_thrown() {
final var functionSpy = Spy.<Function<RuntimeException, FileSystemException>>lambda(err -> FAIL_EXCEPTION);
final var functionSpy = Spy.function((RuntimeException err) -> FAIL_EXCEPTION);
final var handler = Maybe.fromEffect(noOp);

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

0 comments on commit 82a79ee

Please sign in to comment.