diff --git a/java-commons/src/main/java/ru/progrm_jarvis/javacommons/object/Result.java b/java-commons/src/main/java/ru/progrm_jarvis/javacommons/object/Result.java index e7e26f0f..2c225234 100644 --- a/java-commons/src/main/java/ru/progrm_jarvis/javacommons/object/Result.java +++ b/java-commons/src/main/java/ru/progrm_jarvis/javacommons/object/Result.java @@ -8,6 +8,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import ru.progrm_jarvis.javacommons.annotation.Any; +import ru.progrm_jarvis.javacommons.util.SneakyThrower; +import ru.progrm_jarvis.javacommons.util.TypeHints; +import ru.progrm_jarvis.javacommons.util.TypeHints.TypeHint; import ru.progrm_jarvis.javacommons.util.function.ThrowingRunnable; import ru.progrm_jarvis.javacommons.util.function.ThrowingSupplier; @@ -15,6 +18,7 @@ import java.util.concurrent.Callable; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -138,6 +142,53 @@ public interface Result extends Supplier { return optional.>map(Result::success).orElseGet(() -> error(errorSupplier.get())); } + /** + * Creates a result by running the specified runnable. + * + * @param runnable function whose failure indicates the {@link #error(Object) error result} + * @param throwableType class instance representing the type of the thrown exception + * @param type of the thrown throwable + * @return {@link #nullSuccess() successful void-result} if the runnable runs unexceptionally + * or an {@link #error(Object) error result} containing the thrown {@link Throwable throwable} + * if {@link Class#isInstance(Object) it is of} the expected type + * @apiNote if an unexpected exception is thrown then it will be rethrown + */ + @SuppressWarnings("unchecked") + static @NotNull Result<@Nullable Void, @NotNull X> tryRun( + final @NonNull ThrowingRunnable runnable, + final @NonNull Class throwableType + ) { + //noinspection OverlyBroadCatchBlock: cannot catch generic exception type + try { + runnable.runChecked(); + } catch (final Throwable x) { + if (throwableType.isInstance(x)) return error((X) x); + + return SneakyThrower.sneakyThrow(x); + } + + return nullSuccess(); + } + + /** + * Creates a result by running the specified runnable. + * + * @param runnable function whose failure indicates the {@link #error(Object) error result} + * @param throwableTypeHint array used for throwable type discovery + * @param type of the thrown throwable + * @return {@link #nullSuccess() successful void-result} if the runnable runs unexceptionally + * or an {@link #error(Object) error result} containing the thrown {@link Throwable throwable} + * if {@link Class#isInstance(Object) it is of} the expected type + * @apiNote if an unexpected exception is thrown then it will be rethrown + */ + @SafeVarargs + static @NotNull Result<@Nullable Void, @NotNull X> tryRun( + final @NonNull ThrowingRunnable runnable, + @TypeHint final @Nullable X @NonNull ... throwableTypeHint + ) { + return tryRun(runnable, TypeHints.resolve(throwableTypeHint)); + } + /** * Creates a result by running the specified runnable. * @@ -145,7 +196,7 @@ public interface Result extends Supplier { * @return {@link #nullSuccess() successful void-result} if the runnable runs unexceptionally * or an {@link #error(Object) error result} containing the thrown {@link Throwable throwable} otherwise */ - static @NotNull Result<@Nullable Void, ? extends @NotNull Throwable> tryRun( + static @NotNull Result<@Nullable Void, ? extends @NotNull Throwable> tryRunCatchAny( final @NonNull ThrowingRunnable runnable ) { try { @@ -161,16 +212,67 @@ public interface Result extends Supplier { * Creates a result by getting the value of the specified supplier. * * @param supplier provider of the result whose failure indicates the {@link #error(Object) error result} + * @param throwableType class instance representing the type of the thrown exception + * @param type of the {@link #success(Object) successful result} provided by the given supplier + * @return {@link #success(Object) successful result} if the supplier provides the value unexceptionally + * or an {@link #error(Object) error result} containing the thrown {@link Throwable throwable} + * if {@link Class#isInstance(Object) it is of} the expected type + * if {@link Class#isInstance(Object) it is of} the expected type + * @apiNote if an unexpected exception is thrown then it will be rethrown + */ + @SuppressWarnings("unchecked") + static Result tryGet( + final @NonNull ThrowingSupplier supplier, + final @NonNull Class throwableType + ) { + final T value; + //noinspection OverlyBroadCatchBlock: cannot catch generic exception type + try { + value = supplier.getChecked(); + } catch (final Throwable x) { + if (throwableType.isInstance(x)) return error((X) x); + + return SneakyThrower.sneakyThrow(x); + } + + return success(value); + } + + /** + * Creates a result by getting the value of the specified supplier. + * + * @param supplier provider of the result whose failure indicates the {@link #error(Object) error result} + * @param throwableTypeHint array used for throwable type discovery + * @param type of the thrown throwable + * @param type of the {@link #success(Object) successful result} provided by the given supplier * @return {@link #success(Object) successful result} if the supplier provides the value unexceptionally * or an {@link #error(Object) error result} containing the thrown {@link Throwable throwable} otherwise + * if {@link Class#isInstance(Object) it is of} the expected type + * @apiNote if an unexpected exception is thrown then it will be rethrown */ - static Result tryGet( - final @NonNull ThrowingSupplier supplier + @SafeVarargs + static Result tryGet( + final @NonNull ThrowingSupplier supplier, + @TypeHint final @Nullable X @NonNull ... throwableTypeHint + ) { + return tryGet(supplier, TypeHints.resolve(throwableTypeHint)); + } + + /** + * Creates a result by getting the value of the specified supplier. + * + * @param type of the successful result value + * @param supplier provider of the result whose failure indicates the {@link #error(Object) error result} + * @return {@link #success(Object) successful result} if the supplier provides the value unexceptionally + * or an {@link #error(Object) error result} containing the thrown {@link Throwable throwable} + */ + static Result tryGetCatchAny( + final @NonNull ThrowingSupplier supplier ) { final T value; try { - value = supplier.get(); - } catch (final Exception x) { + value = supplier.getChecked(); + } catch (final Throwable x) { return error(x); } @@ -181,7 +283,7 @@ public interface Result extends Supplier { * Creates a result by calling the specified callable. * * @param callable provider of the result whose failure indicates the {@link #error(Object) error result} - * @param type of the result provided by the given callable + * @param type of the {@link #success(Object) successful result} provided by the given supplier * @return {@link #success(Object) successful result} if the callable completes unexceptionally * or an {@link #error(Object) error result} containing the thrown {@link Exception exception} otherwise */ @@ -200,7 +302,7 @@ public interface Result extends Supplier { * Creates a result by calling the specified callable. * * @param callable provider of the result whose failure indicates the {@link #error(Object) error result} - * @param type of the result provided by the given callable + * @param type of the {@link #success(Object) successful result} provided by the given supplier * @return {@link #success(Object) successful result} if the callable completes unexceptionally * or an {@link #error(Object) error result} containing the thrown {@link Exception exception} otherwise * @deprecated in favor of {@link #tryCall(Callable)} @@ -1129,7 +1231,7 @@ public T rethrow( * of the ability to specify {@code super}-bounds on generic parameters relative to the other ones */ @SuppressWarnings("unchecked") // Results are immutable so they are always safe to upcast - public Result upcast(final @NotNull Result result) { + public <@Any T, @Any E> Result upcast(final @NotNull Result result) { return (Result) result; } @@ -1141,7 +1243,97 @@ public Result upcast(final @NotNull Result T any(final @NotNull Result result) { - return Extensions.upcast(result).orComputeDefault(Function.identity()); + return Extensions + .upcast(result) + .orComputeDefault(Function.identity()); + } + + /** + * Conditionally maps the {@link #unwrap() successful value} otherwise applying no changes. + * + * @param result given result + * @param predicate condition on which to apply the mapping to the {@link #unwrap() successful value} + * @param mappingFunction function used to map the {@link #unwrap() successful value} + * if it matches the predicate + * @param type of the successful result value + * @param type of the error result value + * @param type of the resulting successful value, super-type of {@code } + * @return the result whose {@link #unwrap() successful value} is mapped if it matches the predicate + */ + public @NotNull Result mapIf( + final @NonNull Result result, + final @NonNull Predicate predicate, + final @NonNull Function mappingFunction + ) { + return Extensions + .upcast(result) + .map(value -> predicate.test(value) ? mappingFunction.apply(value) : value); + } + + /** + * Conditionally maps the {@link #unwrap() successful value} otherwise applying no changes. + * + * @param result given result + * @param predicate condition on which to not apply the mapping to the {@link #unwrap() successful value} + * @param mappingFunction function used to map the {@link #unwrap() successful value} + * if it does not match the predicate + * @param type of the successful result value + * @param type of the error result value + * @param type of the resulting successful value, super-type of {@code } + * @return the result whose {@link #unwrap() successful value} is mapped if it does not match the predicate + */ + public @NotNull Result mapIfNot( + final @NonNull Result result, + final @NonNull Predicate predicate, + final @NonNull Function mappingFunction + ) { + return Extensions + .upcast(result) + .map(value -> predicate.test(value) ? value : mappingFunction.apply(value)); + } + + /** + * Conditionally maps the {@link #unwrapError() error value} otherwise applying no changes. + * + * @param result given result + * @param predicate condition on which to apply the mapping to the {@link #unwrapError() error value} + * @param mappingFunction function used to map the {@link #unwrapError() error value} + * if it matches the predicate + * @param type of the successful result value + * @param type of the error result value + * @param type of the resulting error value, super-type of {@code } + * @return the result whose {@link #unwrapError() error value} is mapped if it matches the predicate + */ + public @NotNull Result mapErrorIf( + final @NonNull Result result, + final @NonNull Predicate predicate, + final @NonNull Function mappingFunction + ) { + return Extensions + .upcast(result) + .mapError(error -> predicate.test(error) ? mappingFunction.apply(error) : error); + } + + /** + * Conditionally maps the {@link #unwrapError() error value} otherwise applying no changes. + * + * @param result given result + * @param predicate condition on which to not apply the mapping to the {@link #unwrapError() error value} + * @param mappingFunction function used to map the {@link #unwrapError() error value} + * if it does not match the predicate + * @param type of the successful result value + * @param type of the error result value + * @param type of the resulting error value, super-type of {@code } + * @return the result whose {@link #unwrapError() error value} is mapped if it does not match the predicate + */ + public @NotNull Result mapErrorIfNot( + final @NonNull Result result, + final @NonNull Predicate predicate, + final @NonNull Function mappingFunction + ) { + return Extensions + .upcast(result) + .mapError(error -> predicate.test(error) ? error : mappingFunction.apply(error)); } } } diff --git a/java-commons/src/main/java/ru/progrm_jarvis/javacommons/util/function/ThrowingRunnable.java b/java-commons/src/main/java/ru/progrm_jarvis/javacommons/util/function/ThrowingRunnable.java index de43c06d..98c246b2 100644 --- a/java-commons/src/main/java/ru/progrm_jarvis/javacommons/util/function/ThrowingRunnable.java +++ b/java-commons/src/main/java/ru/progrm_jarvis/javacommons/util/function/ThrowingRunnable.java @@ -13,7 +13,7 @@ * @param the type of the exception thrown by the function */ @FunctionalInterface -public interface ThrowingRunnable extends Runnable{ +public interface ThrowingRunnable extends Runnable { /** * Runs an action. diff --git a/java-commons/src/test/java/ru/progrm_jarvis/javacommons/object/ResultTest.java b/java-commons/src/test/java/ru/progrm_jarvis/javacommons/object/ResultTest.java new file mode 100644 index 00000000..391fde19 --- /dev/null +++ b/java-commons/src/test/java/ru/progrm_jarvis/javacommons/object/ResultTest.java @@ -0,0 +1,142 @@ +package ru.progrm_jarvis.javacommons.object; + +import lombok.NonNull; +import lombok.experimental.StandardException; +import lombok.experimental.UtilityClass; +import lombok.val; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class ResultTest { + + @Test + void test_tryRun_withInferredType_success() { + assertTrue( + Result.tryRun(TestMethods::voidMethodNotThrowingDeclaredIOException).isSuccess() + ); + } + + @Test + void test_tryRun_withInferredType_error() { + val thrown = new IOException("Expected exception"); + assertSame(thrown, + Result.tryRun(() -> TestMethods.voidMethodThrowingDeclaredIOException(thrown)).unwrapError() + ); + } + + @Test + void test_tryRun_withInferredType_rethrow() { + val thrown = new RuntimeException("You didn't expect it, did you?"); + assertSame(thrown, + assertThrows(RuntimeException.class, () -> Result.tryRun( + () -> TestMethods.voidMethodNotThrowingDeclaredIOExceptionButRuntimeException(thrown) + )) + ); + } + + @Test + void test_tryRun_withInferredType_typeInference() { + assertTrue( + Result.tryRun(() -> TestMethods.voidMethodThrowingCustomException(new CustomException())) + .peek(aVoid -> fail("Result should be an error result")) + .peekError(CustomException::thisIsACustomError) + .isError() + ); + } + + @Test + void test_tryGet_withInferredType_success() { + val returned = "Hello world"; + assertSame(returned, + Result.tryGet(() -> TestMethods.stringMethodNotThrowingDeclaredIOException(returned)).unwrap() + ); + } + + @Test + void test_tryGet_withInferredType_error() { + val thrown = new IOException("Expected exception"); + assertSame(thrown, + Result.tryGet(() -> TestMethods.stringMethodThrowingDeclaredIOException(thrown)).unwrapError() + ); + } + + @Test + void test_tryGet_withInferredType_rethrow() { + val thrown = new RuntimeException("You didn't expect it, did you?"); + assertSame(thrown, + assertThrows(RuntimeException.class, () -> Result.tryGet( + () -> TestMethods.stringMethodNotThrowingDeclaredIOExceptionButRuntimeException(thrown) + )) + ); + } + + @Test + void test_tryGet_withInferredType_typeInference() { + assertTrue( + Result.tryGet(() -> TestMethods.stringMethodThrowingCustomException(new CustomException())) + .peek(string -> fail("Result should be an error result")) + .peekError(CustomException::thisIsACustomError) + .isError() + ); + } + + @UtilityClass + private class TestMethods { + + @SuppressWarnings("RedundantThrows") + private void voidMethodNotThrowingDeclaredIOException() throws IOException {} + + private void voidMethodThrowingDeclaredIOException( + final @NonNull IOException thrown + ) throws IOException { + throw thrown; + } + + @SuppressWarnings("RedundantThrows") + private void voidMethodNotThrowingDeclaredIOExceptionButRuntimeException( + final @NonNull RuntimeException thrown + ) throws IOException { + throw thrown; + } + + @SuppressWarnings("RedundantThrows") + private String stringMethodNotThrowingDeclaredIOException(final String returned) throws IOException { + return returned; + } + + private String stringMethodThrowingDeclaredIOException( + final @NonNull IOException thrown + ) throws IOException { + throw thrown; + } + + @SuppressWarnings("RedundantThrows") + private String stringMethodNotThrowingDeclaredIOExceptionButRuntimeException( + final @NonNull RuntimeException thrown + ) throws IOException { + throw thrown; + } + + @SuppressWarnings("RedundantThrows") + private void voidMethodThrowingCustomException( + final @NonNull CustomException customException + ) throws CustomException { + throw customException; + } + + @SuppressWarnings("RedundantThrows") + private String stringMethodThrowingCustomException( + final @NonNull CustomException customException + ) throws CustomException { + throw customException; + } + } + + @StandardException + private final class CustomException extends Exception { + public void thisIsACustomError() {} + } +} \ No newline at end of file