From be07e55a3ad21b64c87888a69c8d19ed3a1a5d78 Mon Sep 17 00:00:00 2001 From: JoseLion Date: Mon, 6 Nov 2023 01:14:18 -0500 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat(either):=20Missing=20flatmaps=20a?= =?UTF-8?q?nd=20map=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/github/joselion/maybe/util/Either.java | 69 ++++++++ .../io/github/joselion/maybe/util/Either.java | 69 ++++++++ .../joselion/maybe/EffectHandlerTest.java | 30 ++-- .../joselion/maybe/ResolveHandlerTest.java | 30 ++-- .../joselion/maybe/util/EitherTest.java | 167 ++++++++++++++++-- .../java/io/github/joselion/testing/Spy.java | 20 +++ 6 files changed, 338 insertions(+), 47 deletions(-) diff --git a/src/main/java/io/github/joselion/maybe/util/Either.java b/src/main/java/io/github/joselion/maybe/util/Either.java index 65b9c08..5fb6af2 100644 --- a/src/main/java/io/github/joselion/maybe/util/Either.java +++ b/src/main/java/io/github/joselion/maybe/util/Either.java @@ -141,6 +141,75 @@ default Either mapRight(final Function 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 the type the left value will be mapped to + * @param 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 Either map(final Function leftMapper, final Function 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 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 Either flatMapLeft(final Function> 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 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 Either flatMapRight(final Function> 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 the type the left value will be mapped to + * @param 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 Either flatMap( + final Function> leftMapper, + final Function> rigthMapper + ) { + return unwrap(leftMapper, rigthMapper); + } + /** * Terminal operator. Returns the {@code Left} value if present. Otherwise, * it returns the provided fallback value. diff --git a/src/main/java17/io/github/joselion/maybe/util/Either.java b/src/main/java17/io/github/joselion/maybe/util/Either.java index 226f170..c4d61ab 100644 --- a/src/main/java17/io/github/joselion/maybe/util/Either.java +++ b/src/main/java17/io/github/joselion/maybe/util/Either.java @@ -141,6 +141,75 @@ default Either mapRight(final Function 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 the type the left value will be mapped to + * @param 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 Either map(final Function leftMapper, final Function 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 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 Either flatMapLeft(final Function> 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 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 Either flatMapRight(final Function> 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 the type the left value will be mapped to + * @param 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 Either flatMap( + final Function> leftMapper, + final Function> rigthMapper + ) { + return unwrap(leftMapper, rigthMapper); + } + /** * Terminal operator. Returns the {@code Left} value if present. Otherwise, * it returns the provided fallback value. diff --git a/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java b/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java index f23d433..dad9c93 100644 --- a/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java @@ -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; @@ -35,7 +33,7 @@ @Nested class doOnSuccess { @Nested class when_the_value_is_present { @Test void calls_the_effect_callback() { - final var runnableSpy = Spy.lambda(() -> { }); + final var runnableSpy = Spy.runnable(() -> { }); Maybe.fromEffect(noOp).doOnSuccess(runnableSpy); @@ -45,7 +43,7 @@ @Nested class when_the_value_is_not_present { @Test void never_calls_the_effect_callback() { - final var runnableSpy = Spy.lambda(() -> { }); + final var runnableSpy = Spy.runnable(() -> { }); Maybe.fromEffect(throwingOp).doOnSuccess(runnableSpy); @@ -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.>lambda(error -> { }); + final var consumerSpy = Spy.consumer(error -> { }); Maybe.fromEffect(throwingOp) .doOnError(FileSystemException.class, consumerSpy); @@ -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.>lambda(error -> { }); + final var consumerSpy = Spy.consumer(error -> { }); Maybe.fromEffect(throwingOp) .doOnError(RuntimeException.class, consumerSpy); @@ -82,7 +80,7 @@ @Nested class and_the_error_type_is_not_provided { @Test void calls_the_effect_callback() { - final var consumerSpy = Spy.>lambda(error -> { }); + final var consumerSpy = Spy.consumer(error -> { }); Maybe.fromEffect(throwingOp) .doOnError(consumerSpy); @@ -94,7 +92,7 @@ @Nested class when_the_error_is_not_present { @Test void never_calls_the_effect_callback() { - final var cunsumerSpy = Spy.>lambda(error -> { }); + final var cunsumerSpy = Spy.consumer(error -> { }); Maybe.fromEffect(noOp) .doOnError(RuntimeException.class, cunsumerSpy) @@ -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.>lambda(e -> { }); + final var consumerSpy = Spy.consumer(e -> { }); final var handler = Maybe.fromEffect(throwingOp) .catchError(FileSystemException.class, consumerSpy); @@ -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.>lambda(e -> { }); + final var consumerSpy = Spy.consumer(e -> { }); final var handler = Maybe.fromEffect(throwingOp) .catchError(AccessDeniedException.class, consumerSpy); @@ -135,7 +133,7 @@ @Nested class and_the_error_type_is_not_provided { @Test void calls_the_handler_function() { - final var consumerSpy = Spy.>lambda(e -> { }); + final var consumerSpy = Spy.consumer(e -> { }); final var handler = Maybe.fromEffect(throwingOp) .catchError(consumerSpy); @@ -148,7 +146,7 @@ @Nested class when_the_error_is_not_present { @Test void never_calls_the_handler_function() { - final var consumerSpy = Spy.>lambda(e -> { }); + final var consumerSpy = Spy.consumer(e -> { }); final var handlers = List.of( Maybe.fromEffect(noOp).catchError(RuntimeException.class, consumerSpy), Maybe.fromEffect(noOp).catchError(consumerSpy) @@ -224,7 +222,7 @@ @Nested class orElse { @Nested class when_the_error_is_present { @Test void calls_the_effect_callback() { - final var consumerSpy = Spy.>lambda(e -> { }); + final var consumerSpy = Spy.consumer(e -> { }); final var handler = Maybe.fromEffect(throwingOp); handler.orElse(consumerSpy); @@ -235,7 +233,7 @@ @Nested class when_the_error_is_not_present { @Test void never_calls_the_effect_callback() { - final var consumerSpy = Spy.>lambda(e -> { }); + final var consumerSpy = Spy.consumer(e -> { }); final var handler = Maybe.fromEffect(noOp); handler.orElse(consumerSpy); @@ -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.>lambda(err -> anotherError); + final var functionSpy = Spy.function((FileSystemException err) -> anotherError); final var handler = Maybe.fromEffect(throwingOp); assertThatThrownBy(handler::orThrow).isEqualTo(FAIL_EXCEPTION); @@ -261,7 +259,7 @@ @Nested class when_the_error_is_not_present { @Test void no_exception_is_thrown() { - final var functionSpy = Spy.>lambda(err -> FAIL_EXCEPTION); + final var functionSpy = Spy.function((RuntimeException err) -> FAIL_EXCEPTION); final var handler = Maybe.fromEffect(noOp); assertThatCode(handler::orThrow).doesNotThrowAnyException(); diff --git a/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java b/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java index 1af67a9..86cc04a 100644 --- a/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java @@ -14,9 +14,7 @@ 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 java.util.function.Supplier; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -44,7 +42,7 @@ @Nested class doOnSuccess { @Nested class when_the_value_is_present { @Test void calls_the_effect_callback() { - final var consumerSpy = Spy.>lambda(v -> { }); + final var consumerSpy = Spy.consumer(v -> { }); Maybe.fromResolver(okOp) .doOnSuccess(consumerSpy); @@ -55,7 +53,7 @@ @Nested class when_the_error_is_present { @Test void never_calls_the_effect_callback() { - final var consumerSpy = Spy.>lambda(v -> { }); + final var consumerSpy = Spy.consumer(v -> { }); Maybe.fromResolver(throwingOp) .doOnSuccess(consumerSpy); @@ -70,7 +68,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.>lambda(error -> { }); + final var consumerSpy = Spy.consumer(error -> { }); Maybe.fromResolver(throwingOp) .doOnError(FileSystemException.class, consumerSpy); @@ -81,7 +79,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.>lambda(error -> { }); + final var consumerSpy = Spy.consumer(error -> { }); Maybe.fromResolver(throwingOp) .doOnError(RuntimeException.class, consumerSpy); @@ -93,7 +91,7 @@ @Nested class and_the_error_type_is_not_provided { @Test void calls_the_effect_callback() { - final var consumerSpy = Spy.>lambda(error -> { }); + final var consumerSpy = Spy.consumer(error -> { }); Maybe.fromResolver(throwingOp) .doOnError(consumerSpy); @@ -105,7 +103,7 @@ @Nested class when_the_value_is_present { @Test void never_calls_the_effect_callback() { - final var cunsumerSpy = Spy.>lambda(error -> { }); + final var cunsumerSpy = Spy.consumer(error -> { }); Maybe.fromResolver(okOp) .doOnError(RuntimeException.class, cunsumerSpy) @@ -121,7 +119,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 functionSpy = Spy.>lambda(e -> OK); + final var functionSpy = Spy.function((FileSystemException e) -> OK); final var handler = Maybe.fromResolver(throwingOp) .catchError(FileSystemException.class, functionSpy); @@ -134,7 +132,7 @@ @Nested class and_the_error_is_not_an_instance_of_the_provided_type { @Test void never_calls_the_handler_function() { - final var functionSpy = Spy.>lambda(e -> OK); + final var functionSpy = Spy.function((AccessDeniedException e) -> OK); final var handler = Maybe.fromResolver(throwingOp) .catchError(AccessDeniedException.class, functionSpy); @@ -148,7 +146,7 @@ @Nested class and_the_error_type_is_not_provided { @Test void calls_the_handler_function() { - final var handlerSpy = Spy.>lambda(e -> OK); + final var handlerSpy = Spy.function((FileSystemException e) -> OK); final var resolver = Maybe.fromResolver(throwingOp) .catchError(handlerSpy); @@ -162,7 +160,7 @@ @Nested class when_the_value_is_present { @Test void never_calls_the_handler_function() { - final var functionSpy = Spy.>lambda(e -> OK); + final var functionSpy = Spy.function((RuntimeException e) -> OK); final var resolvers = List.of( Maybe.fromResolver(okOp).catchError(RuntimeException.class, functionSpy), Maybe.fromResolver(okOp).catchError(functionSpy) @@ -377,7 +375,7 @@ @Nested class orElseGet { @Nested class when_the_value_is_present { @Test void never_evaluates_the_supplier_and_returns_the_value() { - final var supplierSpy = Spy.>lambda(() -> OTHER); + final var supplierSpy = Spy.supplier(() -> OTHER); final var handler = Maybe.fromResolver(okOp); assertThat(handler.orElseGet(supplierSpy)).isEqualTo(OK); @@ -388,7 +386,7 @@ @Nested class when_the_error_is_present { @Test void evaluates_the_supplier_and_returns_the_produced_value() { - final var supplierSpy = Spy.>lambda(() -> OTHER); + final var supplierSpy = Spy.supplier(() -> OTHER); final var handler = Maybe.fromResolver(throwingOp); assertThat(handler.orElseGet(supplierSpy)).isEqualTo(OTHER); @@ -419,7 +417,7 @@ @Nested class orThrow { @Nested class when_the_value_is_present { @Test void returns_the_value() throws FileSystemException { - final var functionSpy = Spy.>lambda(error -> FAIL_EXCEPTION); + final var functionSpy = Spy.function((RuntimeException error) -> FAIL_EXCEPTION); final var handler = Maybe.fromResolver(okOp); assertThat(handler.orThrow()).isEqualTo(OK); @@ -432,7 +430,7 @@ @Nested class when_the_error_is_present { @Test void throws_the_error() { final var anotherError = new RuntimeException(OTHER); - final var functionSpy = Spy.>lambda(error -> anotherError); + final var functionSpy = Spy.function((FileSystemException error) -> anotherError); final var handler = Maybe.fromResolver(throwingOp); assertThatThrownBy(handler::orThrow).isEqualTo(FAIL_EXCEPTION); diff --git a/src/test/java/io/github/joselion/maybe/util/EitherTest.java b/src/test/java/io/github/joselion/maybe/util/EitherTest.java index ccfd8b9..4f6a617 100644 --- a/src/test/java/io/github/joselion/maybe/util/EitherTest.java +++ b/src/test/java/io/github/joselion/maybe/util/EitherTest.java @@ -7,9 +7,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import java.util.function.Consumer; -import java.util.function.Function; - import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -99,7 +96,7 @@ @Nested class doOnLeft { @Nested class when_the_left_value_is_present { @Test void runs_the_effect() { - final var effectSpy = Spy.>lambda(value -> { }); + final var effectSpy = Spy.consumer(value -> { }); Either.ofLeft("foo").doOnLeft(effectSpy); @@ -109,7 +106,7 @@ @Nested class when_the_right_value_is_present { @Test void does_not_run_the_effect() { - final var effectSpy = Spy.>lambda(value -> { }); + final var effectSpy = Spy.consumer(value -> { }); Either.ofRight("foo").doOnLeft(effectSpy); @@ -121,7 +118,7 @@ @Nested class doOnRight { @Nested class when_the_left_value_is_present { @Test void does_not_run_the_effect() { - final var effectSpy = Spy.>lambda(value -> { }); + final var effectSpy = Spy.consumer(value -> { }); Either.ofLeft("foo").doOnRight(effectSpy); @@ -131,7 +128,7 @@ @Nested class when_the_right_value_is_present { @Test void runs_the_effect() { - final var effectSpy = Spy.>lambda(value -> { }); + final var effectSpy = Spy.consumer(value -> { }); Either.ofRight("foo").doOnRight(effectSpy); @@ -143,7 +140,7 @@ @Nested class mapLeft { @Nested class when_the_left_value_is_present { @Test void applies_the_mapper_to_the_value() { - final var mapperSpy = Spy.>lambda(Integer::parseInt); + final var mapperSpy = Spy.function(Integer::parseInt); final var either = Either.ofLeft("1").mapLeft(mapperSpy); assertThat(either) @@ -158,7 +155,7 @@ @Nested class when_the_right_value_is_present { @Test void does_not_apply_the_mapper() { - final var mapperSpy = Spy.>lambda(Object::toString); + final var mapperSpy = Spy.function(Object::toString); final var either = Either.ofRight(1).mapLeft(mapperSpy); assertThat(either) @@ -175,7 +172,7 @@ @Nested class mapRight { @Nested class when_the_left_value_is_present { @Test void does_not_apply_the_mapper() { - final var mapperSpy = Spy.>lambda(Object::toString); + final var mapperSpy = Spy.function(Object::toString); final var either = Either.ofLeft(1).mapRight(mapperSpy); assertThat(either) @@ -190,7 +187,7 @@ @Nested class when_the_right_value_is_present { @Test void applies_the_mapper_to_the_value() { - final var mapperSpy = Spy.>lambda(Integer::parseInt); + final var mapperSpy = Spy.function(Integer::parseInt); final var either = Either.ofRight("1").mapRight(mapperSpy); assertThat(either) @@ -204,6 +201,146 @@ } } + @Nested class map { + @Nested class when_the_left_value_is_present { + @Test void applies_only_the_left_mapper_to_the_value() { + final var leftMapperSpy = Spy.function(Integer::parseInt); + final var rightMapperSpy = Spy.function(Object::toString); + final var either = Either.ofLeft("1").map(leftMapperSpy, rightMapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Left.class)) + .extracting(Either.Left::value) + .isExactlyInstanceOf(Integer.class) + .isEqualTo(1); + + verify(leftMapperSpy).apply("1"); + verify(rightMapperSpy, never()).apply(any()); + } + } + + @Nested class when_the_right_value_is_present { + @Test void applies_only_the_right_mapper_to_the_value() { + final var leftMapperSpy = Spy.function(Object::toString); + final var rightMapperSpy = Spy.function(Integer::parseInt); + final var either = Either.ofRight("1").map(leftMapperSpy, rightMapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Right.class)) + .extracting(Either.Right::value) + .isExactlyInstanceOf(Integer.class) + .isEqualTo(1); + + verify(leftMapperSpy, never()).apply(any()); + verify(rightMapperSpy).apply("1"); + } + } + } + + @Nested class flatMapLeft { + @Nested class when_the_left_value_is_present { + @Test void applies_the_mapper_to_the_value_without_wrapping_the_result_within_another_Either() { + final var mapperSpy = Spy.>function(x -> Either.ofLeft(Integer.parseInt(x))); + final var either = Either.ofLeft("1").flatMapLeft(mapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Left.class)) + .extracting(Either.Left::value) + .isExactlyInstanceOf(Integer.class) + .isEqualTo(1); + + verify(mapperSpy).apply("1"); + } + } + + @Nested class when_the_right_value_is_present { + @Test void does_not_apply_the_mapper() { + final var mapperSpy = Spy.function(x -> Either.ofRight(x.toString())); + final var either = Either.ofRight("1").flatMapLeft(mapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Right.class)) + .extracting(Either.Right::value) + .isExactlyInstanceOf(String.class) + .isEqualTo("1"); + + verify(mapperSpy, never()).apply("1"); + } + } + } + + @Nested class flatMapRight { + @Nested class when_the_left_value_is_present { + @Test void does_not_apply_the_mapper() { + final var mapperSpy = Spy.function(x -> Either.ofLeft(x.toString())); + final var either = Either.ofLeft("1").flatMapRight(mapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Left.class)) + .extracting(Either.Left::value) + .isExactlyInstanceOf(String.class) + .isEqualTo("1"); + + verify(mapperSpy, never()).apply("1"); + } + } + + @Nested class when_the_right_value_is_present { + @Test void applies_the_mapper_to_the_value_without_wrapping_the_result_within_another_Either() { + final var mapperSpy = Spy.>function(x -> Either.ofRight(Integer.parseInt(x))); + final var either = Either.ofRight("1").flatMapRight(mapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Right.class)) + .extracting(Either.Right::value) + .isExactlyInstanceOf(Integer.class) + .isEqualTo(1); + + verify(mapperSpy).apply("1"); + } + } + } + + @Nested class flatMap { + @Nested class when_the_left_value_is_present { + @Test void applies_only_the_left_mapper_to_the_value_without_wrapping_the_result_within_another_Either() { + final var leftMapperSpy = Spy.>function( + x -> Either.ofLeft(Integer.parseInt(x)) + ); + final var rightMapperSpy = Spy.>function(x -> Either.ofLeft(0)); + final var either = Either.ofLeft("1").flatMap(leftMapperSpy, rightMapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Left.class)) + .extracting(Either.Left::value) + .isExactlyInstanceOf(Integer.class) + .isEqualTo(1); + + verify(leftMapperSpy).apply("1"); + verify(rightMapperSpy, never()).apply("1"); + } + } + + @Nested class when_the_right_value_is_present { + @Test void applies_only_the_right_mapper_to_the_value_without_wrapping_the_result_within_another_Either() { + final var leftMapperSpy = Spy.function(x -> Either.ofRight(0)); + final var rightMapperSpy = Spy.>function( + x -> Either.ofRight(Integer.parseInt(x)) + ); + final var either = Either.ofRight("1").flatMap(leftMapperSpy, rightMapperSpy); + + assertThat(either) + .asInstanceOf(type(Either.Right.class)) + .extracting(Either.Right::value) + .isExactlyInstanceOf(Integer.class) + .isEqualTo(1); + + verify(leftMapperSpy, never()).apply("1"); + verify(rightMapperSpy).apply("1"); + } + } + } + @Nested class leftOrElse { @Nested class when_the_left_value_is_present { @Test void returns_the_left_value() { @@ -243,8 +380,8 @@ @Nested class unwrap { @Nested class when_the_left_value_is_present { @Test void returns_the_value_using_the_onLeft_handler() { - final var onLeftSpy = Spy.>lambda("The value is: "::concat); - final var onRightSpy = Spy.>lambda(x -> "The value is: ".concat(x.toString())); + final var onLeftSpy = Spy.function("The value is: "::concat); + final var onRightSpy = Spy.function(x -> "The value is: ".concat(x.toString())); final var value = Either.ofLeft("foo").unwrap(onLeftSpy, onRightSpy); assertThat(value).isEqualTo("The value is: foo"); @@ -256,8 +393,8 @@ @Nested class when_the_right_value_is_present { @Test void returns_the_value_using_the_onRight_handler() { - final var onLeftSpy = Spy.>lambda(x -> "The value is: ".concat(x.toString())); - final var onRightSpy = Spy.>lambda("The value is: "::concat); + final var onLeftSpy = Spy.function(x -> "The value is: ".concat(x.toString())); + final var onRightSpy = Spy.function("The value is: "::concat); final var value = Either.ofRight("foo").unwrap(onLeftSpy, onRightSpy); assertThat(value).isEqualTo("The value is: foo"); diff --git a/src/test/java/io/github/joselion/testing/Spy.java b/src/test/java/io/github/joselion/testing/Spy.java index 2586459..3fde8d8 100644 --- a/src/test/java/io/github/joselion/testing/Spy.java +++ b/src/test/java/io/github/joselion/testing/Spy.java @@ -2,6 +2,10 @@ import static org.mockito.AdditionalAnswers.delegatesTo; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + import org.mockito.Mockito; import io.github.joselion.maybe.helpers.Common; @@ -14,4 +18,20 @@ public static T lambda(final T lambda) { return Mockito.mock(toMock, delegatesTo(lambda)); } + + public static Function function(final Function function) { + return lambda(function); + } + + public static Consumer consumer(final Consumer consumer) { + return lambda(consumer); + } + + public static Supplier supplier(final Supplier supplier) { + return lambda(supplier); + } + + public static Runnable runnable(final Runnable runnable) { + return lambda(runnable); + } }