From 9d51c1ac9a144bbaa4799aa7280925b1b2900314 Mon Sep 17 00:00:00 2001 From: Jose Luis Leon Date: Tue, 13 Feb 2024 00:02:45 -0500 Subject: [PATCH] feat(core): Add .cast(..) operations overloads (#236) --- .../java/io/github/joselion/maybe/Maybe.java | 35 ++++++-- .../github/joselion/maybe/SolveHandler.java | 33 +++++-- .../io/github/joselion/maybe/Maybe.java | 35 ++++++-- .../io/github/joselion/maybe/MaybeTest.java | 89 +++++++++++++++---- .../joselion/maybe/SolveHandlerTest.java | 63 +++++++++---- 5 files changed, 198 insertions(+), 57 deletions(-) diff --git a/src/main/java/io/github/joselion/maybe/Maybe.java b/src/main/java/io/github/joselion/maybe/Maybe.java index af3b907..0a9b542 100644 --- a/src/main/java/io/github/joselion/maybe/Maybe.java +++ b/src/main/java/io/github/joselion/maybe/Maybe.java @@ -317,18 +317,37 @@ public EffectHandler effect(final ThrowingConsumer the type that the value will be cast to + * @param the type of the cast value * @param type the class instance of the type to cast - * @return a new {@code Maybe} with the cast value if it can be cast, - * {@link #empty()} otherwise + * @return a handler with either the cast value or a ClassCastException error */ public SolveHandler cast(final Class type) { - return Maybe - .of(this.value) - .solve(type::cast); + return this.solve(type::cast); + } + + /** + * 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. + * + * @param the type of the cast value + * @param the type of the mapped exception + * @param type the class instance of the type to cast + * @param onError a function to map the error in case of failure + * @return a handler with either the cast value or the mapped error + */ + public SolveHandler cast( + final Class type, + final Function onError + ) { + return this + .solve(type::cast) + .mapError(onError); } /** diff --git a/src/main/java/io/github/joselion/maybe/SolveHandler.java b/src/main/java/io/github/joselion/maybe/SolveHandler.java index b4981d0..f9e5c99 100644 --- a/src/main/java/io/github/joselion/maybe/SolveHandler.java +++ b/src/main/java/io/github/joselion/maybe/SolveHandler.java @@ -402,19 +402,40 @@ public SolveHandler flatMap(final Function the type the value will be cast to + * @param the type of the cast value * @param type the class instance of the type to cast - * @return a new handler with either the cast value or a ClassCastException - * error + * @return a handler with either the cast value or a ClassCastException error */ public SolveHandler cast(final Class type) { return this.solve(type::cast); } + /** + * 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. + * + * @param the type of the cast value + * @param the type of the mapped exception + * @param type the class instance of the type to cast + * @param onError a function to map the error in case of failure + * @return a handler with either the cast value or the mapped error + */ + public SolveHandler cast( + final Class type, + final Function onError + ) { + return this + .solve(type::cast) + .mapError(ClassCastException.class, onError); + } + /** * Returns the solved value if present. Another value otherwise. * diff --git a/src/main/java17/io/github/joselion/maybe/Maybe.java b/src/main/java17/io/github/joselion/maybe/Maybe.java index 3787a2c..1e170c3 100644 --- a/src/main/java17/io/github/joselion/maybe/Maybe.java +++ b/src/main/java17/io/github/joselion/maybe/Maybe.java @@ -318,18 +318,37 @@ public EffectHandler effect(final ThrowingConsumer the type that the value will be cast to + * @param the type of the cast value * @param type the class instance of the type to cast - * @return a new {@code Maybe} with the cast value if it can be cast, - * {@link #empty()} otherwise + * @return a handler with either the cast value or a ClassCastException error */ public SolveHandler cast(final Class type) { - return Maybe - .of(this.value) - .solve(type::cast); + return this.solve(type::cast); + } + + /** + * 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. + * + * @param the type of the cast value + * @param the type of the mapped exception + * @param type the class instance of the type to cast + * @param onError a function to map the error in case of failure + * @return a handler with either the cast value or the mapped error + */ + public SolveHandler cast( + final Class type, + final Function onError + ) { + return this + .solve(type::cast) + .mapError(onError); } /** diff --git a/src/test/java/io/github/joselion/maybe/MaybeTest.java b/src/test/java/io/github/joselion/maybe/MaybeTest.java index 028deb0..b9530c9 100644 --- a/src/test/java/io/github/joselion/maybe/MaybeTest.java +++ b/src/test/java/io/github/joselion/maybe/MaybeTest.java @@ -11,6 +11,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; @@ -29,22 +30,22 @@ private static final String OK = "OK"; - private static final IOException FAIL_EXCEPTION = new IOException("FAIL"); + private static final IOException FAILURE = new IOException("FAIL"); private final ThrowingFunction failFunction = val -> { - throw FAIL_EXCEPTION; + throw FAILURE; }; private final ThrowingSupplier failSupplier = () -> { - throw FAIL_EXCEPTION; + throw FAILURE; }; private final ThrowingConsumer failConsumer = it -> { - throw FAIL_EXCEPTION; + throw FAILURE; }; private final ThrowingRunnable failRunnable = () -> { - throw FAIL_EXCEPTION; + throw FAILURE; }; @Nested class of { @@ -113,7 +114,7 @@ final var handler = Maybe.from(supplierSpy); assertThat(handler.success()).isEmpty(); - assertThat(handler.error()).contains(FAIL_EXCEPTION); + assertThat(handler.error()).contains(FAILURE); verify(supplierSpy).get(); } @@ -137,7 +138,7 @@ final var runnableSpy = Spy.lambda(failRunnable); final var handler = Maybe.from(runnableSpy); - assertThat(handler.error()).contains(FAIL_EXCEPTION); + assertThat(handler.error()).contains(FAILURE); verify(runnableSpy).run(); } @@ -158,7 +159,7 @@ assertThat(Maybe.partial(failureSpy).apply(OK)) .isInstanceOf(SolveHandler.class) .extracting(SolveHandler::error, optional(IOException.class)) - .contains(FAIL_EXCEPTION); + .contains(FAILURE); verify(successSpy).apply(OK); verify(failureSpy).apply(OK); @@ -178,7 +179,7 @@ assertThat(Maybe.partial(failureSpy).apply(OK)) .isInstanceOf(EffectHandler.class) .extracting(EffectHandler::error, optional(IOException.class)) - .contains(FAIL_EXCEPTION); + .contains(FAILURE); verify(successSpy).accept(OK); verify(failureSpy).accept(OK); @@ -314,7 +315,7 @@ .solve(functionSpy); assertThat(handler.success()).isEmpty(); - assertThat(handler.error()).contains(FAIL_EXCEPTION); + assertThat(handler.error()).contains(FAILURE); verify(functionSpy).apply(OK); } @@ -367,7 +368,7 @@ final var handler = Maybe.of(OK) .effect(consumerSpy); - assertThat(handler.error()).contains(FAIL_EXCEPTION); + assertThat(handler.error()).contains(FAILURE); verify(consumerSpy).accept(OK); } @@ -375,19 +376,69 @@ } @Nested class cast { - @Nested class when_the_value_is_castable_to_the_passed_type { - @Test void returns_a_solve_handler_with_the_cast_value() { - final var maybe = Maybe.of(3); + @Nested class when_the_value_is_present { + @Nested class and_the_value_is_an_instance_of_the_type { + @Test void returns_a_solve_handler_with_the_cast_value() { + final var mapperSpy = Spy.function((Throwable e) -> FAILURE); + final var handler = Maybe.of((Number) 3); + final var overloads = List.of( + handler.cast(Integer.class), + handler.cast(Integer.class, mapperSpy) + ); + + assertThat(overloads).isNotEmpty().allSatisfy(overload -> { + assertThat(overload.success()).contains(3); + assertThat(overload.error()).isEmpty(); + }); + + verify(mapperSpy, never()).apply(any()); + } + } - assertThat(maybe.cast(Integer.class).success()).contains(3); + @Nested class and_the_value_is_not_an_instance_of_the_type { + @Nested class and_the_error_mapper_is_not_provided { + @Test void returns_a_solve_handler_with_a_ClassCastException() { + final var handler = Maybe.of("3").cast(Integer.class); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).containsInstanceOf(ClassCastException.class); + } + } + + @Nested class and_the_error_mapper_is_provided { + @Test void returns_a_solve_handler_with_the_mapped_error() { + final var mapperSpy = Spy.function((Throwable e) -> FAILURE); + final var handler = Maybe.of("3").cast(Integer.class, mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).contains(FAILURE); + + verify(mapperSpy).apply(any(ClassCastException.class)); + } + } } } - @Nested class when_the_value_is_not_castable_to_the_passed_type { - @Test void returns_a_solve_handler_with_a_ClassCastException() { - final var maybe = Maybe.of("3"); + @Nested class when_the_value_is_not_present { + @Nested class and_the_error_mapper_is_not_provided { + @Test void returns_a_solve_handler_with_a_NoSuchElementException() { + final var handler = Maybe.empty().cast(String.class); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).containsInstanceOf(NoSuchElementException.class); + } + } - assertThat(maybe.cast(Integer.class).error()).containsInstanceOf(ClassCastException.class); + @Nested class and_the_error_mapper_is_provided { + @Test void returns_a_solve_handler_with_the_mapped_error() { + final var mapperSpy = Spy.function((Throwable e) -> FAILURE); + final var handler = Maybe.empty().cast(String.class, mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).contains(FAILURE); + + verify(mapperSpy).apply(any(NoSuchElementException.class)); + } } } } diff --git a/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java b/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java index 5a509aa..3850eb6 100644 --- a/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/SolveHandlerTest.java @@ -602,36 +602,67 @@ @Nested class cast { @Nested class when_the_value_is_present { - @Nested class and_the_object_can_be_cast { + @Nested class and_the_value_is_an_instance_of_the_type { @Test void returns_a_handler_with_the_cast_value() { final var anyValue = (Object) "Hello"; - final var handler = SolveHandler.from(anyValue) - .cast(String.class); + final var mapperSpy = Spy.function((ClassCastException e) -> new RuntimeException(e)); + final var handler = SolveHandler.from(anyValue); + final var overloads = List.of( + handler.cast(String.class), + handler.cast(String.class, mapperSpy) + ); - assertThat(handler.success()).contains("Hello"); - assertThat(handler.error()).isEmpty(); + assertThat(overloads).isNotEmpty().allSatisfy(overload -> { + assertThat(overload.success()).contains("Hello"); + assertThat(overload.error()).isEmpty(); + }); + + verify(mapperSpy, never()).apply(any()); } } - @Nested class and_the_object_can_not_be_cast { - @Test void returns_a_handler_with_the_cast_exception() { - final var handler = SolveHandler.from(3).cast(String.class); + @Nested class and_the_value_is_not_an_instance_of_the_type { + @Nested class and_the_error_mapper_is_not_provided { + @Test void returns_a_handler_with_a_ClassCastException() { + final var handler = SolveHandler.from(3).cast(String.class); - assertThat(handler.success()).isEmpty(); - assertThat(handler.error()) - .get(THROWABLE) - .isExactlyInstanceOf(ClassCastException.class) - .hasMessage("Cannot cast java.lang.Integer to java.lang.String"); + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()) + .get(THROWABLE) + .isExactlyInstanceOf(ClassCastException.class) + .hasMessage("Cannot cast java.lang.Integer to java.lang.String"); + } + } + + @Nested class and_the_error_mapper_is_provided { + @Test void returns_a_handler_with_the_mapped_error() { + final var mapperSpy = Spy.function((ClassCastException e) -> FAILURE); + final var handler = SolveHandler.from(3).cast(String.class, mapperSpy); + + assertThat(handler.success()).isEmpty(); + assertThat(handler.error()).contains(FAILURE); + + verify(mapperSpy).apply(any(ClassCastException.class)); + } } } } @Nested class when_the_error_is_present { @Test void returns_a_handler_with_the_error() { - final var handler = SolveHandler.failure(FAILURE).cast(String.class); + final var mapperSpy = Spy.function((ClassCastException e) -> new RuntimeException(e)); + final var handler = SolveHandler.failure(FAILURE); + final var overloads = List.of( + handler.cast(String.class), + handler.cast(String.class, mapperSpy) + ); - assertThat(handler.success()).isEmpty(); - assertThat(handler.error()).get().isSameAs(FAILURE); + assertThat(overloads).isNotEmpty().allSatisfy(overload -> { + assertThat(overload.success()).isEmpty(); + assertThat(overload.error()).get().isSameAs(FAILURE); + }); + + verify(mapperSpy, never()).apply(any()); } } }