diff --git a/README.md b/README.md index 5036111..45c4ec4 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ class Example { ### Maybe +A Monad. + A non-final substitute for Optional with `peek()` and `stream()` methods. ```java @@ -99,6 +101,8 @@ class Test { ### Result +A Monad. + A container for method return values that may raise an Exception. Useful for when a checked exceptions can't be added to the method signature. diff --git a/src/main/java/net/kemitix/mon/maybe/Just.java b/src/main/java/net/kemitix/mon/maybe/Just.java index 0e480f0..f801bde 100644 --- a/src/main/java/net/kemitix/mon/maybe/Just.java +++ b/src/main/java/net/kemitix/mon/maybe/Just.java @@ -36,7 +36,6 @@ import java.util.stream.Stream; * A Maybe where a value is present. * * @param the type of the content - * * @author Paul Campbell (pcampbell@kemitix.net) */ @RequiredArgsConstructor(access = AccessLevel.PROTECTED) @@ -45,7 +44,12 @@ final class Just implements Maybe { private final T value; @Override - public Maybe map(final Function f) { + public Maybe flatMap(Function> f) { + return f.apply(value); + } + + @Override + public Maybe map(final Function f) { return new Just<>(f.apply(value)); } diff --git a/src/main/java/net/kemitix/mon/maybe/Maybe.java b/src/main/java/net/kemitix/mon/maybe/Maybe.java index e12e83b..b4fffd8 100644 --- a/src/main/java/net/kemitix/mon/maybe/Maybe.java +++ b/src/main/java/net/kemitix/mon/maybe/Maybe.java @@ -24,21 +24,27 @@ package net.kemitix.mon.maybe; import lombok.NonNull; import net.kemitix.mon.Functor; +import java.util.Optional; +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; + /** * A value that may or may not be present. * * @param the type of the content of the Just - * * @author Paul Campbell (pcampbell@kemitix.net) */ -public interface Maybe extends Functor>, MaybeStream, MaybeOptional { +@SuppressWarnings("methodcount") +public interface Maybe extends Functor> { /** * Create a Maybe for the value that is present. * * @param value the value, not null * @param the type of the value - * * @return a Maybe of the value */ static Maybe just(@NonNull final T value) { @@ -49,7 +55,6 @@ public interface Maybe extends Functor>, MaybeStream, MaybeOpt * Create a Maybe for a lack of a value. * * @param the type of the missing value - * * @return an empty Maybe */ @SuppressWarnings("unchecked") @@ -62,7 +67,6 @@ public interface Maybe extends Functor>, MaybeStream, MaybeOpt * * @param value the value, may be null * @param the type of the value - * * @return a Maybe, either a Just, or Nothing if value is null */ static Maybe maybe(final T value) { @@ -72,4 +76,70 @@ public interface Maybe extends Functor>, MaybeStream, MaybeOpt return just(value); } + /** + * Monad binder maps the Maybe into another Maybe using the binder method f. + * + * @param f the mapper function + * @param the type of the value in the final maybe + * @return a Maybe with the mapped value + */ + Maybe flatMap(Function> f); + + @Override + Maybe map(Function f); + + /** + * Provide a value to use when Maybe is Nothing. + * + * @param supplier supplier for an alternate value + * @return a Maybe + */ + T orElseGet(Supplier supplier); + + /** + * A value to use when Maybe is Nothing. + * + * @param otherValue an alternate value + * @return a Maybe + */ + T orElse(T otherValue); + + /** + * Convert the Maybe to an {@link Optional}. + * + * @return an Optional containing a value for a Just, or empty for a Nothing + */ + Optional toOptional(); + + /** + * Throw the exception if the Maybe is a Nothing. + * + * @param e the exception to throw + * @throws Exception if the Maybe is a Nothing + */ + @SuppressWarnings("illegalthrows") + void orElseThrow(Supplier e) throws Exception; + + /** + * Converts the Maybe into either a single value stream or and empty stream. + * + * @return a Stream containing the value or nothing. + */ + Stream stream(); + + /** + * Filter a Maybe by the predicate, replacing with Nothing when it fails. + * + * @param predicate the test + * @return the Maybe, or Nothing if the test returns false + */ + Maybe filter(Predicate predicate); + + /** + * Provide the value within the Maybe, if it exists, to the Supplier, and returns the Maybe. + * + * @param consumer the Consumer to the value if present + * @return the Maybe + */ + Maybe peek(Consumer consumer); } diff --git a/src/main/java/net/kemitix/mon/maybe/MaybeOptional.java b/src/main/java/net/kemitix/mon/maybe/MaybeOptional.java deleted file mode 100644 index 44f8700..0000000 --- a/src/main/java/net/kemitix/mon/maybe/MaybeOptional.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * The MIT License (MIT) - * - * Copyright (c) 2017 Paul Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies - * or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE - * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package net.kemitix.mon.maybe; - -import java.util.Optional; -import java.util.function.Supplier; - -/** - * Enables emulation parts of and conversion to the Java Optional class for Maybe. - * - * @param the type of the content of the Just - * - * @author Paul Campbell (pcampbell@kemitix.net) - */ -interface MaybeOptional { - - /** - * Provide a value to use when Maybe is Nothing. - * - * @param supplier supplier for an alternate value - * - * @return a Maybe - */ - T orElseGet(Supplier supplier); - - /** - * A value to use when Maybe is Nothing. - * - * @param otherValue an alternate value - * - * @return a Maybe - */ - T orElse(T otherValue); - - /** - * Convert the Maybe to an {@link Optional}. - * - * @return an Optional containing a value for a Just, or empty for a Nothing - */ - Optional toOptional(); - - /** - * Throw the exception if the Maybe is a Nothing. - * - * @param e the exception to throw - * - * @throws Exception if the Maybe is a Nothing - */ - @SuppressWarnings("illegalthrows") - void orElseThrow(Supplier e) throws Exception; - -} diff --git a/src/main/java/net/kemitix/mon/maybe/MaybeStream.java b/src/main/java/net/kemitix/mon/maybe/MaybeStream.java deleted file mode 100644 index 4036ee8..0000000 --- a/src/main/java/net/kemitix/mon/maybe/MaybeStream.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * The MIT License (MIT) - * - * Copyright (c) 2017 Paul Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies - * or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE - * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package net.kemitix.mon.maybe; - -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * Enables emulation parts of and conversion to the Java Stream class for Maybe. - * - * @param the type of the content of the Just - * - * @author Paul Campbell (pcampbell@kemitix.net) - */ -interface MaybeStream { - - /** - * Converts the Maybe into either a single value stream or and empty stream. - * - * @return a Stream containing the value or nothing. - */ - Stream stream(); - - /** - * Filter a Maybe by the predicate, replacing with Nothing when it fails. - * - * @param predicate the test - * - * @return the Maybe, or Nothing if the test returns false - */ - Maybe filter(Predicate predicate); - - /** - * Provide the value within the Maybe, if it exists, to the Supplier, and returns the Maybe. - * - * @param consumer the Consumer to the value if present - * - * @return the Maybe - */ - Maybe peek(Consumer consumer); - -} diff --git a/src/main/java/net/kemitix/mon/maybe/Nothing.java b/src/main/java/net/kemitix/mon/maybe/Nothing.java index 1ea6eb4..7a72d46 100644 --- a/src/main/java/net/kemitix/mon/maybe/Nothing.java +++ b/src/main/java/net/kemitix/mon/maybe/Nothing.java @@ -32,7 +32,6 @@ import java.util.stream.Stream; * A Maybe where no value is present. * * @param the type of the missing content - * * @author Paul Campbell (pcampbell@kemitix.net) */ final class Nothing implements Maybe { @@ -40,8 +39,14 @@ final class Nothing implements Maybe { static final Maybe INSTANCE = new Nothing<>(); @Override - public Maybe map(final Function f) { - return this; + public Maybe flatMap(Function> f) { + return Maybe.nothing(); + } + + @Override + @SuppressWarnings("unchecked") + public Maybe map(final Function f) { + return (Maybe) INSTANCE; } @Override diff --git a/src/main/java/net/kemitix/mon/result/Err.java b/src/main/java/net/kemitix/mon/result/Err.java index e454ade..0e76126 100644 --- a/src/main/java/net/kemitix/mon/result/Err.java +++ b/src/main/java/net/kemitix/mon/result/Err.java @@ -22,9 +22,11 @@ package net.kemitix.mon.result; import lombok.RequiredArgsConstructor; +import net.kemitix.mon.maybe.Maybe; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; /** * An Error Result. @@ -51,8 +53,28 @@ class Err implements Result { return Result.error(error); } + @Override + public Result map(final Function f) { + return Result.error(error); + } + @Override public void match(final Consumer onSuccess, final Consumer onError) { onError.accept(error); } + + @Override + public Result> maybe(final Predicate predicate) { + return Result.error(error); + } + + @Override + public T orElseThrow() throws Throwable { + throw error; + } + + @Override + public String toString() { + return String.format("Result.Error{error=%s}", error); + } } diff --git a/src/main/java/net/kemitix/mon/result/Result.java b/src/main/java/net/kemitix/mon/result/Result.java index 30f446b..ea0de64 100644 --- a/src/main/java/net/kemitix/mon/result/Result.java +++ b/src/main/java/net/kemitix/mon/result/Result.java @@ -21,23 +21,51 @@ package net.kemitix.mon.result; +import net.kemitix.mon.maybe.Maybe; + import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; /** - * An Either type for holding a result or an error (exception). + * An Either type for holding a result or an error (Throwable). * * @param the type of the result when a success - * * @author Paul Campbell (pcampbell@kemitix.net) */ +@SuppressWarnings("methodcount") public interface Result { + /** + * Creates a Result from the Maybe, where the Result will be an error if the Maybe is Nothing. + * + * @param maybe the Maybe the might contain the value of the Result + * @param error the error that will be the Result if maybe is Nothing + * @param the type of the Maybe and the Result + * @return a Result containing the value of the Maybe when it is a Just, or the error when it is Nothing + */ + static Result fromMaybe(final Maybe maybe, final Supplier error) { + return maybe.map(Result::ok) + .orElseGet(() -> Result.error(error.get())); + } + + /** + * Create a Result for an error. + * + * @param error the error (Throwable) + * @param the type had the result been a success + * @return an error Result + */ + static Result error(final Throwable error) { + return new Err<>(error); + } + /** * Create a Result for a success. * * @param value the value - * @param the type of the value + * @param the type of the value * @return a successful Result */ static Result ok(final T value) { @@ -45,14 +73,32 @@ public interface Result { } /** - * Create a Result for an error. + * Creates a Result from the Maybe, where the Result will be an error if the Maybe is Nothing. * - * @param error the error (exception) - * @param the type had the result been a success - * @return an error Result + * @param result the Result the might contain the value of the Result + * @param the type of the Maybe and the Result + * @return a Result containing the value of the Maybe when it is a Just, or the error when it is Nothing */ - static Result error(final Throwable error) { - return new Err<>(error); + @SuppressWarnings("illegalcatch") + static Maybe toMaybe(final Result result) { + try { + return Maybe.just(result.orElseThrow()); + } catch (final Throwable throwable) { + return Maybe.nothing(); + } + } + + /** + * Swaps the inner Result of a Maybe, so that a Result is on the outside. + * @param maybeResult the Maybe the contains a Result + * @param the type of the value that may be in the Result + * @return a Result containing a Maybe, the value in the Maybe was the value in a successful Result within the + * original Maybe. If the original Maybe is Nothing, the Result will contain Nothing. If the original Result was an + * error, then the Result will also be an error. + */ + static Result> invert(final Maybe> maybeResult) { + return maybeResult.orElseGet(() -> Result.ok(null)) + .flatMap(value -> Result.ok(Maybe.maybe(value))); } /** @@ -73,18 +119,42 @@ public interface Result { * Returns a new Result consisting of the result of applying the function to the contents of the Result. * * @param f the mapping function the produces a Result - * @param the type of the result of the mapping function - * + * @param the type of the value withing the Result of the mapping function * @return a Result */ Result flatMap(Function> f); + /** + * Applies the functions to the value of a successful result, while doing nothing with an error. + * + * @param f the mapping function to produce the new value + * @param the type of the result of the mapping function + * @return a Result + */ + Result map(Function f); + /** * Matches the Result, either success or error, and supplies the appropriate Consumer with the value or error. * * @param onSuccess the Consumer to pass the value of a successful Result to - * @param onError the Consumer to pass the error from an error Result to + * @param onError the Consumer to pass the error from an error Result to */ void match(Consumer onSuccess, Consumer onError); + /** + * Wraps the value within the Result in a Maybe, either a Just if the predicate is true, or Nothing. + * + * @param predicate the test to decide + * @return a Result containing a Maybe that may or may not contain a value + */ + Result> maybe(Predicate predicate); + + /** + * Extracts the successful value from the result, or throws the error Throwable. + * + * @return the value if a success + * @throws Throwable the result is an error + */ + @SuppressWarnings("illegalthrows") + T orElseThrow() throws Throwable; } diff --git a/src/main/java/net/kemitix/mon/result/Success.java b/src/main/java/net/kemitix/mon/result/Success.java index 413d7c5..9093b08 100644 --- a/src/main/java/net/kemitix/mon/result/Success.java +++ b/src/main/java/net/kemitix/mon/result/Success.java @@ -22,9 +22,12 @@ package net.kemitix.mon.result; import lombok.RequiredArgsConstructor; +import net.kemitix.mon.maybe.Maybe; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; /** * A Successful Result. @@ -51,8 +54,41 @@ class Success implements Result { return f.apply(value); } + @Override + public Result map(final Function f) { + return Result.ok(f.apply(value)); + } + @Override public void match(final Consumer onSuccess, final Consumer onError) { onSuccess.accept(value); } + + @Override + public Result> maybe(final Predicate predicate) { + if (predicate.test(value)) { + return Result.ok(Maybe.just(value)); + } + return Result.ok(Maybe.nothing()); + } + + @Override + public T orElseThrow() { + return value; + } + + @Override + public boolean equals(Object other) { + return other instanceof Success && Objects.equals(value, ((Success) other).value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return String.format("Result.Success{value=%s}", value); + } } diff --git a/src/test/java/net/kemitix/mon/MaybeMonadTest.java b/src/test/java/net/kemitix/mon/MaybeMonadTest.java new file mode 100644 index 0000000..28e9e8b --- /dev/null +++ b/src/test/java/net/kemitix/mon/MaybeMonadTest.java @@ -0,0 +1,39 @@ +package net.kemitix.mon; + +import net.kemitix.mon.maybe.Maybe; +import org.assertj.core.api.WithAssertions; +import org.junit.Test; + +import java.util.function.Function; + +public class MaybeMonadTest implements WithAssertions { + + @Test + public void leftIdentity() { + //given + final int value = 1; + final Maybe maybe = Maybe.maybe(value); + final Function> f = i -> Maybe.maybe(i * 2); + //then + assertThat(maybe.flatMap(f)).isEqualTo(f.apply(value)); + } + + @Test + public void rightIdentity() { + //given + final Maybe maybe = Maybe.maybe(1); + //then + assertThat(maybe.flatMap(Maybe::maybe)).isEqualTo(maybe); + } + + @Test + public void associativity() { + //given + final Maybe maybe = Maybe.maybe(1); + final Function> f = i -> Maybe.maybe(i * 2); + final Function> g = i -> Maybe.maybe(i + 6); + //then + assertThat(maybe.flatMap(f).flatMap(g)).isEqualTo(maybe.flatMap(x -> f.apply(x).flatMap(g))); + } + +} diff --git a/src/test/java/net/kemitix/mon/MaybeTest.java b/src/test/java/net/kemitix/mon/MaybeTest.java index bbe60c5..61941ee 100644 --- a/src/test/java/net/kemitix/mon/MaybeTest.java +++ b/src/test/java/net/kemitix/mon/MaybeTest.java @@ -10,9 +10,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Stream; -import static net.kemitix.mon.maybe.Maybe.just; -import static net.kemitix.mon.maybe.Maybe.maybe; -import static net.kemitix.mon.maybe.Maybe.nothing; +import static net.kemitix.mon.maybe.Maybe.*; public class MaybeTest implements WithAssertions { @@ -89,7 +87,6 @@ public class MaybeTest implements WithAssertions { assertThat(Optional.empty().map(Maybe::just).orElseGet(Maybe::nothing)).isEqualTo(nothing()); } - @Test public void peek() { final AtomicInteger ref = new AtomicInteger(0); @@ -126,4 +123,47 @@ public class MaybeTest implements WithAssertions { //then assertThat(stream).isEmpty(); } + + @Test + public void justFlatMap() { + //given + final Maybe just1 = Maybe.just(1); + final Maybe just2 = Maybe.just(2); + //when + final Maybe result = just1.flatMap(v1 -> + just2.flatMap(v2 -> + Maybe.maybe(v1 + v2) + )); + //then + assertThat(result.toOptional()).contains(3); + } + + @Test + public void nothingFlatMap() { + //given + final Maybe nothing1 = Maybe.nothing(); + final Maybe nothing2 = Maybe.nothing(); + //when + final Maybe result = nothing1.flatMap(v1 -> + nothing2.flatMap(v2 -> + Maybe.maybe(v1 + v2) + )); + //then + assertThat(result.toOptional()).isEmpty(); + } + + @Test + public void justNothingFlatMap() { + //given + final Maybe just1 = Maybe.just(1); + final Maybe nothing2 = Maybe.nothing(); + //when + final Maybe result = just1.flatMap(v1 -> + nothing2.flatMap(v2 -> + Maybe.maybe(v1 + v2) + )); + //then + assertThat(result.toOptional()).isEmpty(); + } + } diff --git a/src/test/java/net/kemitix/mon/ResultMonadTest.java b/src/test/java/net/kemitix/mon/ResultMonadTest.java new file mode 100644 index 0000000..8a8470f --- /dev/null +++ b/src/test/java/net/kemitix/mon/ResultMonadTest.java @@ -0,0 +1,39 @@ +package net.kemitix.mon; + +import net.kemitix.mon.result.Result; +import org.assertj.core.api.WithAssertions; +import org.junit.Test; + +import java.util.function.Function; + +public class ResultMonadTest implements WithAssertions { + + @Test + public void leftIdentity() { + //given + final int value = 1; + final Result result = Result.ok(value); + final Function> f = i -> Result.ok(i * 2); + //then + assertThat(result.flatMap(f)).isEqualTo(f.apply(value)); + } + + @Test + public void rightIdentity() { + //given + final Result result = Result.ok(1); + //then + assertThat(result.flatMap(Result::ok)).isEqualTo(result); + } + + @Test + public void associativity() { + //given + final Result result = Result.ok(1); + final Function> f = i -> Result.ok(i * 2); + final Function> g = i -> Result.ok(i + 6); + //then + assertThat(result.flatMap(f).flatMap(g)).isEqualTo(result.flatMap(x -> f.apply(x).flatMap(g))); + } + +} diff --git a/src/test/java/net/kemitix/mon/ResultTest.java b/src/test/java/net/kemitix/mon/ResultTest.java index 82eeebc..75ab5d1 100644 --- a/src/test/java/net/kemitix/mon/ResultTest.java +++ b/src/test/java/net/kemitix/mon/ResultTest.java @@ -1,6 +1,7 @@ package net.kemitix.mon; import lombok.RequiredArgsConstructor; +import net.kemitix.mon.maybe.Maybe; import net.kemitix.mon.result.Result; import org.assertj.core.api.WithAssertions; import org.junit.Test; @@ -105,6 +106,202 @@ public class ResultTest implements WithAssertions { assertThat(flatMap.isError()).isTrue(); } + @Test + public void success_whenMap_isSuccess() { + //given + final Result okResult = Result.ok(1); + //when + final Result result = okResult.map(value -> String.valueOf(value)); + //then + assertThat(result.isOkay()).isTrue(); + result.match( + success -> assertThat(success).isEqualTo("1"), + error -> fail("not an error") + ); + } + + @Test + public void error_whenMap_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result errorResult = Result.error(exception); + //when + final Result result = errorResult.map(value -> String.valueOf(value)); + //then + assertThat(result.isError()).isTrue(); + result.match( + success -> fail("not an success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + public void successMaybe_whenPasses_isSuccessJust() { + //given + final Result okResult = Result.ok(1); + //when + final Result> maybeResult = okResult.maybe(value -> value >= 0); + //then + assertThat(maybeResult.isOkay()).isTrue(); + maybeResult.match( + success -> assertThat(success.toOptional()).contains(1), + error -> fail("not an error") + ); + } + + @Test + public void successMaybe_whenFails_isSuccessNothing() { + //given + final Result okResult = Result.ok(1); + //when + final Result> maybeResult = okResult.maybe(value -> value >= 4); + //then + assertThat(maybeResult.isOkay()).isTrue(); + maybeResult.match( + success -> assertThat(success.toOptional()).isEmpty(), + error -> fail("not an error") + ); + } + + @Test + public void errorMaybe_whenPasses_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result errorResult = Result.error(exception); + //when + final Result> maybeResult = errorResult.maybe(value -> value >= 0); + //then + assertThat(maybeResult.isError()).isTrue(); + maybeResult.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + public void errorMaybe_whenFails_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result errorResult = Result.error(exception); + //when + final Result> maybeResult = errorResult.maybe(value -> value >= 4); + //then + assertThat(maybeResult.isError()).isTrue(); + maybeResult.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + public void justMaybe_isSuccess() { + //given + final Maybe just = Maybe.just(1); + //when + final Result result = Result.fromMaybe(just, () -> new RuntimeException()); + //then + assertThat(result.isOkay()).isTrue(); + result.match( + success -> assertThat(success).isEqualTo(1), + error -> fail("not an error") + ); + } + + @Test + public void nothingMaybe_isError() { + //given + final Maybe nothing = Maybe.nothing(); + final RuntimeException exception = new RuntimeException(); + //when + final Result result = Result.fromMaybe(nothing, () -> exception); + //then + assertThat(result.isError()).isTrue(); + result.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + public void success_toMaybe_isJust() { + //given + final Result ok = Result.ok(1); + //when + final Maybe maybe = Result.toMaybe(ok); + //then + assertThat(maybe.toOptional()).contains(1); + } + + @Test + public void error_toMaybe_isNothing() { + //given + final Result error = Result.error(new RuntimeException()); + //when + final Maybe maybe = Result.toMaybe(error); + //then + assertThat(maybe.toOptional()).isEmpty(); + } + + @Test + public void success_whenOrElseThrow_isValue() throws Throwable { + //given + final Result ok = Result.ok(1); + //when + final Integer value = ok.orElseThrow(); + //then + assertThat(value).isEqualTo(1); + } + + @Test + public void error_whenOrElseThrow_throws() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = Result.error(exception); + //when + assertThatThrownBy(() -> error.orElseThrow()) + .isSameAs(exception); + } + + @Test + public void JustSuccess_invert_thenSuccessJust() { + //given + final Maybe> justSuccess = Maybe.just(Result.ok(1)); + //when + final Result> result = Result.invert(justSuccess); + //then + result.match( + success -> assertThat(success.toOptional()).contains(1), + error -> fail("Not an error") + ); + } + + @Test + public void JustError_invert_thenError() { + //given + final RuntimeException exception = new RuntimeException(); + final Maybe> justError = Maybe.just(Result.error(exception)); + //when + final Result> result = Result.invert(justError); + //then + result.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + public void Nothing_invert_thenSuccessNothing() { + //given + final Maybe> nothing = Maybe.nothing(); + //when + final Result> result = Result.invert(nothing); + //then + result.match( + success -> assertThat(success.toOptional()).isEmpty(), + error -> fail("Not an error") + ); + } + @Test public void useCase_whenOkay_thenReturnSuccess() { //given @@ -151,6 +348,26 @@ public class ResultTest implements WithAssertions { ); } + @Test + public void success_toString() { + //given + final Result ok = Result.ok(1); + //when + final String toString = ok.toString(); + //then + assertThat(toString).contains("Result.Success{value=1}"); + } + + @Test + public void err_toString() { + //given + final Result error = Result.error(new RuntimeException("failed")); + //when + final String toString = error.toString(); + //then + assertThat(toString).contains("Result.Error{error=java.lang.RuntimeException: failed}"); + } + @RequiredArgsConstructor private static class UseCase { @@ -175,20 +392,20 @@ public class ResultTest implements WithAssertions { calculateAverage(adjustedIntFromFile1, intFromFile2)))); } - private Result calculateAverage(final Integer val1, final Integer val2) { - return Result.ok((double) (val1 + val2) / 2); - } - - private Result adjustValue(Integer value) { - return Result.ok(value + 2); - } - - private Result readIntFromFile(String fileName) { + private Result readIntFromFile(final String fileName) { if (okay) { return Result.ok(fileName.length()); } return Result.error(new RuntimeException(fileName)); } + private Result adjustValue(final Integer value) { + return Result.ok(value + 2); + } + + private Result calculateAverage(final Integer val1, final Integer val2) { + return Result.ok((double) (val1 + val2) / 2); + } + } } \ No newline at end of file