diff --git a/src/main/java/io/github/joselion/maybe/Maybe.java b/src/main/java/io/github/joselion/maybe/Maybe.java index 5f46ea9..8503609 100644 --- a/src/main/java/io/github/joselion/maybe/Maybe.java +++ b/src/main/java/io/github/joselion/maybe/Maybe.java @@ -1,11 +1,13 @@ package io.github.joselion.maybe; +import java.io.Closeable; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Function; import org.eclipse.jdt.annotation.Nullable; +import io.github.joselion.maybe.helpers.Common; import io.github.joselion.maybe.util.function.ThrowingConsumer; import io.github.joselion.maybe.util.function.ThrowingFunction; import io.github.joselion.maybe.util.function.ThrowingRunnable; @@ -97,8 +99,7 @@ public static ResolveHandler fromResolver(final T try { return ResolveHandler.ofSuccess(resolver.get()); } catch (Throwable e) { // NOSONAR - @SuppressWarnings("unchecked") - final var error = (E) e; + final var error = Common.cast(e); return ResolveHandler.ofError(error); } } @@ -118,8 +119,7 @@ public static EffectHandler fromEffect(final ThrowingRu effect.run(); return EffectHandler.empty(); } catch (Throwable e) { // NOSONAR - @SuppressWarnings("unchecked") - final var error = (E) e; + final var error = Common.cast(e); return EffectHandler.ofError(error); } } @@ -201,6 +201,28 @@ public static ResourceHolder the type of the resource. Extends from {@link AutoCloseable} + * @param the type of error the holder may have + * @param supplier the throwing supplier o the {@link AutoCloseable} resource + * @return a {@link ResourceHolder} which let's you choose to resolve a value + * or run an effect using the prepared resource + */ + public static ResourceHolder solveResource( + final ThrowingSupplier supplier + ) { + return Maybe + .fromResolver(supplier) + .map(ResourceHolder::from) + .orElse(ResourceHolder::failure); + } + /** * If present, maps the value to another using the provided mapper function. * Otherwise, ignores the mapper and returns {@link #nothing()}. @@ -246,14 +268,14 @@ public Maybe flatMap(final Function> mapper) { * @return a {@link ResolveHandler} with either the resolved value, or the * thrown exception to be handled */ - @SuppressWarnings("unchecked") public ResolveHandler resolve(final ThrowingFunction resolver) { try { return value .map(Maybe.partialResolver(resolver)) .orElseThrow(); - } catch (final NoSuchElementException error) { - return ResolveHandler.ofError((E) error); + } catch (final NoSuchElementException e) { + final var error = Common.cast(e); + return ResolveHandler.ofError(error); } } @@ -266,14 +288,14 @@ public ResolveHandler resolve(final ThrowingFunct * @return an {@link EffectHandler} with either the thrown exception to be * handled or nothing */ - @SuppressWarnings("unchecked") public EffectHandler runEffect(final ThrowingConsumer effect) { try { return value .map(Maybe.partialEffect(effect)) .orElseThrow(); - } catch (final NoSuchElementException error) { - return EffectHandler.ofError((E) error); + } catch (final NoSuchElementException e) { + final var error = Common.cast(e); + return EffectHandler.ofError(error); } } diff --git a/src/main/java/io/github/joselion/maybe/ResolveHandler.java b/src/main/java/io/github/joselion/maybe/ResolveHandler.java index 6f309e8..1bcdc42 100644 --- a/src/main/java/io/github/joselion/maybe/ResolveHandler.java +++ b/src/main/java/io/github/joselion/maybe/ResolveHandler.java @@ -7,6 +7,7 @@ import org.eclipse.jdt.annotation.Nullable; +import io.github.joselion.maybe.helpers.Common; import io.github.joselion.maybe.util.Either; import io.github.joselion.maybe.util.function.ThrowingConsumer; import io.github.joselion.maybe.util.function.ThrowingFunction; @@ -60,7 +61,7 @@ static ResolveHandler ofError(final E error) { * @return the possible success value */ Optional success() { - return value.rightToOptional(); + return this.value.rightToOptional(); } /** @@ -69,7 +70,7 @@ Optional success() { * @return the possible thrown exception */ Optional error() { - return value.leftToOptional(); + return this.value.leftToOptional(); } /** @@ -80,7 +81,7 @@ Optional error() { * @return the same handler to continue chainning operations */ public ResolveHandler doOnSuccess(final Consumer effect) { - value.doOnRight(effect); + this.value.doOnRight(effect); return this; } @@ -96,7 +97,7 @@ public ResolveHandler doOnSuccess(final Consumer effect) { * @return the same handler to continue chainning operations */ public ResolveHandler doOnError(final Class ofType, final Consumer effect) { - value.leftToOptional() + this.value.leftToOptional() .filter(ofType::isInstance) .map(ofType::cast) .ifPresent(effect); @@ -112,7 +113,7 @@ public ResolveHandler doOnError(final Class ofTyp * @return the same handler to continue chainning operations */ public ResolveHandler doOnError(final Consumer effect) { - value.doOnLeft(effect); + this.value.doOnLeft(effect); return this; } @@ -130,7 +131,7 @@ public ResolveHandler doOnError(final Consumer effect) { * provided type was caught. The same handler instance otherwise */ public ResolveHandler catchError(final Class ofType, final Function handler) { - return value + return this.value .leftToOptional() .filter(ofType::isInstance) .map(ofType::cast) @@ -149,7 +150,7 @@ public ResolveHandler catchError(final Class ofType, fina * handler instance otherwise */ public ResolveHandler catchError(final Function handler) { - return value + return this.value .mapLeft(handler) .mapLeft(ResolveHandler::ofSuccess) .leftOrElse(this); @@ -176,7 +177,7 @@ public ResolveHandler resolve( final ThrowingFunction onSuccess, final ThrowingFunction onError ) { - return value.unwrap( + return this.value.unwrap( Maybe.partialResolver(onError), Maybe.partialResolver(onSuccess) ); @@ -192,12 +193,13 @@ public ResolveHandler resolve( * resolves another * @return a new handler with either the resolved value or an error */ - @SuppressWarnings("unchecked") public ResolveHandler resolve(final ThrowingFunction resolver) { - return value.unwrap( - error -> ResolveHandler.ofError((X) error), - Maybe.partialResolver(resolver) - ); + return this.value + .mapLeft(Common::cast) + .unwrap( + ResolveHandler::ofError, + Maybe.partialResolver(resolver) + ); } /** @@ -214,7 +216,7 @@ public EffectHandler runEffect( final ThrowingConsumer onSuccess, final ThrowingConsumer onError ) { - return value.unwrap( + return this.value.unwrap( Maybe.partialEffect(onError), Maybe.partialEffect(onSuccess) ); @@ -230,12 +232,13 @@ public EffectHandler runEffect( * @return a new {@link EffectHandler} representing the result of the success * callback or containg the error */ - @SuppressWarnings("unchecked") public EffectHandler runEffect(final ThrowingConsumer effect) { - return value.unwrap( - error -> EffectHandler.ofError((X) error), - Maybe.partialEffect(effect) - ); + return this.value + .mapLeft(Common::cast) + .unwrap( + EffectHandler::ofError, + Maybe.partialEffect(effect) + ); } /** @@ -248,7 +251,7 @@ public EffectHandler runEffect(final ThrowingConsumer ResolveHandler map(final Function mapper) { - return value + return this.value .mapRight(mapper) .unwrap( ResolveHandler::ofError, @@ -267,7 +270,7 @@ public ResolveHandler map(final Function mapper) { * error */ public ResolveHandler cast(final Class type) { - return value.unwrap( + return this.value.unwrap( error -> ofError(new ClassCastException(error.getMessage())), success -> { try { @@ -286,7 +289,7 @@ public ResolveHandler cast(final Class type) { * @return the resolved value if present. Another value otherwise */ public T orElse(final T fallback) { - return value.rightOrElse(fallback); + return this.value.rightOrElse(fallback); } /** @@ -299,7 +302,7 @@ public T orElse(final T fallback) { * @return the resolved value if present. Another value otherwise */ public T orElse(final Function mapper) { - return value.unwrap(mapper, Function.identity()); + return this.value.unwrap(mapper, Function.identity()); } /** @@ -314,7 +317,7 @@ public T orElse(final Function mapper) { * @return the resolved value if present. Another value otherwise */ public T orElseGet(final Supplier supplier) { - return value + return this.value .rightToOptional() .orElseGet(supplier); } @@ -332,7 +335,7 @@ public T orElseGet(final Supplier supplier) { * @return the resolved value if present. Just {@code null} otherwise. */ public @Nullable T orNull() { - return value.rightOrNull(); + return this.value.rightOrNull(); } /** @@ -342,9 +345,9 @@ public T orElseGet(final Supplier supplier) { * @throws E the error thrown by the {@code resolve} operation */ public T orThrow() throws E { - return value + return this.value .rightToOptional() - .orElseThrow(value::leftOrNull); + .orElseThrow(this.value::leftOrNull); } /** @@ -357,9 +360,9 @@ public T orThrow() throws E { * @throws X a mapped exception */ public T orThrow(final Function mapper) throws X { - return value + return this.value .rightToOptional() - .orElseThrow(() -> mapper.apply(value.leftOrNull())); + .orElseThrow(() -> mapper.apply(this.value.leftOrNull())); } /** @@ -369,7 +372,7 @@ public T orThrow(final Function mapper) throws X { * @return the resolved value wrapped in a {@link Maybe} or holding the error */ public Maybe toMaybe() { - return value + return this.value .rightToOptional() .map(Maybe::just) .orElseGet(Maybe::nothing); @@ -384,7 +387,7 @@ public Maybe toMaybe() { * {@code empty} optional otherwise. */ public Optional toOptional() { - return value.rightToOptional(); + return this.value.rightToOptional(); } /** @@ -399,7 +402,7 @@ public Optional toOptional() { * error on the left */ public Either toEither() { - return value; + return this.value; } /** @@ -418,11 +421,41 @@ public Either toEither() { * @see ResourceHolder#runEffectClosing(ThrowingConsumer) */ public ResourceHolder mapToResource(final Function mapper) { - return value + return this.value .mapRight(mapper) .unwrap( ResourceHolder::failure, ResourceHolder::from ); } + + /** + * Resolve a function that may create an {@link AutoCloseable} resource using + * the value in the handle, (if any). If the function is resolved it returns + * a {@link ResourceHolder} that will close the resource after used. If the + * function does not resolves or the value is not present, the error is + * propagated to the {@link ResourceHolder}. + * + * @param the type of the {@link AutoCloseable} resource + * @param the error type the solver function may throw + * @param solver a function that returns either a resource or throws an exception + * @return a {@link ResourceHolder} with the solved resource if the value is + * present or the error otherwise. + */ + public ResourceHolder solveResource( + final ThrowingFunction solver + ) { + return this.value + .mapLeft(Common::cast) + .unwrap( + ResourceHolder::failure, + prev -> + Maybe + .just(prev) + .resolve(solver) + .map(ResourceHolder::from) + .catchError(ResourceHolder::failure) + .orElse(ResourceHolder::failure) + ); + } } diff --git a/src/main/java/io/github/joselion/maybe/ResourceHolder.java b/src/main/java/io/github/joselion/maybe/ResourceHolder.java index fdbbacc..ccb5b16 100644 --- a/src/main/java/io/github/joselion/maybe/ResourceHolder.java +++ b/src/main/java/io/github/joselion/maybe/ResourceHolder.java @@ -2,6 +2,7 @@ import java.util.Optional; +import io.github.joselion.maybe.helpers.Common; import io.github.joselion.maybe.util.Either; import io.github.joselion.maybe.util.function.ThrowingConsumer; import io.github.joselion.maybe.util.function.ThrowingFunction; @@ -89,18 +90,20 @@ Optional error() { * @return a {@link ResolveHandler} with either the value resolved or the thrown * exception to be handled */ - @SuppressWarnings("unchecked") public ResolveHandler resolveClosing(final ThrowingFunction resolver) { - return value.unwrap( - error -> ResolveHandler.ofError((X) error), - resource -> { - try (var res = resource) { - return ResolveHandler.ofSuccess(resolver.apply(res)); - } catch (final Throwable e) { //NOSONAR - final var error = (X) e; - return ResolveHandler.ofError(error); + return value + .mapLeft(Common::cast) + .unwrap( + ResolveHandler::ofError, + resource -> { + try (var res = resource) { + return ResolveHandler.ofSuccess(resolver.apply(res)); + } catch (final Throwable e) { //NOSONAR + final var error = Common.cast(e); + return ResolveHandler.ofError(error); + } } - }); + ); } /** @@ -119,19 +122,20 @@ public ResolveHandler resolveClosing(final Throwi * @return an {@link EffectHandler} with either the thrown exception to be * handled or nothing */ - @SuppressWarnings("unchecked") public EffectHandler runEffectClosing(final ThrowingConsumer effect) { - return value.unwrap( - error -> EffectHandler.ofError((X) error), - resource -> { - try (var res = resource) { - effect.accept(res); - return EffectHandler.empty(); - } catch (final Throwable e) { // NOSONAR - final var error = (X) e; - return EffectHandler.ofError(error); + return value + .mapLeft(Common::cast) + .unwrap( + EffectHandler::ofError, + resource -> { + try (var res = resource) { + effect.accept(res); + return EffectHandler.empty(); + } catch (final Throwable e) { // NOSONAR + final var error = Common.cast(e); + return EffectHandler.ofError(error); + } } - } - ); + ); } } diff --git a/src/main/java/io/github/joselion/maybe/helpers/Common.java b/src/main/java/io/github/joselion/maybe/helpers/Common.java new file mode 100644 index 0000000..4ebc6be --- /dev/null +++ b/src/main/java/io/github/joselion/maybe/helpers/Common.java @@ -0,0 +1,13 @@ +package io.github.joselion.maybe.helpers; + +public class Common { + + Common() { + throw new UnsupportedOperationException("Cannot instantiate a helper class"); + } + + @SuppressWarnings("unchecked") + public static T cast(final Object value) { + return (T) value; + } +} diff --git a/src/main/java17/io/github/joselion/maybe/Maybe.java b/src/main/java17/io/github/joselion/maybe/Maybe.java index 1849c5a..78304fc 100644 --- a/src/main/java17/io/github/joselion/maybe/Maybe.java +++ b/src/main/java17/io/github/joselion/maybe/Maybe.java @@ -1,11 +1,13 @@ package io.github.joselion.maybe; +import java.io.Closeable; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Function; import org.eclipse.jdt.annotation.Nullable; +import io.github.joselion.maybe.helpers.Common; import io.github.joselion.maybe.util.function.ThrowingConsumer; import io.github.joselion.maybe.util.function.ThrowingFunction; import io.github.joselion.maybe.util.function.ThrowingRunnable; @@ -97,8 +99,7 @@ public static ResolveHandler fromResolver(final T try { return ResolveHandler.ofSuccess(resolver.get()); } catch (Throwable e) { // NOSONAR - @SuppressWarnings("unchecked") - final var error = (E) e; + final var error = Common.cast(e); return ResolveHandler.ofError(error); } } @@ -118,8 +119,7 @@ public static EffectHandler fromEffect(final ThrowingRu effect.run(); return EffectHandler.empty(); } catch (Throwable e) { // NOSONAR - @SuppressWarnings("unchecked") - final var error = (E) e; + final var error = Common.cast(e); return EffectHandler.ofError(error); } } @@ -201,6 +201,28 @@ public static ResourceHolder the type of the resource. Extends from {@link AutoCloseable} + * @param the type of error the holder may have + * @param supplier the throwing supplier o the {@link AutoCloseable} resource + * @return a {@link ResourceHolder} which let's you choose to resolve a value + * or run an effect using the prepared resource + */ + public static ResourceHolder solveResource( + final ThrowingSupplier supplier + ) { + return Maybe + .fromResolver(supplier) + .map(ResourceHolder::from) + .orElse(ResourceHolder::failure); + } + /** * If present, maps the value to another using the provided mapper function. * Otherwise, ignores the mapper and returns {@link #nothing()}. @@ -246,14 +268,14 @@ public Maybe flatMap(final Function> mapper) { * @return a {@link ResolveHandler} with either the resolved value, or the * thrown exception to be handled */ - @SuppressWarnings("unchecked") public ResolveHandler resolve(final ThrowingFunction resolver) { try { return value .map(Maybe.partialResolver(resolver)) .orElseThrow(); - } catch (final NoSuchElementException error) { - return ResolveHandler.ofError((E) error); + } catch (final NoSuchElementException e) { + final var error = Common.cast(e); + return ResolveHandler.ofError(error); } } @@ -266,14 +288,14 @@ public ResolveHandler resolve(final ThrowingFunct * @return an {@link EffectHandler} with either the thrown exception to be * handled or nothing */ - @SuppressWarnings("unchecked") public EffectHandler runEffect(final ThrowingConsumer effect) { try { return value .map(Maybe.partialEffect(effect)) .orElseThrow(); - } catch (final NoSuchElementException error) { - return EffectHandler.ofError((E) error); + } catch (final NoSuchElementException e) { + final var error = Common.cast(e); + return EffectHandler.ofError(error); } } diff --git a/src/test/java/io/github/joselion/maybe/MaybeTest.java b/src/test/java/io/github/joselion/maybe/MaybeTest.java index f20fdbe..5602181 100644 --- a/src/test/java/io/github/joselion/maybe/MaybeTest.java +++ b/src/test/java/io/github/joselion/maybe/MaybeTest.java @@ -1,6 +1,7 @@ package io.github.joselion.maybe; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.INPUT_STREAM; import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE; import static org.assertj.core.api.InstanceOfAssertFactories.optional; import static org.mockito.ArgumentMatchers.any; @@ -9,11 +10,11 @@ import static org.mockito.Mockito.verify; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.NoSuchElementException; import java.util.Optional; -import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -181,19 +182,43 @@ } @Nested class withResource { - @Test void returns_the_resource_spec_with_the_resource() { - try ( - var softly = new AutoCloseableSoftAssertions(); - var fis = Maybe.just("./src/test/resources/readTest.txt") - .resolve(FileInputStream::new) - .orThrow(Error::new); - ) { - softly.assertThat(Maybe.withResource(fis).resource()) + @Test void returns_the_ResourceHolder_with_the_resource() throws FileNotFoundException, IOException { + try (var fis = new FileInputStream("./src/test/resources/readTest.txt")) { + final var holder = Maybe.withResource(fis); + + assertThat(holder.resource()) .isPresent() .containsInstanceOf(FileInputStream.class) .containsSame(fis); - } catch (Throwable error) { - throw new Error(error); + assertThat(holder.error()).isEmpty(); + } + } + } + + @Nested class solveResource { + @Nested class and_the_solver_does_not_throw { + @Test void returns_a_ResourceHolder_with_the_resource() throws FileNotFoundException, IOException { + final var path = "./src/test/resources/readTest.txt"; + final var holder = Maybe.solveResource(() -> new FileInputStream(path)); + + assertThat(holder.resource()) + .isPresent() + .containsInstanceOf(FileInputStream.class) + .get() + .asInstanceOf(INPUT_STREAM) + .hasContent("foo"); + assertThat(holder.error()).isEmpty(); + } + } + + @Nested class and_the_solver_throws { + @Test void returns_a_ResourceHolder_with_the_thrown_exception() { + final var holder = Maybe.solveResource(() -> new FileInputStream("invalid.txt")); + + assertThat(holder.resource()).isEmpty(); + assertThat(holder.error()) + .isPresent() + .containsInstanceOf(FileNotFoundException.class); } } } diff --git a/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java b/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java index ecd60e0..1af67a9 100644 --- a/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java +++ b/src/test/java/io/github/joselion/maybe/ResolveHandlerTest.java @@ -526,4 +526,53 @@ } } } + + @Nested class solveResource { + @Nested class when_the_value_is_present { + @Nested class and_the_solver_does_not_throw { + @Test void returns_a_ResourceHolder_with_the_resource() { + final var path = "./src/test/resources/readTest.txt"; + final var holder = Maybe + .just(path) + .resolve(ThrowingFunction.identity()) + .solveResource(FileInputStream::new); + + assertThat(holder.resource()) + .isPresent() + .containsInstanceOf(FileInputStream.class) + .get() + .asInstanceOf(INPUT_STREAM) + .hasContent("foo"); + assertThat(holder.error()).isEmpty(); + } + } + + @Nested class and_the_solver_throws { + @Test void returns_a_ResourceHolder_with_the_thrown_exception() { + final var holder = Maybe + .just("invalid.txt") + .resolve(ThrowingFunction.identity()) + .solveResource(FileInputStream::new); + + assertThat(holder.resource()).isEmpty(); + assertThat(holder.error()) + .isPresent() + .containsInstanceOf(FileNotFoundException.class); + } + } + } + + @Nested class when_the_error_is_present { + @Test void returns_a_ResourceHolder_with_the_propagated_error() { + final var holder = Maybe + .fromResolver(throwingOp) + .solveResource(FileInputStream::new); + + assertThat(holder.resource()).isEmpty(); + assertThat(holder.error()) + .isPresent() + .containsInstanceOf(FileSystemException.class); + } + } + } } diff --git a/src/test/java/io/github/joselion/maybe/helpers/CommonTest.java b/src/test/java/io/github/joselion/maybe/helpers/CommonTest.java new file mode 100644 index 0000000..2363ab8 --- /dev/null +++ b/src/test/java/io/github/joselion/maybe/helpers/CommonTest.java @@ -0,0 +1,39 @@ +package io.github.joselion.maybe.helpers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import io.github.joselion.testing.UnitTest; + +@UnitTest class CommonTest { + + @Nested class helper { + @Nested class when_the_class_is_instantiated { + @Test void throws_an_UnsupportedOperationException() { + assertThatCode(Common::new) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("Cannot instantiate a helper class"); + } + } + } + + @Nested class cast { + @Nested class when_the_value_can_be_cast { + @Test void returns_the_value_as_the_parameter_type() { + final Number value = 3; + + assertThat(Common.cast(value)).isInstanceOf(Integer.class); + } + } + + @Nested class when_the_value_cannot_be_cast { + @Test void throws_a_ClassCastException() { + assertThatCode(() -> Common.cast("3").intValue()) // NOSONAR + .isInstanceOf(ClassCastException.class); + } + } + } +} diff --git a/src/test/java/io/github/joselion/testing/Spy.java b/src/test/java/io/github/joselion/testing/Spy.java index 10b008e..2586459 100644 --- a/src/test/java/io/github/joselion/testing/Spy.java +++ b/src/test/java/io/github/joselion/testing/Spy.java @@ -1,17 +1,17 @@ package io.github.joselion.testing; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.AdditionalAnswers.delegatesTo; import org.mockito.Mockito; +import io.github.joselion.maybe.helpers.Common; + public class Spy { - @SuppressWarnings("unchecked") public static T lambda(final T lambda) { - Class[] interfaces = lambda.getClass().getInterfaces(); - assertThat(interfaces).hasSize(1); + final var interfaces = lambda.getClass().getInterfaces(); + final var toMock = Common.>cast(interfaces[0]); - return Mockito.mock((Class) interfaces[0], delegatesTo(lambda)); + return Mockito.mock(toMock, delegatesTo(lambda)); } }