From 2b914b0efe041280ac715ca3d87bc9ce8f101b85 Mon Sep 17 00:00:00 2001 From: JoseLion Date: Mon, 12 Feb 2024 18:15:26 -0500 Subject: [PATCH] feat(errors): Add .mapError(..) operations --- .../github/joselion/maybe/EffectHandler.java | 44 ++++++++++ .../github/joselion/maybe/SolveHandler.java | 54 +++++++++++- .../joselion/maybe/EffectHandlerTest.java | 75 ++++++++++++++++ .../joselion/maybe/SolveHandlerTest.java | 88 +++++++++++++++++-- 4 files changed, 254 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/joselion/maybe/EffectHandler.java b/src/main/java/io/github/joselion/maybe/EffectHandler.java index aca31f6..1b178dd 100644 --- a/src/main/java/io/github/joselion/maybe/EffectHandler.java +++ b/src/main/java/io/github/joselion/maybe/EffectHandler.java @@ -155,6 +155,50 @@ public EffectHandler catchError(final Consumer handler) { .orElse(this); } + /** + * 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 previous 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 be the same as it was. + * + * @param the type of error to match + * @param the type of the mapped error + * @param ofType a class instance of the error type to match + * @param mapper a function which takes the error as argument and returns + * another error + * @return a handler with either the mapped error or empty + */ + public EffectHandler mapError( + final Class ofType, + final Function mapper + ) { + return this.error + .filter(ofType::isInstance) + .map(Commons::cast) + .map(mapper) + .map(EffectHandler::failure) + .orElseGet(() -> Commons.cast(this)); + } + + /** + * 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 be empty. + * + * @param the type of the mapped error + * @param mapper a function which takes the error as argument and returns + * another error + * @return a handler with either the mapped error or empty + */ + public EffectHandler mapError(final Function mapper) { + return this.error + .map(mapper) + .map(EffectHandler::failure) + .orElseGet(EffectHandler::empty); + } + /** * Chain another effect covering both cases of success or error of the * previous effect in two different callbacks. diff --git a/src/main/java/io/github/joselion/maybe/SolveHandler.java b/src/main/java/io/github/joselion/maybe/SolveHandler.java index b259b5b..b4981d0 100644 --- a/src/main/java/io/github/joselion/maybe/SolveHandler.java +++ b/src/main/java/io/github/joselion/maybe/SolveHandler.java @@ -312,9 +312,9 @@ public EffectHandler effect(final ThrowingConsumer the type the value is mapped to + * @param the type the mapped value * @param mapper a function which takes the value as argument and returns * another value * @return a new handler with either the mapped value or an error @@ -328,6 +328,56 @@ public SolveHandler map(final Function 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. + * + * @param the type of error to match + * @param the type of the mapped error + * @param ofType a class instance of the error type to match + * @param mapper a function which takes the error as argument and returns + * another error + * @return a new handler with either the mapped error or the value + */ + public SolveHandler mapError( + final Class ofType, + final Function mapper + ) { + return this.value.unwrap( + e -> { + final var nextError = ofType.isInstance(e) + ? mapper.apply(Commons.cast(e)) + : Commons.cast(e); + + return SolveHandler.failure(nextError); + }, + SolveHandler::from + ); + } + + /** + * 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. + * + * @param the type of the mapped error + * @param mapper a function which takes the error as argument and returns + * another error + * @return a new handler with either the mapped error or the value + */ + public SolveHandler mapError(final Function mapper) { + return this.value + .mapLeft(mapper) + .unwrap( + SolveHandler::failure, + SolveHandler::from + ); + } + /** * If the value is present, map it to another value using the {@code mapper} * function. If an error is present, the {@code mapper} function is never diff --git a/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java b/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java index 633951f..f2a7235 100644 --- a/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/EffectHandlerTest.java @@ -228,6 +228,81 @@ } } + @Nested class mapError { + @Nested class when_the_error_is_present { + @Nested class and_the_error_type_is_provided { + @Nested class and_the_error_is_an_instance_of_the_provided_type { + @Test void returns_a_handler_with_the_mapped_error() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((FileSystemException e) -> nextError); + final var handler = EffectHandler.failure(FAILURE).mapError(FileSystemException.class, mapperSpy); + + assertThat(handler.error()).contains(nextError); + + verify(mapperSpy).apply(FAILURE); + } + } + + @Nested class and_the_error_is_not_an_instance_of_the_provided_type { + @Test void returns_an_empty_handler_and_never_calls_the_mapper() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((FileSystemException e) -> nextError); + final var handler = EffectHandler.failure(FAILURE).mapError(AccessDeniedException.class, mapperSpy); + + assertThat(handler.error()).get().isEqualTo(FAILURE); + + verify(mapperSpy, never()).apply(FAILURE); + } + } + } + + @Nested class and_the_error_type_is_not_provided { + @Nested class and_the_error_matches_the_type_of_the_arg { + @Test void returns_a_handler_with_the_mapped_error() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((Throwable e) -> nextError); + final var handler = EffectHandler.failure(FAILURE).mapError(mapperSpy); + + assertThat(handler.error()).contains(nextError); + + verify(mapperSpy).apply(FAILURE); + } + } + + @Nested class and_the_error_does_not_match_the_type_of_the_arg { + @Test void returns_a_handler_with_the_mapped_error() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((Throwable e) -> nextError); + final var handler = EffectHandler.failure(FAILURE).effect(noop).mapError(mapperSpy); + + assertThat(handler.error()).contains(nextError); + + verify(mapperSpy).apply(FAILURE); + } + } + } + } + + @Nested class when_the_error_is_not_present { + @Test void returns_an_empty_handler_and_never_calls_the_mapper() { + final var runtimeSpy = Spy.function((RuntimeException e) -> FAILURE); + final var throwableSpy = Spy.function((Throwable e) -> FAILURE); + final var handler = EffectHandler.empty(); + final var overloads = List.of( + handler.mapError(RuntimeException.class, runtimeSpy), + handler.mapError(throwableSpy) + ); + + assertThat(overloads).isNotEmpty().allSatisfy(overload -> { + assertThat(overload.error()).isEmpty(); + }); + + verify(runtimeSpy, never()).apply(any()); + verify(throwableSpy, never()).apply(any()); + } + } + } + @Nested class effect { @Nested class when_the_error_is_not_present { @Test void calls_the_effect_callback_and_returns_a_new_handler() throws FileSystemException { diff --git a/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java b/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java index bbb2cc5..5a509aa 100644 --- a/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java @@ -40,7 +40,7 @@ @Nested class from { @Nested class when_the_value_is_not_null { - @Test void returns_a_handler_withThe_value() { + @Test void returns_a_handler_with_the_value() { final var handler = SolveHandler.from(OK); assertThat(handler.success()).containsSame(OK); @@ -480,8 +480,7 @@ @Nested class map { @Nested class when_the_value_is_present { @Test void returns_a_handler_applying_the_mapper_function() { - final var handler = SolveHandler.from("Hello world!") - .map(String::length); + final var handler = SolveHandler.from("Hello world!").map(String::length); assertThat(handler.success()).contains(12); @@ -491,8 +490,7 @@ @Nested class when_the_error_is_present { @Test void returns_a_handler_with_the_previous_error() { - final var handler = SolveHandler.failure(FAILURE) - .map(Object::toString); + final var handler = SolveHandler.failure(FAILURE).map(Object::toString); assertThat(handler.success()).isEmpty(); assertThat(handler.error()).contains(FAILURE); @@ -500,6 +498,86 @@ } } + @Nested class mapError { + @Nested class when_the_error_is_present { + @Nested class and_the_error_type_is_provided { + @Nested class and_the_error_is_an_instance_of_the_provided_type { + @Test void returns_a_handler_with_the_mapped_error() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((FileSystemException e) -> nextError); + final var handler = SolveHandler.failure(FAILURE).mapError(FileSystemException.class, mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).contains(nextError); + + verify(mapperSpy).apply(FAILURE); + } + } + + @Nested class and_the_error_is_not_an_instance_of_the_provided_type { + @Test void returns_a_handler_with_the_value_and_the_mapper_is_never_called() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((FileSystemException e) -> nextError); + final var handler = SolveHandler.failure(FAILURE).mapError(AccessDeniedException.class, mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).get().isEqualTo(FAILURE); + + verify(mapperSpy, never()).apply(any()); + } + } + } + + @Nested class and_the_error_type_is_not_provided { + @Nested class and_the_error_matches_the_type_of_the_arg { + @Test void returns_a_handler_with_the_mapped_error() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((Throwable e) -> nextError); + final var handler = SolveHandler.failure(FAILURE).mapError(mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).contains(nextError); + + verify(mapperSpy).apply(FAILURE); + } + } + + @Nested class and_the_error_does_not_match_the_type_of_the_arg { + @Test void returns_a_handler_with_the_mapped_error() { + final var nextError = new RuntimeException("OTHER"); + final var mapperSpy = Spy.function((Throwable e) -> nextError); + final var handler = SolveHandler.failure(FAILURE).cast(String.class).mapError(mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).contains(nextError); + + verify(mapperSpy).apply(FAILURE); + } + } + } + } + + @Nested class when_the_value_is_present { + @Test void returns_a_handler_with_the_value_and_the_mapper_is_never_called() { + final var runtimeSpy = Spy.function((RuntimeException e) -> FAILURE); + final var throwableSpy = Spy.function((Throwable e) -> FAILURE); + final var handler = SolveHandler.from(OK); + final var overloads = List.of( + handler.mapError(RuntimeException.class, runtimeSpy), + handler.mapError(throwableSpy) + ); + + assertThat(overloads).isNotEmpty().allSatisfy(overload -> { + assertThat(overload.success()).contains(OK); + assertThat(overload.error()).isEmpty(); + }); + + verify(runtimeSpy, never()).apply(any()); + verify(throwableSpy, never()).apply(any()); + } + } + } + @Nested class flatMap { @Nested class when_the_value_is_present { @Test void returns_a_handler_applying_the_mapper_function() {