diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f348764..ba7e8e6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,12 +4,6 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 10 - ignore: - - dependency-name: com.github.spotbugs:spotbugs-annotations - versions: - - 4.2.0 - - 4.2.1 - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/build-maven.yml b/.github/workflows/build-maven.yml index f4c6b40..76fae11 100644 --- a/.github/workflows/build-maven.yml +++ b/.github/workflows/build-maven.yml @@ -11,9 +11,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 8, 11, 16 ] + java: [ 11, 16 ] steps: - - uses: kamiazya/setup-graphviz@v1 - uses: actions/checkout@v2 - name: setup-jdk-${{ matrix.java }} uses: actions/setup-java@v2.1.0 diff --git a/.github/workflows/deploy-sonatype.yml b/.github/workflows/deploy-sonatype.yml index b0d58af..1f68fe6 100644 --- a/.github/workflows/deploy-sonatype.yml +++ b/.github/workflows/deploy-sonatype.yml @@ -10,13 +10,12 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: kamiazya/setup-graphviz@v1 - uses: actions/checkout@v2 - name: Set up JDK uses: actions/setup-java@v2.1.0 with: distribution: 'adopt' - java-version: 8 + java-version: 11 - name: Build with Maven run: mvn -B install - name: Nexus Repo Publish diff --git a/README.md b/README.md index 2f77f06..fc539ca 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ https://search.maven.org/artifact/net.kemitix/mon) - [Wrapper](#Wrapper) - light-weight type-alias-like - [TypeAlias](#TypeAlias) - type-alias-like monadic wrapper - [Maybe](#Maybe) - Maybe, Just or Nothing -- [Result](#Result) - Result, Success or Err +- [Result](https://kemitix.github.io/mon/net/kemitix/mon/result/package-summary.html) - Result, Success or Err - [Tree](#Tree) - generic trees - [Lazy](#Lazy) - lazy evaluation - [Either](#Either) - Either, Left or Right @@ -402,292 +402,7 @@ Optional optional = Maybe.maybe(getValue()) .toOptional(); ``` --- -## Result -Allows handling error conditions without the need to `catch` exceptions. - -When a `Result` is returned from a method, it will contain one of two values. -Either the actual result, or an error in the form of an `Exception`. The -exception is returned within the `Result` and is not thrown. - -`Result` is a Monad. - -### Example - -``` java -import net.kemitix.mon.result.Result; - -import java.io.IOException; - -class ResultExample implements Runnable { - - public static void main(final String[] args) { - new ResultExample().run(); - } - - @Override - public void run() { - Result.of(() -> callRiskyMethod()) - .flatMap(state -> doSomething(state)) - .match( - success -> System.out.println(success), - error -> error.printStackTrace() - ); - } - - private String callRiskyMethod() throws IOException { - return "I'm fine"; - } - - private Result doSomething(final String state) { - return Result.of(() -> state + ", it's all good."); - } - -} -``` - -In the above example the string `"I'm fine"` is returned by -`callRiskyMethod()` within a successful `Result`. The `.flatMap()` call, -unwraps that `Result` and, as it is a success, passes the contents to -`doSomething()`, which in turn returns a `Result` that the `.flatMap()` call -returns. `match()` is called on the `Result` and, being a success, will call -the success `Consumer`. - -Had `callRiskyMethod()` thrown an exception it would have been caught by the -`Result.of()` method, which would then return an error `Result`. An error -`Result` would skip the `flatMap` and continue at the `match()` where it -would have called the error `Consumer`. - -### Static Constructors - -#### `static Result of(Callable callable)` - -Create a `Result` for the output of the `Callable`. - -If the `Callable` throws an `Exception`, then the `Result` will be an error and -will contain that exception. - -This will be the main starting point for most `Result`s where the callable -could throw an `Exception`. - -``` java -Result okay = Result.of(() -> 1); -Result error = Result.of(() -> {throw new RuntimeException();}); -``` ---- -#### `static Result ok(T value)` - -Create a `Result` for a success. - -Use this where you have a value that you want to place into the `Result` context. - -``` java -Result okay = Result.ok(1); -``` ---- -#### `static Result error(Throwable error)` - -Create a `Result` for an error. - -``` java -Result error = Result.error(new RuntimeException()); -``` ---- -### Static Methods - -These static methods provide integration with the `Maybe` class. - -#### `static Maybe toMaybe(Result result)` - -Creates a `Maybe` from the `Result`, where the `Result` is a success, then -the `Maybe` will be a `Just` contain the value of the `Result`. However, if the -`Result` is an error, then the `Maybe` will be `Nothing`. - -``` java -Result result = Result.of(() -> getValue()); -Maybe maybe = Result.toMaybe(result); -``` ---- -#### `static Result fromMaybe(Maybe maybe, Supplier error)` - -Creates a `Result` from the `Maybe`, where the `Result` will be an error -if the `Maybe` is nothing. Where the `Maybe` is nothing, then the -`Supplier` will provide the error for the `Result`. - -``` java -Maybe maybe = Maybe.maybe(getValue()); -Result result = Result.fromMaybe(maybe, - () -> new NoSuchFileException("filename")); -``` ---- -#### `static Result> invert(Maybe> maybeResult)` - -Swaps the `Result` within a `Maybe`, so that `Result` contains a `Maybe`. - -``` java -Maybe> maybe = Maybe.maybe(Result.of(() -> getValue())); -Result> result = Result.invert(maybe); -``` ---- -#### `static Result> flatMapMaybe(Result> maybeResult, Function,Result>> f)` - -Applies the function to the contents of a `Maybe` within the `Result`. - -``` java -Result> result = Result.of(() -> Maybe.maybe(getValue())); -Result> maybeResult = Result.flatMapMaybe(result, - maybe -> Result.of(() -> maybe.map(v -> v * 2))); -``` ---- -### Instance Methods - -#### ` Result map(Function f)` - -If the `Result` is a success, then apply the function to the value within the -`Result`, returning the result within another `Result`. If the `Result` is an -error, then return the error. - -``` java -Result result = Result.of(() -> getValue()) - .map(v -> String.valueOf(v)); -``` ---- -#### ` Result flatMap(Function> f)` - -If the `Result` is a success, then return a new `Result` containing the result -of applying the function to the contents of the `Result`. If the `Result` is an -error, then return the error. - -``` java -Result result = - Result.of(() -> getValue()) - .flatMap(v -> Result.of(() -> String.valueOf(v))); -``` ---- -#### ` Result andThen(Function> f)` - -Maps a successful `Result` to another `Result` using a `Callable` that is able -to throw a checked exception. - -``` java -Result result = - Result.of(() -> getValue()) - .andThen(v -> () -> {throw new IOException();}); -``` ---- -#### `void match(Consumer onSuccess, Consumer onError)` - -Matches the `Result`, either success or error, and supplies the appropriate -`Consumer` with the value or error. - -``` java -Result.of(() -> getValue()) - .match( - success -> System.out.println(success), - error -> System.err.println(error.getMessage()) - ); -``` ---- -#### `Result recover(Function> f)` - -Provide a way to attempt to recover from an error state. - -``` java -Result result = Result.of(() -> getValue()) - .recover(e -> Result.of(() -> getSafeValue(e))); -``` ---- -#### `Result peek(Consumer consumer)` - -Provide the value within the Result, if it is a success, to the `Consumer`, -and returns this Result. - -``` java -Result result = Result.of(() -> getValue()) - .peek(v -> System.out.println(v)); -``` ---- -#### `Result thenWith(Function> f)` - -Perform the continuation with the current `Result` value then return the -current `Result`, assuming there was no error in the continuation. - -``` java -Result result = - Result.of(() -> getValue()) - .thenWith(v -> () -> System.out.println(v)) - .thenWith(v -> () -> {throw new IOException();}); -``` ---- -#### `Result> maybe(Predicate predicate)` - -Wraps the value within the `Result` in a `Maybe`, either a `Just` if the -predicate is true, or `Nothing`. - -``` java -Result> result = Result.of(() -> getValue()) - .maybe(v -> v % 2 == 0); -``` ---- -#### `T orElseThrow()` - -Extracts the successful value from the `Result`, or throws the error -within a `CheckedErrorResultException`. - -``` java -Integer result = Result.of(() -> getValue()) - .orElseThrow(); -``` ---- -#### ` T orElseThrow(Class type) throws E` - -Extracts the successful value from the `Result`, or throws the error when it -is of the given type. Any other errors will be thrown inside an -`UnexpectedErrorResultException`. - -``` java -Integer result = Result.of(() -> getValue()) - .orElseThrow(IOException.class); -``` ---- -#### `T orElseThrowUnchecked()` - -Extracts the successful value from the `Result`, or throws the error within -an `ErrorResultException`. - -``` java -Integer result = Result.of(() -> getValue()) - .orElseThrowUnchecked(); -``` ---- -#### `void onError(Consumer errorConsumer)` - -A handler for error states. If the `Result` is an error, then supply the error -to the `Consumer`. Does nothing if the `Result` is a success. - -``` java -Result.of(() -> getValue()) - .onError(e -> handleError(e)); -``` ---- -#### `boolean isOkay()` - -Checks if the `Result` is a success. - -``` java -boolean isOkay = Result.of(() -> getValue()) - .isOkay(); -``` ---- -#### `boolean isError()` - -Checks if the `Result` is an error. - -``` java -boolean isError = Result.of(() -> getValue()) - .isError(); -``` ---- ## Tree A Generalised tree, where each node may or may not have an item, and may have diff --git a/pom.xml b/pom.xml index 7c4365e..44a377d 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,6 @@ 2017 - 1.8 5.7.2 3.11.2 3.20.2 @@ -45,6 +44,7 @@ 1.6.8 0.14 4.2.3 + 1.1.1 @@ -55,6 +55,12 @@ provided + + org.apiguardian + apiguardian-api + ${apiguardian-api.version} + + org.junit.jupiter junit-jupiter-api diff --git a/src/main/java/net/kemitix/mon/ThrowableFunctor.java b/src/main/java/net/kemitix/mon/ThrowableFunctor.java new file mode 100644 index 0000000..273d6ba --- /dev/null +++ b/src/main/java/net/kemitix/mon/ThrowableFunctor.java @@ -0,0 +1,36 @@ +package net.kemitix.mon; + +import net.kemitix.mon.result.ThrowableFunction; + +/** + * The ThrowableFunctor is used for types that can be mapped over. + * + *

A ThrowableFunctor is identical to a normal Functor except that the + * map method may throw an exception.

+ * + *

Implementations of ThrowableFunctor should satisfy the following laws:

+ * + *
    + *
  • map id == id
  • + *
  • map (f . g) == map f . map g
  • + *
+ * + * @param the type of the Functor + * @param the type of the mapped Functor + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +public interface ThrowableFunctor> { + + /** + * Applies the function to the value within the ThrowableFunctor, returning + * the result within another ThrowableFunctor. + * + * @param f the function to apply + * @param the type of the content of the mapped functor + * + * @return a ThrowableFunctor containing the result of the function + * {@code f} applied to the value + */ + F map(ThrowableFunction f); +} diff --git a/src/main/java/net/kemitix/mon/TypeReference.java b/src/main/java/net/kemitix/mon/TypeReference.java new file mode 100644 index 0000000..0d78a4f --- /dev/null +++ b/src/main/java/net/kemitix/mon/TypeReference.java @@ -0,0 +1,31 @@ +package net.kemitix.mon; + +/** + * Helper class to capture a reference to a type. + * + *

Usually to be used when passing a type as a parameter to method.

+ * + *

+ * TypeReference<Integer> ref1 = TypeReference.create();
+ * var ref2 = TypeReference.<Integer>create();
+ * 
+ * + * @param the type being references + */ +@SuppressWarnings("PMD.ClassNamingConventions") +final public class TypeReference { + + private TypeReference() { + } + + /** + * Creates a new instance of a TypeReference. + * + * @param the type being references. + * @return the TypeReference + */ + public static TypeReference create() { + return new TypeReference<>(); + } + +} diff --git a/src/main/java/net/kemitix/mon/maybe/Maybe.java b/src/main/java/net/kemitix/mon/maybe/Maybe.java index 03e8ff4..84c5469 100644 --- a/src/main/java/net/kemitix/mon/maybe/Maybe.java +++ b/src/main/java/net/kemitix/mon/maybe/Maybe.java @@ -94,6 +94,18 @@ public interface Maybe extends Functor> { .orElseGet(Maybe::nothing); } + /** + * Creates a Maybe from an Optional. + * + * @param optional the Optional + * @param the type of the value + * @return a Just if the Optional contains a value, otherwise a Nothing + */ + static Maybe fromOptional(Optional optional) { + return optional.map(Maybe::maybe) + .orElseGet(Maybe::nothing); + } + /** * Checks if the Maybe is a Just. * diff --git a/src/main/java/net/kemitix/mon/result/BaseResult.java b/src/main/java/net/kemitix/mon/result/BaseResult.java new file mode 100644 index 0000000..f83703d --- /dev/null +++ b/src/main/java/net/kemitix/mon/result/BaseResult.java @@ -0,0 +1,61 @@ +package net.kemitix.mon.result; + +import org.apiguardian.api.API; + +import java.util.function.Consumer; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * Base interface for {@link Result} and {@link ResultVoid}. + */ +public interface BaseResult { + + /** + * Checks if the Result is an error. + * + *

+     * boolean isError = Result.of(() -> getValue())
+     *                         .isError();
+     * 
+ * + * @return true if the Result is an error. + */ + @API(status = STABLE) + boolean isError(); + + /** + * Checks if the Result is a success. + * + *

+     * boolean isOkay = Result.of(() -> getValue())
+     *                        .isOkay();
+     * 
+ * + * @return true if the Result is a success. + */ + @API(status = STABLE) + boolean isOkay(); + + /** + * A handler for error states. + * + *

If the {@code Result} is an error, then supply the error + * to the {@code Consumer}. Does nothing if the {@code Result} is a + * success.

+ * + *

When this is an error then tne Consumer will be supplied with the + * error. When this is a success, then nothing happens.

+ * + *

+     * void handleError(Throwable e) {...}
+     * Result.of(() -> doSomething())
+     *       .onError(e -> handleError(e));
+     * 
+ * + * @param errorConsumer the consumer to handle the error + */ + @API(status = STABLE) + void onError(Consumer errorConsumer); + +} diff --git a/src/main/java/net/kemitix/mon/result/Err.java b/src/main/java/net/kemitix/mon/result/Err.java index da57110..6082cb8 100644 --- a/src/main/java/net/kemitix/mon/result/Err.java +++ b/src/main/java/net/kemitix/mon/result/Err.java @@ -22,14 +22,12 @@ package net.kemitix.mon.result; import lombok.RequiredArgsConstructor; -import net.kemitix.mon.maybe.Maybe; import java.util.Objects; import java.util.concurrent.Callable; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Predicate; /** * An Error Result. @@ -37,7 +35,8 @@ import java.util.function.Predicate; * @param the type of the value in the Result if it has been a success */ @RequiredArgsConstructor -@SuppressWarnings({"methodcount", "PMD.CyclomaticComplexity"}) +@SuppressWarnings({"methodcount", "PMD.TooManyMethods", "PMD.ExcessivePublicCount", + "PMD.CyclomaticComplexity"}) class Err implements Result { private final Throwable error; @@ -54,12 +53,17 @@ class Err implements Result { @Override public Result flatMap(final Function> f) { - return err(error); + return new Err<>(error); } @Override - public Result map(final Function f) { - return err(error); + public ResultVoid flatMapV(final Function f) { + return new ErrVoid(error); + } + + @Override + public Result map(final ThrowableFunction f) { + return new Err<>(error); } @Override @@ -67,18 +71,13 @@ class Err implements Result { onError.accept(error); } - @Override - public Result> maybe(final Predicate predicate) { - return err(error); - } - @Override public T orElseThrow() throws CheckedErrorResultException { throw CheckedErrorResultException.with(error); } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "PMD.AvoidDuplicateLiterals"}) public T orElseThrow(final Class type) throws E { if (type.isInstance(error)) { throw (E) error; @@ -101,14 +100,31 @@ class Err implements Result { return f.apply(error); } + @Override + public void onSuccess(final Consumer successConsumer) { + // do nothing + } + @Override public void onError(final Consumer errorConsumer) { errorConsumer.accept(error); } + @Override + @SuppressWarnings("unchecked") + public Result onError( + final Class errorClass, + final Consumer consumer + ) { + if (error.getClass().isAssignableFrom(errorClass)) { + consumer.accept((E) error); + } + return this; + } + @Override public Result andThen(final Function> f) { - return err(error); + return (Result) this; } @Override @@ -116,6 +132,11 @@ class Err implements Result { return this; } + @Override + public ResultVoid thenWithV(final Function> f) { + return toVoid(); + } + @Override public Result reduce(final Result identify, final BinaryOperator operator) { return this; @@ -123,7 +144,8 @@ class Err implements Result { @Override public boolean equals(final Object other) { - return other instanceof Err && Objects.equals(error, ((Err) other).error); + return other instanceof Err + && Objects.equals(error, ((Err) other).error); } @Override @@ -135,4 +157,9 @@ class Err implements Result { public String toString() { return String.format("Result.Error{error=%s}", error); } + + @Override + public ResultVoid toVoid() { + return new ErrVoid(error); + } } diff --git a/src/main/java/net/kemitix/mon/result/ErrVoid.java b/src/main/java/net/kemitix/mon/result/ErrVoid.java new file mode 100644 index 0000000..55c2b5f --- /dev/null +++ b/src/main/java/net/kemitix/mon/result/ErrVoid.java @@ -0,0 +1,79 @@ +package net.kemitix.mon.result; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +public class ErrVoid implements ResultVoid { + + private final Throwable error; + + ErrVoid(final Throwable error) { + this.error = error; + } + + @Override + public boolean isError() { + return true; + } + + @Override + public boolean isOkay() { + return false; + } + + @Override + public void match( + final Runnable onSuccess, + final Consumer onError + ) { + onError.accept(error); + } + + @Override + public ResultVoid recover(final Function f) { + return f.apply(error); + } + + @Override + public void onSuccess(final Runnable runnable) { + // do nothing + } + + @Override + public void onError(final Consumer errorConsumer) { + errorConsumer.accept(error); + } + + @Override + public ResultVoid onError( + final Class errorClass, + final Consumer consumer + ) { + if (error.getClass().isAssignableFrom(errorClass)) { + consumer.accept((E) error); + } + return this; + } + + @Override + public ResultVoid andThen(final VoidCallable f) { + return this; + } + + @Override + public String toString() { + return String.format("Result.ErrVoid{error=%s}", error); + } + + @Override + public boolean equals(final Object other) { + return other instanceof ErrVoid && Objects.equals(error, ((ErrVoid) other).error); + } + + @Override + public int hashCode() { + return Objects.hash(error); + } + +} diff --git a/src/main/java/net/kemitix/mon/result/Result.java b/src/main/java/net/kemitix/mon/result/Result.java index 038974e..d569d3e 100644 --- a/src/main/java/net/kemitix/mon/result/Result.java +++ b/src/main/java/net/kemitix/mon/result/Result.java @@ -21,178 +21,416 @@ package net.kemitix.mon.result; -import net.kemitix.mon.Functor; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import net.kemitix.mon.ThrowableFunctor; +import net.kemitix.mon.TypeReference; +import net.kemitix.mon.experimental.either.Either; import net.kemitix.mon.maybe.Maybe; +import org.apiguardian.api.API; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.*; +import java.util.stream.Stream; + +import static org.apiguardian.api.API.Status.*; /** - * An Either type for holding a result or an error (Throwable). + * A type for holding a result or an error. + * + *

Static Constructors:

+ *
    + *
  • {@link #ok()}
  • + *
  • {@link #ok(Object)}
  • + *
  • {@link #of(Callable)}
  • + *
  • {@link #ofVoid(VoidCallable)}
  • + *
  • {@link #error(Throwable)}
  • + *
  • {@link #error(TypeReference, Throwable)}
  • + *
  • {@link #from(Either)}
  • + *
  • {@link #from(Maybe, Supplier)}
  • + *
* * @param the type of the result when a success * @author Paul Campbell (pcampbell@kemitix.net) */ -@SuppressWarnings({"methodcount", "PMD.TooManyMethods"}) -public interface Result extends Functor> { +@SuppressWarnings({"methodcount", "PMD.TooManyMethods", "PMD.ExcessivePublicCount", "PMD.ExcessiveClassLength", + "PMD.AvoidCatchingThrowable"}) +public interface Result extends BaseResult, ThrowableFunctor> { + + // BEGIN Static Constructors /** - * Creates a Result from the Maybe, where the Result will be an error if the Maybe is Nothing. + * Creates a success Result with no value. * - * @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 + *

+     * ResultVoid okay = Result.ok();
+     * 
+ * @return a successful Result */ - static Result fromMaybe(final Maybe maybe, final Supplier error) { - return maybe.map(Result::ok) - .orElseGet(() -> Result.error(error.get())); + @API(status = STABLE) + static ResultVoid ok() { + return SuccessVoid.getInstance(); } /** - * Create a Result for an error. + * Create a success Result with a value. * - * @param error the error (Throwable) - * @param the type had the result been a success - * @return an error Result + *

+     * Result<Integer> okay = Result.ok(1);
+     * 
+ * + * @param value the value + * @param the type of the value + * @return a successful Result */ - default Result err(final Throwable error) { - return new Err<>(error); + @API(status = STABLE) + static Result ok(final R value) { + return new Success<>(value); } /** - * Create a Result for an error. + * Create a {@link Result} for the output of the {@link Callable}. * - * @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 output of the Callable. + *

If the {@code Callable} succeeds then the {@code Result} will be a + * {@link Success} and will contain the value. + * If it throws an {@code Exception}, then the {@code Result} will be an + * {@link Err} and will contain that exception.

+ * + *

+     * Result<Integer$gt; okay = Result.of(() -> 1);
+     * Result<Integer> error = Result.of(() -> {
+     *     throw new RuntimeException();
+     * });
+     * 
* * @param callable the callable to produce the result * @param the type of the value * @return a Result */ - @SuppressWarnings({"illegalcatch", "PMD.AvoidCatchingThrowable", "PMD.AvoidDuplicateLiterals"}) - default Result result(final Callable callable) { - try { - return Result.ok(callable.call()); - } catch (final Throwable e) { - return Result.error(e); - } - } - - /** - * Create a Result for a output of the Callable. - * - * @param callable the callable to produce the result - * @param the type of the value - * @return a Result - */ - @SuppressWarnings({"illegalcatch", "PMD.AvoidCatchingThrowable"}) + @API(status = STABLE) static Result of(final Callable callable) { try { return Result.ok(callable.call()); + } catch (final Throwable e) { + return new Err<>(e); + } + } + + /** + * Create a {@code ResultVoid} after calling a {@link VoidCallable} + * that produces no output. + * + *

If the {@code callable} completes successfully then a + * {@link SuccessVoid} will be returned. if the {@code callable} throws an + * exception, then a {@link ErrVoid} containing the exception will be + * returned.

+ * + *

+     * ResultVoid okay = Result.ofVoid(() -> System.out.println("Hello, World!"));
+     * ResultVoid error = Result.ofVoid(() -> {
+     *     throw new Exception();
+     * });
+     * 
+ * + * @param callable the callable to call + * @return a Result with no value + */ + @API(status = STABLE) + static ResultVoid ofVoid(final VoidCallable callable) { + try { + callable.call(); + return Result.ok(); } catch (final Throwable e) { return Result.error(e); } } /** - * Create a Result for a success. + * Create a Result for an error. * - * @param value the value - * @param the type of the value - * @return a successful Result + *

+     * ResultVoid error = Result.error(new RuntimeException());
+     * 
+ * + * @param error the error (Throwable) + * @return an error Result */ - default Result success(final T value) { - return new Success<>(value); + @API(status = STABLE) + static ResultVoid error(final Throwable error) { + return new ErrVoid(error); } /** - * Create a Result for a success. + * Create a Result for an error. * - * @param value the value - * @param the type of the value - * @return a successful Result + *

+     * Result<Integer> error = Result.error(TypeReference.create(), new RuntimeException());
+     * 
+ * + * @param type the type of the missing) value + * @param error the error (Throwable) + * @param The type of the missing value + * @return an error Result */ - static Result ok(final T value) { - return new Success<>(value); + @API(status = STABLE) + @SuppressFBWarnings(value = "UP_UNUSED_PARAMETER", + justification = "Use the type parameter to fingerprint the return type") + static Result error(final TypeReference type, final Throwable error) { + return new Err<>(error); } /** - * Creates a {@link Maybe} from the Result, where the Result is a success, then the Maybe will contain the value. + * Creates a Result from the Either, where the Result will be an error if + * the Either is a Left, and a success if it is a Right. * - *

However, if the Result is an error then the Maybe will be nothing.

+ *

+     * import net.kemitix.mon.experimental.either.Either;
      *
-     * @param result the Result the might contain the value of the Result
-     * @param     the type of the Maybe and the Result
+     * Either<Throwable, String> eitherRight = Either.right("Hello, World!");
+     * Either<Throwable, String> eitherLeft = Either.left(new RuntimeException());
+     *
+     * Result<String> success = Result.from(eitherRight);
+     * Result<String> error = Result.from(eitherLeft);
+     * 
+ * + * @param either the either that could contain an error in left or a value in right + * @param the type of the right value + * @return a Result containing the right value of the Either when it is a + * Right, or the left error when it is a Left. + */ + @API(status = EXPERIMENTAL) + static Result from(Either either) { + return Result.from( + Maybe.fromOptional(either.getRight()), + () -> either.getLeft().get()); + } + + /** + * Creates a Result from the Maybe, where the Result will be an error if the Maybe is Nothing. + * + *

Where the {@code Maybe} is nothing, then the Supplier will provide the error for the Result.

+ * + *

+     * Maybe<Integer> maybe = Maybe.maybe(1);
+     * Result<Integer> result = Result.from(maybe, () -> new RuntimeException());</p>
+     * 
+ * + * @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 value in 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 Maybe toMaybe(final Result result) { - try { - return Maybe.just(result.orElseThrow()); - } catch (final CheckedErrorResultException throwable) { - return Maybe.nothing(); - } + @API(status = EXPERIMENTAL) + static Result from(final Maybe maybe, final Supplier error) { + return maybe.map(Result::ok) + .orElseGet(() -> new Err<>(error.get())); + } + + // END Static Constructors + // BEGIN Static methods + + /** + * Applies a function to a stream of values, folding the results using the + * zero value and accumulator function. + * + *

Returns a success {@code Result} of the accumulated outputs if all + * values were transformed successfully by the function, or an error + * {@code Result} for the first error. If any value results in an error when applying the function, then + * processing stops and a Result containing that error is returned,

+ * + *

+     * Function<String, Integer> f = s -> {
+     *     if ("dd".equals(s)) {
+     *         throw new RuntimeException("Invalid input: " + s);
+     *     }
+     *     return s.length();
+     * };
+     *
+     * Stream<String> okayStream = Stream.of("aa", "bb");
+     * Result<Integer> resultOkay = Result.applyOver(okayStream, f, 0, Integer::sum);
+     * resultOkay.match(
+     *     success -> System.out.println("Total length: " + success),
+     *     error -> System.out.println("Error: " + error.getMessage())
+     * );
+     * // Total length: 4
+     *
+     * Stream<String> errorStream = Stream.of("cc", "dd");
+     * Result<Integer> resultError = Result.applyOver(errorStream, f, 0, Integer::sum);
+     * resultError.match(
+     *     success -> System.out.println("Total length: " + success), // will not match
+     *     error -> System.out.println("Error: " + error.getMessage())
+     * );
+     * // Error: Invalid input: dd
+     * 
+ * + * @param stream the values to apply the function to + * @param f the function to apply to the values + * @param zero the initial value to use with the accumulator + * @param accumulator the function to combine function outputs together + * @param the type of the stream values + * @param the type of the output value + * @return a Success Result of the accumulated function outputs if all + * values were transformed successfully by the function, or an Err Result + * for the first value that failed. + */ + @API(status = STABLE) + static Result applyOver( + Stream stream, + Function f, + R zero, + BiFunction accumulator + ) { + var acc = new AtomicReference<>(Result.ok(zero)); + stream.map(t -> Result.of(() -> f.apply(t))) + .peek(r -> + r.onSuccess(vNew -> + acc.getAndUpdate(rResult -> + rResult.map(vOld -> + accumulator.apply(vNew, vOld))))) + .dropWhile(Result::isOkay) + .limit(1) + .forEach(acc::set); + return acc.get(); } /** - * Extracts the successful value from the result, or throws the error within a {@link CheckedErrorResultException}. + * Applies a consumer to a stream of values. * - * @return the value if a success - * @throws CheckedErrorResultException if the result is an error + *

If any value results in an error when accepted by the consumer, then + * processing stops and a Result containing that error is returned,

+ * + *

Returns a success Result (with no value) if all values were consumed + * successfully by the function, or an error Result for the first value that + * failed.

+ * + *

+     * List<String> processed = new ArrayList<>();
+     * Consumer<String> consumer = s -> {
+     *     if ("dd".equals(s)) {
+     *         throw new RuntimeException("Invalid input: " + s);
+     *     }
+     *     processed.add(s);
+     * };
+     *
+     * Stream<String> okayStream = Stream.of("aa", "bb");
+     * ResultVoid resultOkay = Result.applyOver(okayStream, consumer);
+     * resultOkay.match(
+     *         () -> System.out.println("All processed okay."),
+     *         error -> System.out.println("Error: " + error.getMessage())
+     * );
+     * System.out.println("Processed: " + processed);
+     * // All processed okay.
+     * // Processed: [aa, bb]
+     *
+     * processed.add("--");
+     * Stream<String> errorStream = Stream.of("cc", "dd", "ee");// fails at 'dd'
+     * ResultVoid resultError = Result.applyOver(errorStream, consumer);
+     * resultError.match(
+     *         () -> System.out.println("All processed okay."),
+     *         error -> System.out.println("Error: " + error.getMessage())
+     * );
+     * System.out.println("Processed: " + processed);
+     * // Error: Invalid input: dd
+     * // Processed: [aa, bb, --, cc]
+     * 
+ * + * @param stream the value to supply to the consumer + * @param consumer the consumer to receive the values + * @param the type of the stream values + * @return a Success Result (with no value) if all values were transformed + * successfully by the function, or an Err Result for the first value that + * failed. */ - T orElseThrow() throws CheckedErrorResultException; - - /** - * Extracts the successful value from the result, or throws the error Throwable. - * - * @param type the type of checked exception that may be thrown - * @param the type of the checked exception to throw - * - * @return the value if a success - * @throws E if the result is an error - */ - T orElseThrow(Class type) throws E; - - /** - * Extracts the successful value from the result, or throws the error in a {@link UnexpectedErrorResultException}. - * - * @return the value if a success - */ - T orElseThrowUnchecked(); - - /** - * 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> swap(final Maybe> maybeResult) { - return maybeResult.orElseGet(() -> Result.ok(null)) - .flatMap(value -> Result.ok(Maybe.maybe(value))); + @API(status = STABLE) + static ResultVoid applyOver( + Stream stream, + Consumer consumer + ) { + return applyOver(stream, n -> { + consumer.accept(n); + return null; + }, null, (unused1, unused2) -> null) + .toVoid(); } /** - * Returns a new Result consisting of the result of applying the function to the contents of the Result. + * Applies a function to a stream of values, folding the results using the + * zero value and accumulator function. * - * @param f the mapping function the produces a Result - * @param the type of the value withing the Result of the mapping function - * @return a Result + *

If any value results in an error when applying the function, then + * processing stops and a {@code Result} containing that error is returned.

+ * + *

Returns a success {@code Result} of the accumulated function outputs + * if all values were transformed successfully, or an error {@code Result} + * for the first value that failed.

+ * + *

Similar to {@link #applyOver(Stream, Function, Object, BiFunction)}, + * except that the result of the {@code f} function is a {@code Result}; and + * to a {@code flatMap} method in that the {@code Result} is not nested with + * in another {@code Result}.

+ * + *

+     * Function<String, Integer> f = s -> {
+     *     if ("dd".equals(s)) {
+     *         throw new RuntimeException("Invalid input: " + s);
+     *     }
+     *     return s.length();
+     * };
+     *
+     * Stream<String> okayStream = Stream.of("aa", "bb");
+     * Result<Integer> resultOkay = Result.applyOver(okayStream, f, 0, Integer::sum);
+     * resultOkay.match(
+     *     success -> assertThat(success).isEqualTo(4),
+     *     error -> fail("not an err")
+     * );
+     * // Total length: 4
+     *
+     * Stream<String> errorStream = Stream.of("cc", "dd");
+     * Result<Integer> resultError = Result.applyOver(errorStream, f, 0, Integer::sum);
+     * resultError.match(
+     *     success -> fail("not a success"), // will not match
+     *     error -> assertThat(error.getMessage()).isEqualTo("Invalid input: dd")
+     * );
+     * // Error: Invalid input: dd
+     * 
+ * + * @param stream the values to apply the function to + * @param f the function to apply to the values + * @param zero the initial value to use with the accumulator + * @param accumulator the function to combine function outputs together + * @param the type of the stream values + * @param the type of the output value + * @return a Success Result of the accumulated function outputs if all + * values were transformed successfully by the function, or an Err Result + * for the first value that failed. */ - Result flatMap(Function> f); + @API(status = STABLE) + static Result flatApplyOver( + Stream stream, + Function> f, + R zero, + BiFunction accumulator + ) { + var acc = new AtomicReference<>(Result.ok(zero)); + stream.map(f) + .peek(r -> r.onSuccess(vNew -> + acc.getAndUpdate(rResult -> + rResult.map(vOld -> + accumulator.apply(vNew, vOld))))) + .dropWhile(Result::isOkay) + .limit(1) + .forEach(acc::set); + return acc.get(); + } /** - * Applies the function to the contents of a Maybe within the Result. + * Applies the function to the contents of a {@link Maybe} within the {@code Result}. + * + *

+     * Result<Maybe<Integer>> result = Result.of(() -> Maybe.maybe(getValue()));
+     * Result<Maybe<Integer>> maybeResult = Result.flatMapMaybe(result,
+     *        maybe -> Result.of(() -> maybe.map(v -> v * 2)));
+     * 
* * @param maybeResult the Result that may contain a value * @param f the function to apply to the value @@ -200,6 +438,7 @@ public interface Result extends Functor> { * @param the type of the updated Result * @return a new Maybe within a Result */ + @API(status = EXPERIMENTAL) static Result> flatMapMaybe( final Result> maybeResult, final Function, Result>> f @@ -208,63 +447,296 @@ public interface Result extends Functor> { } /** - * Checks if the Result is an error. + * Swaps the inner {@code Result} of a {@link Maybe}, so that a {@code Result} contains a {@code Maybe}. * - * @return true if the Result is an error. + * @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. + * @deprecated */ - boolean isError(); + @API(status = DEPRECATED) + @Deprecated + static Result> swap(final Maybe> maybeResult) { + return maybeResult.orElseGet(() -> Result.ok(null)) + .flatMap(value -> Result.ok(Maybe.maybe(value))); + } /** - * Checks if the Result is a success. + * Creates a {@link Maybe} from the {@code Result}. * - * @return true if the Result is a success. + *

Where the {@code Result} is a {@link Success}, the {@code Maybe} will be a {@code Just} contain the value of + * the {@code Result}.

+ * + *

However, if the {@code Result} is an {@link Err}, then the {@code Maybe} will be {@code Nothing}.

+ * + *

+     * Result<Integer> result = Result.of(() -> getValue());
+     * Maybe<Integer> maybe = Result.toMaybe(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 */ - boolean isOkay(); + @API(status = EXPERIMENTAL) + static Maybe toMaybe(final Result result) { + try { + return Maybe.just(result.orElseThrow()); + } catch (final CheckedErrorResultException throwable) { + return Maybe.nothing(); + } + } + // END Static methods + + /** + * Create a {@link Result} for the output of the {@link Callable}. + * + *

If the {@code Callable} succeeds then the {@code Result} will be a + * {@link Success} and will contain the value. + * If it throws an {@code Exception}, then the {@code Result} will be an + * {@link Err} and will contain that exception.

+ * + *

+     * Result<Integer> start = Result.ok(1);
+     * Result<Integer> okay = start.result(() -> 1);
+     * Result<Integer> error = start.result(() -> {
+     *     throw new RuntimeException();
+     * });
+     * 
+ * + * @param callable the callable to produce the result + * @param the type of the value + * @return a Result + */ + @API(status = EXPERIMENTAL) + default Result result(final Callable callable) { + return Result.of(callable); + } + + /** + * Converts the {@code Result} into an {@link Either}. + * + *

+     * Result<String> success = Result.ok("success");
+     * RuntimeException exception = new RuntimeException();
+     * Result<String> error = Result.error(String.class, exception);
+     *
+     * Either<Throwable, String> eitherRight = success.toEither();
+     * Either<Throwable, String> eitherLeft = error.toEither();
+     * 
+ * + * @return A {@code Right} for a success or a {@code Left} for an error. + */ + @API(status = EXPERIMENTAL) + default Either toEither() { + var either = new AtomicReference>(); + match( + success -> either.set(Either.right(success)), + error -> either.set(Either.left(error)) + ); + return either.get(); + } + + /** + * Extracts the successful value from the result, or throws a {@link CheckedErrorResultException} with the error + * as the cause. + * + *

+     * Integer result = Result.of(() -> getValue())
+     *                        .orElseThrow();
+     * 
+ * + * @return the value if a success + * @throws CheckedErrorResultException if the result is an error + */ + @API(status = STABLE) + T orElseThrow() throws CheckedErrorResultException; + + /** + * Return the successful value from the result, or throws the error if it is an instance of the type specified, + * otherwise it will throw an {@link UnexpectedErrorResultException} with the error as the cause. + * + *

+     * Integer result = Result.of(() -> getValue())
+     *                        .orElseThrow(IOException.class);
+     * 
+ * + * @param type the type of checked exception that may be thrown + * @param the type of the checked exception to throw + * @return the value if a success + * @throws E if the result is an error + */ + @API(status = STABLE) + T orElseThrow(Class type) throws E; + + /** + * Returns the successful value from the result, or throws an {@link ErrorResultException}, an unchecked exception, + * with the error as the cause. + * + *

+     * Integer result = Result.of(() -> getValue())
+     *                        .orElseThrowUnchecked();
+     * 
+ * + * @return the value if a success + */ + @API(status = STABLE) + T orElseThrowUnchecked(); + + /** + * Applies the function to the value within the {@code Result} and returns the result if this is a success, + * otherwise returns a new {@code Result} with the existing error. + * + *

+     * Result<String> result = Result.of(() -> getValue())
+     *                               .flatMap(v -> Result.of(() -> String.valueOf(v)));
+     * 
+ * + * @param f the mapping function the produces a Result + * @param the type of the value withing the Result of the mapping function + * @return a Result + */ + @API(status = STABLE) + Result flatMap(Function> f); + + /** + * Applies the function to the value within the {@code Result} and returns the void result if this is a success, + * otherwise returns a new {@code Result} with the existing error. + * + *

+     * ResultVoid result = Result.of(() -> getValue())
+     *                           .flatMapV(v -> Result.ok());
+     * 
+ * + * @param f the mapping function the produces a ResultVoid + * @return a ResultVoid + */ + @API(status = STABLE) + ResultVoid flatMapV(Function f); + + /** + * Applies the function to the value within the {@code Result}, returning + * the result within another {@code Result}. + * + *

If the initial {@code Result} is a success, then apply the function to + * the value within the {@code Result}, returning the result within another + * {@code Result}. If the initial {@code Result} is an error, then return + * another error without invoking the supplied function.

+ * + *

If the supplied function throws an exception, then an error + * {@code Result} will be returned containing that exception.

+ * + *

+     * Result<String> result = Result.of(() -> getValue())
+     *                               .map(v -> String.valueOf(v));
+     * 
+ * + * @param f the function to apply + * @param the type of the value returned by the function to be applied + * @return A {@code Result} containing either the original error, the + * function output, or any exception thrown by the supplied function. + */ @Override - Result map(Function f); + @API(status = STABLE) + Result map(ThrowableFunction f); /** - * Matches the Result, either success or error, and supplies the appropriate Consumer with the value or error. + * Matches the Result, either success or error, and supplies the appropriate + * Consumer with the value or error. + * + *

+     * Result.of(()-> getValue())
+     *       .match(
+     *           success -> doSomething(success),
+     *           error -> handleError(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 */ + @API(status = STABLE) 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); - /** * Provide the value within the Result, if it is a success, to the Consumer, and returns this Result. * + *

+     * Result<Integer> result = Result.of(() -> getValue())
+     *                                .peek(v -> System.out.println(v));
+     * 
+ * * @param consumer the Consumer to the value if a success * @return this Result */ + @API(status = STABLE) Result peek(Consumer consumer); /** - * Provide a way to attempt to recover from an error state. + * Attempts to restore an error {@code Result} to a success. + * + *

When the Result is already a success, then the result is returned + * unmodified.

+ * + *

+     * Result<Integer> result = Result.of(() -> getValue())
+     *                                .recover(e -> Result.of(() -> getSafeValue(e)));
+     * 
* * @param f the function to recover from the error - * @return a new Result, either a Success, or if recovery is not possible an other Err. + * @return if Result is an error, a new Result, either a Success, or if + * recovery is not possible another error. If the Result is already a + * success, then this returns itself. */ + @API(status = STABLE) Result recover(Function> f); /** - * A handler for error states. + * A handler for success states. * - *

When this is an error then tne Consumer will be supplier with the error. When this is a success, then nothing - * happens.

+ *

+     * void handleSuccess(Integer value) {...}
+     * Result.of(() -> getValue())
+     *       .onSuccess(v -> handleSuccess(v));
+     * 
* - * @param errorConsumer the consumer to handle the error + *

When this is a success then tne Consumer will be supplied with the + * success value. When this is an error, then nothing happens.

+ * + * @param successConsumer the consumer to handle the success */ - void onError(Consumer errorConsumer); + @API(status = STABLE) + void onSuccess(Consumer successConsumer); + + /** + * A handler for error state, when the error matches the errorClass. + * + *

If the `Result` is an error and that error is an instance of the + * errorClass, then supply the error to the `Consumer`. Does nothing if the + * error is not an instance of the errorClass, or is a success.

+ * + *

Similar to the catch block in a try-catch.

+ * + *

+     * void handleError(UnsupportedOperationException e) {...}
+     * Result.of(() -> getValue())
+     *       .onError(UnsupportedOperationException.class,
+     *                e -> handleError(e))
+     * 
+ * + * @param errorClass the class of Throwable to match + * @param consumer the consumer to call if it matches + * @param the Type of the Throwable to match + * @return the original unmodified Result + */ + @API(status = STABLE) + Result onError( + Class errorClass, + Consumer consumer + ); /** * Maps a Success Result to another Result using a Callable that is able to throw a checked exception. @@ -272,10 +744,10 @@ public interface Result extends Functor> { *

Combination of {@link #flatMap(Function)} and {@link #of(Callable)}.

* *

-     *     Integer doSomething() {...}
-     *     String doSomethingElse(final Integer value) {...}
-     *     Result<String> r = Result.of(() -> doSomething())
-     *                              .andThen(value -> () -> doSomethingElse(value));
+     * Integer doSomething() {...}
+     * String doSomethingElse(final Integer value) {...}
+     * Result<String> r = Result.of(() -> doSomething())
+     *                          .andThen(value -> () -> doSomethingElse(value));
      * 
* *

When the Result is an Err, then the original error is carried over and the Callable is never called.

@@ -283,18 +755,47 @@ public interface Result extends Functor> { * @param f the function to map the Success value into the Callable * @param the type of the final Result * @return a new Result + * @deprecated Use {@link #map(ThrowableFunction)} */ + @API(status = DEPRECATED) + @Deprecated Result andThen(Function> f); + /** + * Perform the continuation with the value within the success {@code Result} + * and return itself. + * + *

Where the {@code Result} is a success, then if an exception is thrown + * by the continuation the {@code Result} returned will be a new error + * {@code Result} containing that exception, otherwise the original + * {@code Result}will be returned.

+ + *

Where the {@code Result} is an error, then the {@code Result} is + * returned immediately and the continuation is ignored.

+ * + *

+     * Integer doSomething() {...}
+     * void doSomethingElse(final Integer value) {...}
+     * Result<Integer> r = Result.of(() -> doSomething())
+     *                           .thenWith(value -> () -> doSomethingElse(value));
+     * 
+ * + * @param f the function to map the Success value into the result + * continuation + * @return the Result or a new error Result + */ + @API(status = STABLE) + Result thenWith(Function> f); + /** * Perform the continuation with the current Result value then return the current Result, assuming there was no * error in the continuation. * *

-     *     Integer doSomething() {...}
-     *     void doSomethingElse(final Integer value) {...}
-     *     Result<Integer> r = Result.of(() -> doSomething())
-     *                              .thenWith(value -> () -> doSomethingElse(value));
+     * Integer doSomething() {...}
+     * void doSomethingElse(final Integer value) {...}
+     * Result<Integer> r = Result.of(() -> doSomething())
+     *                           .thenWith(value -> () -> doSomethingElse(value));
      * 
* *

Where the Result is an Err, then the Result is returned immediately and the continuation is ignored.

@@ -304,7 +805,8 @@ public interface Result extends Functor> { * @param f the function to map the Success value into the result continuation * @return the Result or a new error Result */ - Result thenWith(Function> f); + @API(status = STABLE) + ResultVoid thenWithV(Function> f); /** * Reduce two Results of the same type into one using the reducing function provided. @@ -312,9 +814,24 @@ public interface Result extends Functor> { *

If either Result is an error, then the reduce will return the error. If both are errors, then the error of * {@code this} Result will be returned.

* - * @param identify the identify Result + * @param identify the identity Result * @param operator the function to combine the values the Results * @return a Result containing the combination of the two Results */ + @API(status = EXPERIMENTAL) Result reduce(Result identify, BinaryOperator operator); + + /** + * Discard any success value while retaining any error. + * + *

+     * ResultVoid result = Result.of(() -> getResultValue())
+     *                           .toVoid();
+     * 
+ * + * @return A {@code SuccessVoid} for a {@code Success} or a {@code ErrVoid} for an {@code Err}. + */ + @API(status = STABLE) + ResultVoid toVoid(); + } diff --git a/src/main/java/net/kemitix/mon/result/ResultVoid.java b/src/main/java/net/kemitix/mon/result/ResultVoid.java new file mode 100644 index 0000000..1d77b3c --- /dev/null +++ b/src/main/java/net/kemitix/mon/result/ResultVoid.java @@ -0,0 +1,134 @@ +package net.kemitix.mon.result; + +import org.apiguardian.api.API; + +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * A @{link Result} with no value. + */ +public interface ResultVoid extends BaseResult { + + /** + * Matches the Result, either success or error, and supplies the appropriate + * Consumer with the value or error. + * + *

+     * Result.ok()
+     *       .match(
+     *           () -> doSomething(),
+     *           error -> handleError(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 + */ + @API(status = STABLE) + void match(Runnable onSuccess, Consumer onError); + + /** + * Attempts to restore an error {@code ResultVoid} to a success. + * + *

When the Result is already a success, then the result is returned + * unmodified.

+ * + *

+     * void doSomethingRisky(String s) throws Exception {...}
+     * ResultVoid result = Result.ofVoid(() -> doSomethingRisky("first"))
+     *                           .recover(e -> Result.ofVoid(() -> doSomethingRisky("second")));
+     * 
+ * + * @param f the function to recover from the error + * @return if Result is an error, a new Result, either a Success, or if + * recovery is not possible another error. If the Result is already a + * success, then this returns itself. + */ + @API(status = STABLE) + ResultVoid recover(Function f); + + + /** + * A handler for success states. + * + *

+     * void doSomethingRisky() throws Exception {...}
+     * void handleSuccess() {...}
+     * Result.ofVoid(() -> doSomethingRisky()) // ResultVoid
+     *       .onSuccess(() -> handleSuccess());
+     * 
+ * + *

When this is a success then tne Consumer will be supplied with the + * success value. When this is an error, then nothing happens.

+ * + * @param runnable the call if the Result is a success + */ + @API(status = STABLE) + void onSuccess(Runnable runnable); + + /** + * A handler for error state, when the error matches the errorClass. + * + *

If the `Result` is an error and that error is an instance of the + * errorClass, then supply the error to the `Consumer`. Does nothing if the + * error is not an instance of the errorClass, or is a success.

+ * + *

Similar to the catch block in a try-catch.

+ * + *

+     * void handleError(UnsupportedOperationException e) {...}
+     * Result.of(() -> getValue())
+     *       .onError(UnsupportedOperationException.class,
+     *                e -> handleError(e))
+     * 
+ * + * @param errorClass the class of Throwable to match + * @param consumer the consumer to call if it matches + * @param the Type of the Throwable to match + * @return the original unmodified Result + */ + @API(status = STABLE) + ResultVoid onError( + Class errorClass, + Consumer consumer + ); + + /** + * Execute the callable if the {@code Result} is a success, ignore it if is an error. + * + *

+     * Result.ofVoid(() -> doSomethingRisky())
+     *       .andThen(() -> doSomethingRisky("again"));
+     * 
+ * + * @param f the function to map the Success value into the Callable + * @return itself unless the callable fails when it will return a new error Result + */ + @API(status = STABLE) + ResultVoid andThen(VoidCallable f); + + /** + * Replaces the current Result with the result of the callable. + * + *

Discards the success/error state or the current Result.

+ * + *

If the callable results in a new error, then that error will be in the returned Result.

+ * + *

+     * Result<Integer> result = Result.ofVoid(() -> doSomethingRisky())
+     *                          .inject(() -> 1);
+     * 
+ * + * @param callable the callable to create the new value + * @param the type of the new value + * @return a new Result with the result of callable + */ + default Result inject(Callable callable) { + return Result.of(callable); + } + +} diff --git a/src/main/java/net/kemitix/mon/result/Success.java b/src/main/java/net/kemitix/mon/result/Success.java index 5c0a13d..89a8c03 100644 --- a/src/main/java/net/kemitix/mon/result/Success.java +++ b/src/main/java/net/kemitix/mon/result/Success.java @@ -22,14 +22,12 @@ package net.kemitix.mon.result; import lombok.RequiredArgsConstructor; -import net.kemitix.mon.maybe.Maybe; import java.util.Objects; import java.util.concurrent.Callable; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Predicate; /** * A Successful Result. @@ -37,7 +35,8 @@ import java.util.function.Predicate; * @param the type of the value in the Result */ @RequiredArgsConstructor -@SuppressWarnings({"methodcount", "PMD.CyclomaticComplexity"}) +@SuppressWarnings({"methodcount", "PMD.TooManyMethods", "PMD.ExcessivePublicCount", + "PMD.CyclomaticComplexity"}) class Success implements Result { private final T value; @@ -54,11 +53,11 @@ class Success implements Result { @Override @SuppressWarnings({"illegalcatch", "PMD.AvoidCatchingThrowable"}) - public Result map(final Function f) { + public Result map(final ThrowableFunction f) { try { - return success(f.apply(this.value)); + return new Success<>(f.apply(value)); } catch (Throwable e) { - return err(e); + return new Err<>(e); } } @@ -67,14 +66,6 @@ class Success implements Result { onSuccess.accept(value); } - @Override - public Result> maybe(final Predicate predicate) { - if (predicate.test(value)) { - return success(Maybe.just(value)); - } - return success(Maybe.nothing()); - } - @Override public T orElseThrow() { return value; @@ -101,11 +92,24 @@ class Success implements Result { return this; } + @Override + public void onSuccess(final Consumer successConsumer) { + successConsumer.accept(value); + } + @Override public void onError(final Consumer errorConsumer) { // do nothing - this is not an error } + @Override + public Result onError( + final Class errorClass, + final Consumer consumer + ) { + return this; + } + @Override public Result andThen(final Function> f) { return result(f.apply(value)); @@ -116,6 +120,11 @@ class Success implements Result { return f.apply(value).call(this); } + @Override + public ResultVoid thenWithV(final Function> f) { + return f.apply(value).call(this).toVoid(); + } + @Override public Result reduce(final Result identity, final BinaryOperator operator) { return flatMap(a -> identity.flatMap(b -> result(() -> operator.apply(a, b)))); @@ -126,6 +135,11 @@ class Success implements Result { return f.apply(value); } + @Override + public ResultVoid flatMapV(final Function f) { + return f.apply(value); + } + @Override public boolean equals(final Object other) { return other instanceof Success && Objects.equals(value, ((Success) other).value); @@ -140,4 +154,9 @@ class Success implements Result { public String toString() { return String.format("Result.Success{value=%s}", value); } + + @Override + public ResultVoid toVoid() { + return SuccessVoid.getInstance(); + } } diff --git a/src/main/java/net/kemitix/mon/result/SuccessVoid.java b/src/main/java/net/kemitix/mon/result/SuccessVoid.java new file mode 100644 index 0000000..7d099ea --- /dev/null +++ b/src/main/java/net/kemitix/mon/result/SuccessVoid.java @@ -0,0 +1,91 @@ +package net.kemitix.mon.result; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public class SuccessVoid implements ResultVoid { + + private static final ResultVoid INSTANCE = new SuccessVoid(); + + /** + * Get the SuccessVoid instance. + * + *

The SuccessVoid, having no value, represents the state of success.

+ * + * @return the SuccessVoid + */ + public static ResultVoid getInstance() { + return INSTANCE; + } + + @Override + public boolean isError() { + return false; + } + + @Override + public boolean isOkay() { + return true; + } + + @Override + public void match(final Runnable onSuccess, final Consumer onError) { + onSuccess.run(); + } + + @Override + public ResultVoid recover(final Function f) { + return this; + } + + @Override + public void onSuccess(final Runnable runnable) { + runnable.run(); + } + + @Override + public void onError(final Consumer errorConsumer) { + // do nothing - this is not an error + } + + @Override + public ResultVoid onError( + final Class errorClass, + final Consumer consumer + ) { + return this; + } + + @Override + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public ResultVoid andThen(final VoidCallable f) { + try { + f.call(); + return this; + } catch (Exception e) { + return new ErrVoid(e); + } + } + + @Override + public boolean equals(final Object other) { + return other instanceof SuccessVoid; + } + + @Override + public int hashCode() { + return Objects.hash(SuccessVoid.class); + } + + @Override + public String toString() { + return "Result.SuccessVoid{}"; + } + +} diff --git a/src/main/java/net/kemitix/mon/result/ThrowableFunction.java b/src/main/java/net/kemitix/mon/result/ThrowableFunction.java new file mode 100644 index 0000000..6900f48 --- /dev/null +++ b/src/main/java/net/kemitix/mon/result/ThrowableFunction.java @@ -0,0 +1,23 @@ +package net.kemitix.mon.result; + +/** + * Represents a function that accepts one argument and produces a result. + * This is a functional interface whose functional method is apply(Object). + * + * @param the type of the input to the function + * @param the type of the result of the function + * @param the type of the exception that could be thrown + */ +@FunctionalInterface +public interface ThrowableFunction { + + /** + * Applies this function to the given argument. + * + * @param value the function argument + * @return the function result + * @throws E if the function fails + */ + R apply(T value) throws E; + +} diff --git a/src/main/java/net/kemitix/mon/result/VoidCallable.java b/src/main/java/net/kemitix/mon/result/VoidCallable.java new file mode 100644 index 0000000..4519962 --- /dev/null +++ b/src/main/java/net/kemitix/mon/result/VoidCallable.java @@ -0,0 +1,19 @@ +package net.kemitix.mon.result; + +/** + * A task that returns void and may throw an exception. + * + *

Implementors define a single method with no arguments called call. + * The VoidCallable interface is similar to {@link java.util.concurrent.Callable}, + * but does not return a value.

+ */ +@FunctionalInterface +public interface VoidCallable { + /** + * Executes and may throw an exception. + * + * @throws Exception if unable to complete + */ + @SuppressWarnings("PMD.SignatureDeclareThrowsException") + void call() throws Exception; +} diff --git a/src/main/java/net/kemitix/mon/result/WithResultContinuation.java b/src/main/java/net/kemitix/mon/result/WithResultContinuation.java index e4baa8c..46412de 100644 --- a/src/main/java/net/kemitix/mon/result/WithResultContinuation.java +++ b/src/main/java/net/kemitix/mon/result/WithResultContinuation.java @@ -21,6 +21,8 @@ package net.kemitix.mon.result; +import net.kemitix.mon.TypeReference; + /** * A Callable-like interface for performing an action with a Result that, if there are no errors is returned as-is, but * if there is an error then a new error Result is returned. @@ -50,7 +52,7 @@ public interface WithResultContinuation { try { run(); } catch (Throwable e) { - return Result.error(e); + return Result.error(TypeReference.create(), e); } return currentResult; } diff --git a/src/main/java/net/kemitix/mon/result/package-info.java b/src/main/java/net/kemitix/mon/result/package-info.java index b9f390a..61fb32e 100644 --- a/src/main/java/net/kemitix/mon/result/package-info.java +++ b/src/main/java/net/kemitix/mon/result/package-info.java @@ -20,9 +20,28 @@ */ /** - * An experiment in creating something similar to a Type-Alias in Java. + *

Result

* - *

Ideas initially lifted from the Design with Types series at https://fsharpforfunandprofit.com/

+ * Allows handling error conditions without the need to {@code catch} + * exceptions. + * + *

When a {@link Result} is returned from a method, it will be in one of two + * states: {@link Success} or {@link Err}. The {@code Success} state will + * contain a value from the method. The {@code Err} state will contain a + * {@link java.lang.Throwable} detailing the reason for the failure.

+ * + *

Methods returning a {@code Result} should not throw any exceptions.

+ * + *

{@code Result} is a Monad.

+ * + *

Static Constructors:

+ *
    + *
  • {@link Result#ok()}
  • + *
  • {@link Result#ok(Object)}
  • + *
  • {@link Result#of(Callable)}
  • + *
  • {@link Result#ofVoid(VoidCallable)}
  • + *
  • {@link Result#error(Throwable)}
  • + *
* * @author Paul Campbell (pcampbell@kemitix.net) */ diff --git a/src/test/java/net/kemitix/mon/MaybeTest.java b/src/test/java/net/kemitix/mon/MaybeTest.java index 4da89c9..383db36 100644 --- a/src/test/java/net/kemitix/mon/MaybeTest.java +++ b/src/test/java/net/kemitix/mon/MaybeTest.java @@ -114,11 +114,17 @@ class MaybeTest implements WithAssertions { } @Test - void fromOptional() { + void mapFromOptional() { assertThat(Optional.of(1).map(Maybe::just).orElseGet(Maybe::nothing)).isEqualTo(just(1)); assertThat(Optional.empty().map(Maybe::just).orElseGet(Maybe::nothing)).isEqualTo(nothing()); } + @Test + void fromOptional() { + assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(just(1)); + assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(nothing()); + } + @Test void peek() { final AtomicInteger ref = new AtomicInteger(0); diff --git a/src/test/java/net/kemitix/mon/ResultTest.java b/src/test/java/net/kemitix/mon/ResultTest.java index 231702b..0b92742 100644 --- a/src/test/java/net/kemitix/mon/ResultTest.java +++ b/src/test/java/net/kemitix/mon/ResultTest.java @@ -1,805 +1,2277 @@ package net.kemitix.mon; import lombok.RequiredArgsConstructor; +import net.kemitix.mon.experimental.either.Either; import net.kemitix.mon.maybe.Maybe; import net.kemitix.mon.result.CheckedErrorResultException; import net.kemitix.mon.result.ErrorResultException; -import net.kemitix.mon.result.UnexpectedErrorResultException; import net.kemitix.mon.result.Result; +import net.kemitix.mon.result.ResultVoid; +import net.kemitix.mon.result.SuccessVoid; +import net.kemitix.mon.result.UnexpectedErrorResultException; +import net.kemitix.mon.result.VoidCallable; import org.assertj.core.api.WithAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; class ResultTest implements WithAssertions { - @Test - void equality() { - assertThat(Result.ok(1)).isEqualTo(Result.ok(1)); - assertThat(Result.ok(1)).isNotEqualTo(Result.ok(2)); - final RuntimeException runtimeException = new RuntimeException(); - assertThat(Result.ok(1)).isNotEqualTo(Result.error(runtimeException)); - assertThat(Result.error(runtimeException)).isEqualTo(Result.error(runtimeException)); - assertThat(Result.error(runtimeException)).isNotEqualTo(Result.error(new RuntimeException())); - assertThat(Result.ok(1).equals("1")).isFalse(); - assertThat(Result.error(new RuntimeException()).equals("1")).isFalse(); + @Nested + @DisplayName("Basic Properties") + class BasicPropertiesTests { + + @Test + @SuppressWarnings({"PMD.JUnitTestContainsTooManyAsserts"}) + void equality() { + final RuntimeException runtimeException = new RuntimeException(); + // Success + assertThat(Result.ok(1)).as("Success: same integer value").isEqualTo(Result.ok(1)); + assertThat(Result.ok(1)).as("Success: diff integer values").isNotEqualTo(Result.ok(2)); + assertThat(Result.ok(1).equals("1")).as("Success: string v integer").isFalse(); // NOPMD + assertThat(Result.ok(1)).as("Success: success v error").isNotEqualTo(Result.error(runtimeException)); + // SuccessVoid + assertThat(Result.ok()).as("SuccessVoid: void v void").isEqualTo(Result.ok()); + assertThat(Result.ok().hashCode()).as("SuccessVoid: hash v void").isNotNull(); + assertThat(Result.ok()).as("SuccessVoid: void v integer").isNotEqualTo(Result.ok(1)); + assertThat(Result.ok()).as("SuccessVoid: v ErrVoid").isNotEqualTo(Result.error(runtimeException)); + // Error + TypeReference integerReference = TypeReference.create(); + assertThat(Result.error(integerReference, runtimeException)).as("error v error").isEqualTo(Result.error(integerReference, runtimeException)); + assertThat(Result.error(integerReference, runtimeException)).as("error v other error").isNotEqualTo(Result.error(integerReference, new RuntimeException())); + assertThat(Result.error(integerReference, runtimeException)).as("error v string").isNotEqualTo("1"); + // ErrorVoid + assertThat(Result.error(runtimeException)).as("same error value").isEqualTo(Result.error(runtimeException)); + assertThat(Result.error(runtimeException)).as("diff error values").isNotEqualTo(Result.error(new RuntimeException())); + assertThat(Result.error(new RuntimeException()).equals("1")).as("error v string").isFalse(); // NOPMD + } + + @Test + void successHashCodesAreUnique() { + assertThat(Result.ok(1).hashCode()).isNotEqualTo(Result.ok(2).hashCode()); + } + + @Test + void errorHashCodesAreUnique() { + // despite having 'equivalent' exceptions, the exceptions are distinct instances, so should be considered unique + //given + final RuntimeException exception1 = new RuntimeException("message"); + final RuntimeException exception2 = new RuntimeException("message"); + assumeThat(exception1.hashCode()).isNotEqualTo(exception2.hashCode()); + //then + assertThat(Result.error(exception1).hashCode()).isNotEqualTo(Result.error(exception2).hashCode()); + } + + @Test + void errorVHashCodesAreUnique() { + // despite having 'equivalent' exceptions, the exceptions are distinct instances, so should be considered unique + //given + final RuntimeException exception1 = new RuntimeException("message"); + final RuntimeException exception2 = new RuntimeException("message"); + assumeThat(exception1.hashCode()).isNotEqualTo(exception2.hashCode()); + TypeReference integerReference = TypeReference.create(); + //then + assertThat(Result.error(integerReference, exception1).hashCode()) + .isNotEqualTo(Result.error(integerReference, exception2).hashCode()); + } + + @Test + void whenOkVoid_isOkay() { + //when + var result = Result.ok(); + //then + assertThat(result.isOkay()).isTrue(); + } + + @Test + void whenOkVoid_isError() { + //when + var result = Result.ok(); + //then + assertThat(result.isError()).isFalse(); + } + + @Test + void whenOkayVoid_match_isNull() { + //when + var result = Result.ok(); + //then + result.match( + () -> assertThat(true).isTrue(), + error -> fail("not an error") + ); + } + + @Test + void whenOk_isOkay() { + //when + final Result result = Result.ok("good"); + //then + assertThat(result.isOkay()).isTrue(); + } + + @Test + void whenOkay_isNotError() { + //when + final Result result = Result.ok("good"); + //then + assertThat(result.isError()).isFalse(); + } + + @Test + void whenOkay_matchSuccess() { + //given + final Result result = Result.ok("good"); + //then + result.match( + success -> assertThat(success).isEqualTo("good"), + error -> fail("not an error") + ); + } + + @Test + void whenError_isError() { + //when + final Result result = anError(new Exception()); + //then + assertThat(result.isOkay()).isFalse(); + } + + @Test + void whenError_isNotSuccess() { + //when + final Result result = anError(new Exception()); + //then + assertThat(result.isError()).isTrue(); + } + + @Test + void whenError_matchError() { + //given + final Result result = anError(new Exception("bad")); + //then + result.match( + success -> fail("not a success"), + error -> assertThat(error.getMessage()).isEqualTo("bad") + ); + } + + @Test + void okay_toString() { + //given + final Result ok = Result.ok(1); + //when + final String toString = ok.toString(); + //then + assertThat(toString).contains("Result.Success{value=1}"); + } + + @Test + void okayVoid_toString() { + //given + final ResultVoid ok = Result.ok(); + //when + final String toString = ok.toString(); + //then + assertThat(toString).contains("Result.SuccessVoid{}"); + } + + @Test + void error_toString() { + //given + final Result error = anError(new RuntimeException("failed")); + //when + final String toString = error.toString(); + //then + assertThat(toString).contains("Result.Error{error=java.lang.RuntimeException: failed}"); + } + + @Test + void errorVoid_toString() { + //given + final ResultVoid error = Result.error(new RuntimeException("failed")); + //when + final String toString = error.toString(); + //then + assertThat(toString).contains("Result.ErrVoid{error=java.lang.RuntimeException: failed}"); + } + + @Test + void value_whenResultOf_isOkay() { + //given + final Callable c = () -> "okay"; + //when + final Result result = Result.of(c); + //then + result.match( + success -> assertThat(success).isEqualTo("okay"), + error -> fail("not an error") + ); + } + + @Test + void exception_whenResultOf_isError() { + //given + final Callable c = () -> { + throw new IOException(); + }; + //when + final Result result = Result.of(c); + //then + result.match( + success -> fail("not a success"), + error -> assertThat(error).isInstanceOf(IOException.class) + ); + } } - @Test - void successHashCodesAreUnique() { - assertThat(Result.ok(1).hashCode()).isNotEqualTo(Result.ok(2).hashCode()); + private Result anError(Exception e) { + return Result.ok(1) + .flatMap(s -> Result.of(() -> {throw e;})); } - @Test - void errorHashCodesAreUnique() { - // despite having 'equivalent' exceptions, the exceptions are distinct instances, so should be considered unique - //given - final RuntimeException exception1 = new RuntimeException("message"); - final RuntimeException exception2 = new RuntimeException("message"); - assumeThat(exception1.hashCode()).isNotEqualTo(exception2.hashCode()); - //then - assertThat(Result.error(exception1).hashCode()).isNotEqualTo(Result.error(exception2).hashCode()); + @Nested + @DisplayName("flatMap") + class FlatMapTests { + @Test + void okay_whenFlatMapToOkay_isOkay() { + //given + final Result ok = Result.ok("good"); + //when + final Result result = ok.flatMap(v -> Result.ok(v.toUpperCase())); + //then + result.match( + success -> assertThat(success).isEqualTo("GOOD"), + error -> fail("not an error") + ); + } + + @Test + void okay_whenFlatMapToError_isError() { + //given + final Result result = Result.ok("good"); + //when + final Result flatMap = result.flatMap(v -> anError(new Exception("bad flat map"))); + //then + assertThat(flatMap.isOkay()).isFalse(); + } + + @Test + void error_whenFlatMapToOkay_isError() { + //given + final Result result = anError(new Exception("bad")); + //when + final Result flatMap = result.flatMap(Result::ok); + //then + assertThat(flatMap.isError()).isTrue(); + } + + @Test + void error_whenFlatMapToError_isError() { + //given + final Result result = anError(new Exception("bad")); + //when + final Result flatMap = result.flatMap(v -> anError(new Exception("bad flat map"))); + //then + assertThat(flatMap.isError()).isTrue(); + } } - @Test - void whenOk_isOkay() { - //when - final Result result = Result.ok("good"); - //then - assertThat(result.isOkay()).isTrue(); + @Nested + @DisplayName("flatMapV") + class FlatMapVTests { + @Test + void okay_whenFlatMapVToOkay_isOkayVoid() { + //given + final Result result = Result.ok("good"); + //when + final ResultVoid flatMap = result.flatMapV(v -> Result.ok()); + //then + flatMap.match( + () -> assertThat(true).isTrue(), + error -> fail("not an error") + ); + } + + @Test + void okay_whenFlatMapVToError_isErrorVoid() { + //given + final Result result = Result.ok("good"); + //when + final ResultVoid flatMap = result.flatMapV(v -> Result.error(new Exception("bad flat map"))); + //then + assertThat(flatMap.isOkay()).isFalse(); + } + + @Test + void error_whenFlatMapVToOkay_isErrorVoid() { + //given + final Result result = anError(new Exception("bad")); + //when + final ResultVoid flatMap = result.flatMapV(value -> Result.ok()); + //then + assertThat(flatMap.isError()).isTrue(); + } + + @Test + void error_whenFlatMapVToError_isErrorVoid() { + //given + final Result result = anError(new Exception("bad")); + //when + final ResultVoid flatMap = result.flatMapV(v -> Result.error(new Exception("bad flat map"))); + //then + assertThat(flatMap.isError()).isTrue(); + } } - @Test - void whenOkay_isNotError() { - //when - final Result result = Result.ok("good"); - //then - assertThat(result.isError()).isFalse(); + @Nested + @DisplayName("map") + class MapTest { + @Test + void okay_whenMapToOkay_isOkay() { + //given + final Result okResult = Result.ok(1); + //when + final Result result = okResult.map(value -> String.valueOf(value)); + //then + result.match( + success -> assertThat(success).isEqualTo("1"), + error -> fail("not an error") + ); + } + + @Test + void okay_whenMapToError_isError() { + //given + final Result okResult = Result.ok(1); + //when + final Result result = okResult.map(value -> { + throw new RuntimeException("map error"); + }); + //then + result.match( + success -> fail("not an success"), + error -> assertThat(error).hasMessage("map error") + ); + } + + @Test + void error_whenMap_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result errorResult = anError(exception); + //when + final Result result = errorResult.map(value -> String.valueOf(value)); + //then + result.match( + success -> fail("not an success"), + error -> assertThat(error).isSameAs(exception) + ); + } } - @Test - void whenOkay_matchSuccess() { - //given - final Result result = Result.ok("good"); - //then - result.match( - success -> assertThat(success).isEqualTo("good"), - error -> fail("not an error") - ); + @Nested + @DisplayName("maybe") + class MaybeTests { + + @Nested + @DisplayName("fromMaybe") + class FromMaybeTests { + + @Test + void just_whenFromMaybe_isOkay() { + //given + final Maybe just = Maybe.just(1); + //when + final Result result = Result.from(just, () -> new RuntimeException()); + //then + result.match( + success -> assertThat(success).isEqualTo(1), + error -> fail("not an error") + ); + } + + @Test + void nothing_whenFromMaybe_isError() { + //given + final Maybe nothing = Maybe.nothing(); + final RuntimeException exception = new RuntimeException(); + //when + final Result result = Result.from(nothing, () -> exception); + //then + result.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + } + + @Nested + @DisplayName("toMaybe") + class ToMaybeTests { + + @Test + void okay_whenToMaybe_isJust() { + //given + final Result ok = Result.ok(1); + //when + final Maybe maybe = Result.toMaybe(ok); + //then + assertThat(maybe.toOptional()).contains(1); + } + + @Test + void error_whenToMaybe_isNothing() { + //given + final Result error = anError(new RuntimeException()); + //when + final Maybe maybe = Result.toMaybe(error); + //then + assertThat(maybe.toOptional()).isEmpty(); + } + } } - @Test - void whenError_isError() { - //when - final Result result = Result.error(new Exception()); - //then - assertThat(result.isOkay()).isFalse(); + @Nested + @DisplayName("orElseThrow") + class OrElseThrowTests { + @Test + void okay_whenOrElseThrow_isValue() throws Throwable { + //given + final Result ok = Result.ok(1); + //when + final Integer value = ok.orElseThrow(); + //then + assertThat(value).isEqualTo(1); + } + + @Test + void error_whenOrElseThrow_throws() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + assertThatThrownBy(() -> error.orElseThrow()) + .isInstanceOf(CheckedErrorResultException.class) + .hasCause(exception); + } + + @Test + void okay_whenOrElseThrowT_isValue() throws Exception { + //given + final Result ok = Result.ok(1); + //when + final Integer value = ok.orElseThrow(Exception.class); + //then + assumeThat(value).isEqualTo(1); + } + + @Test + void errorT_whenOrElseThrowT_throwsT() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //then + assertThatThrownBy(() -> error.orElseThrow(RuntimeException.class)).isSameAs(exception); + } + + @Test + void errorR_whenOrElseThrowT_throwsWrappedR() { + //given + final IOException exception = new IOException(); + final Result error = anError(exception); + //then + assertThatThrownBy(() -> error.orElseThrow(RuntimeException.class)) + .isInstanceOf(UnexpectedErrorResultException.class) + .hasCause(exception); + } } - @Test - void whenError_isNotSuccess() { - //when - final Result result = Result.error(new Exception()); - //then - assertThat(result.isError()).isTrue(); + @Nested + @DisplayName("orElseThrowUnchecked") + class OrElseThrowUncheckedTests { + @Test + void okay_whenOrElseThrowUnchecked_isValue() { + //given + final Result ok = Result.ok(1); + //when + final Integer value = ok.orElseThrowUnchecked(); + //then + assumeThat(value).isEqualTo(1); + } + + @Test + void error_whenOrElseThrowUnchecked_throwsWrapped() { + //given + final IOException exception = new IOException(); + final Result error = anError(exception); + //then + assertThatThrownBy(() -> error.orElseThrowUnchecked()) + .isInstanceOf(ErrorResultException.class) + .hasCause(exception); + } } - @Test - void whenError_matchError() { - //given - final Result result = Result.error(new Exception("bad")); - //then - result.match( - success -> fail("not a success"), - error -> assertThat(error.getMessage()).isEqualTo("bad") - ); + @Nested + @DisplayName("invert") + class InvertTests { + @Test + void justOkay_whenInvert_thenOkayJust() { + //given + final Maybe> justSuccess = Maybe.just(Result.ok(1)); + //when + final Result> result = Result.swap(justSuccess); + //then + result.match( + success -> assertThat(success.toOptional()).contains(1), + error -> fail("Not an error") + ); + } + + @Test + void JustError_whenInvert_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final Maybe> justError = Maybe.just(anError(exception)); + //when + final Result> result = Result.swap(justError); + //then + result.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + void nothing_whenInvert_thenOkayNothing() { + //given + final Maybe> nothing = Maybe.nothing(); + //when + final Result> result = Result.swap(nothing); + //then + result.match( + success -> assertThat(success.toOptional()).isEmpty(), + error -> fail("Not an error") + ); + } } - @Test - void okay_whenFlatMapToOkay_isOkay() { - //given - final Result result = Result.ok("good"); - //when - final Result flatMap = result.flatMap(v -> Result.ok(v.toUpperCase())); - //then - assertThat(flatMap.isOkay()).isTrue(); - flatMap.match( - success -> assertThat(success).isEqualTo("GOOD"), - error -> fail("not an error") - ); + @Nested + @DisplayName("use cases") + class UseCaseTests { + @Test + void useCase_whenOkay_thenReturnSuccess() { + //given + final UseCase useCase = UseCase.isOkay(); + //when + final Result doubleResult = useCase.businessOperation("file a", "file bc"); + //then + doubleResult.match( + success -> assertThat(success).isEqualTo(7.5), + error -> fail("not an error") + ); + } + + @Test + void useCase_whenFirstReadIsError_thenReturnError() { + //given + final UseCase useCase = UseCase.firstReadInvalid(); + //when + final Result doubleResult = useCase.businessOperation("file def", "file ghij"); + //then + doubleResult.match( + success -> fail("not okay"), + error -> assertThat(error) + .isInstanceOf(RuntimeException.class) + .hasMessage("file def") + ); + } + + @Test + void useCase_whenSecondReadIsError_thenReturnError() { + //given + final UseCase useCase = UseCase.secondReadInvalid(); + //when + final Result doubleResult = useCase.businessOperation("file klmno", "file pqrstu"); + //then + doubleResult.match( + success -> fail("not okay"), + error -> assertThat(error) + .isInstanceOf(RuntimeException.class) + .hasMessage("file klmno") + ); + } } - @Test - void okay_whenFlatMapToError_isError() { - //given - final Result result = Result.ok("good"); - //when - final Result flatMap = result.flatMap(v -> Result.error(new Exception("bad flat map"))); - //then - assertThat(flatMap.isOkay()).isFalse(); + @Nested + @DisplayName("peek") + class PeekTests { + @Test + void okay_whenPeek_isConsumed() { + //given + final Result okay = Result.ok(1); + final AtomicBoolean consumed = new AtomicBoolean(false); + //when + okay.peek(v -> consumed.set(true)); + //then + assertThat(consumed).isTrue(); + } + + @Test + void okay_whenPeek_isOriginal() { + //given + final Result okay = Result.ok(1); + final AtomicBoolean consumed = new AtomicBoolean(false); + //when + final Result result = okay.peek(v -> consumed.set(true)); + //then + assertThat(result).isSameAs(okay); + } + + @Test + void error_whenPeek_isNotConsumed() { + //given + final Result error = anError(new RuntimeException()); + final AtomicBoolean consumed = new AtomicBoolean(false); + //when + error.peek(newValue -> consumed.set(true)); + //then + assertThat(consumed).isFalse(); // peek should not occur + } + @Test + void error_whenPeek_isSelf() { + //given + final Result error = anError(new RuntimeException()); + final AtomicBoolean consumed = new AtomicBoolean(false); + //when + final Result result = error.peek(v -> consumed.set(true)); + //then + assertThat(result).isSameAs(error); + } } - @Test - void error_whenFlatMapToOkay_isError() { - //given - final Result result = Result.error(new Exception("bad")); - //when - final Result flatMap = result.flatMap(v -> Result.ok(v.toUpperCase())); - //then - assertThat(flatMap.isError()).isTrue(); + @Nested + @DisplayName("onError - all error") + class OnErrorTests { + @Test + void okay_whenOnError_isIgnored() { + //given + final Result ok = Result.ok(1); + //when + ok.onError(e -> fail("not an error")); + } + + @Test + void error_whenOnError_isConsumed() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + final AtomicReference capture = new AtomicReference<>(); + //when + error.onError(capture::set); + //then + assertThat(capture).hasValue(exception); + } + + @Test + void okayVoid_whenOnError_isIgnored() { + //given + final ResultVoid ok = Result.ok(); + //when + ok.onError(e -> fail("not an error")); + } + + @Test + void errorVoid_whenOnError_isConsumed() { + //given + final RuntimeException exception = new RuntimeException(); + final ResultVoid error = Result.error(exception); + final AtomicReference capture = new AtomicReference<>(); + //when + error.onError(capture::set); + //then + assertThat(capture).hasValue(exception); + } } - @Test - void error_whenFlatMapToError_isError() { - //given - final Result result = Result.error(new Exception("bad")); - //when - final Result flatMap = result.flatMap(v -> Result.error(new Exception("bad flat map"))); - //then - assertThat(flatMap.isError()).isTrue(); + @Nested + @DisplayName("onError - by type") + class OnErrorByTypeTests { + + @Test + @DisplayName("okay when on error is ignored") + void okay_whenOnError_isIgnored() { + //given + final Result ok = Result.ok(1); + //when + ok.onError(Throwable.class, + e -> fail("not an error")); + } + + @Test + @DisplayName("error with matching type is consumed") + void error_whenOnError_isConsumed() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + final AtomicReference capture = new AtomicReference<>(); + //when + error.onError(RuntimeException.class, + capture::set); + //then + assertThat(capture).hasValue(exception); + } + + @Test + @DisplayName("error with non-matching type is ignored") + void error_withNoMatch_whenOnError_isIgnored() { + //given + RuntimeException exception = new RuntimeException(); + Result error = anError(exception); + final AtomicReference capture = new AtomicReference<>(); + //when + error.onError(Exception.class, + capture::set); + //then + assertThat(capture).hasValue(null); + } + + @Test + @DisplayName("okay void when on error is ignored") + void okayVoid_whenOnError_isIgnored() { + //given + final ResultVoid ok = Result.ok(); + //when + ok.onError(Throwable.class, + e -> fail("not an error")); + } + + @Test + @DisplayName("error void with matching type is consumed") + void errorVoid_whenOnError_isConsumed() { + //given + final RuntimeException exception = new RuntimeException(); + final ResultVoid error = Result.error(exception); + final AtomicReference capture = new AtomicReference<>(); + //when + error.onError(RuntimeException.class, + capture::set); + //then + assertThat(capture).hasValue(exception); + } + + @Test + @DisplayName("error void with non-matching type is ignored") + void errorVoid_withNoMatch_whenOnError_isIgnored() { + //given + RuntimeException exception = new RuntimeException(); + ResultVoid error = Result.error(exception); + final AtomicReference capture = new AtomicReference<>(); + //when + error.onError(Exception.class, + capture::set); + //then + assertThat(capture).hasValue(null); + } } - @Test - void okay_whenMapToOkay_isOkay() { - //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") - ); + @Nested + @DisplayName("onSuccess") + class OnSuccessTests { + @Test + void error_whenOnSuccess_isIgnored() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + error.onSuccess(e -> fail("not a success")); + } + + @Test + void okay_whenOnSuccess_isConsumed() { + //given + final AtomicReference capture = new AtomicReference<>(); + final Result ok = Result.ok(1); + //when + ok.onSuccess(capture::set); + //then + assertThat(capture).hasValue(1); + } + + @Test + void errorVoid_whenOnSuccess_isIgnored() { + //given + final RuntimeException exception = new RuntimeException(); + final ResultVoid error = Result.error(exception); + //when + error.onSuccess(() -> fail("not a success")); + } + + @Test + void okayVoid_whenOnSuccess_isConsumed() { + //given + final AtomicReference capture = new AtomicReference<>(); + final ResultVoid ok = Result.ok(); + //when + ok.onSuccess(() -> capture.set(1)); + //then + assertThat(capture).hasValue(1); + } } - @Test - void okay_whenMapToError_isError() { - //given - final Result okResult = Result.ok(1); - //when - final Result result = okResult.map(value -> { - throw new RuntimeException("map error"); - }); - //then - assertThat(result.isError()).isTrue(); - result.match( - success -> fail("not an success"), - error -> assertThat(error).hasMessage("map error") - ); + @Nested + @DisplayName("recover") + class RecoverTests { + @Nested @DisplayName("Result") + class ResultRecoveryTests { + @Test + void okay_whenRecover_thenNoChange() { + //given + final Result ok = Result.ok(1); + //when + final Result recovered = ok.recover(e -> Result.ok(2)); + //then + assertThat(recovered).isSameAs(ok); + } + + @Test + void error_whenRecover_isSuccess() { + //given + final Result error = anError(new RuntimeException()); + //when + // recover can't change the type of the result + final Result recovered = error.recover(e -> Result.ok(2)); + //then + recovered.peek(v -> assertThat(v).isEqualTo(2)); + } + + @Test + void error_whenRecover_whereError_isUpdatedError() { + //given + final Result error = anError(new RuntimeException("original")); + //when + final Result recovered = error.recover(e -> anError(new RuntimeException("updated"))); + //then + recovered.onError(e -> assertThat(e).hasMessage("updated")); + } + } + @Nested @DisplayName("ResultVoid") + class ResultVoidRecoveryTests { + @Test + void okayVoid_whenRecover_thenNoChange() { + //given + final ResultVoid ok = Result.ok(); + //when + final ResultVoid recovered = ok.recover(e -> Result.ok()); + //then + assertThat(recovered).isSameAs(ok); + } + + @Test + void error_whenRecover_isSuccess() { + //given + final ResultVoid error = Result.error(new RuntimeException()); + //when + // recover can't change the type of the result + final ResultVoid recovered = error.recover(e -> Result.ok()); + //then + assertThat(recovered.isOkay()).isTrue(); + } + + @Test + void error_whenRecover_whereError_isUpdatedError() { + //given + final ResultVoid error = Result.error(new RuntimeException("original")); + //when + final ResultVoid recovered = error.recover(e -> Result.error(new RuntimeException("updated"))); + //then + recovered.onError(e -> assertThat(e).hasMessage("updated")); + } + } } - @Test - 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) - ); + @Nested + @DisplayName("inject (recover for ResultVoid)") + class InjectTests { + @Test + @DisplayName("SuccessVoid#inject is Success with Value") + void okayVoid_whenInject_isOkayValue() { + //given + final ResultVoid ok = Result.ok(); + //when + final Result result = ok.inject(() -> 1); + //then + assertThat(result).isEqualTo(Result.ok(1)); + } + + @Test + @DisplayName("okayVoid when Inject fails then error") + void okayVoid_whenInjectIsError_isError() { + //given + final ResultVoid ok = Result.ok(); + //when + final Result result = ok.inject(() -> {throw new RuntimeException();}); + //then + assertThat(result.isError()).isTrue(); + } + + @Test + @DisplayName("errorVoid when inject is Success with Value") + void errorVoid_whenInject_isInjectedValue() { + //given + final ResultVoid error = Result.error(new RuntimeException()); + //when + final Result result = error.inject(() -> 1); + //then + assertThat(result).isEqualTo(Result.ok(1)); + } + + @Test + @DisplayName("errorVoid when Inject fails then new error") + void errorVoid_whenInjectIsError_isNewError() { + //given + final ResultVoid error = Result.error(new RuntimeException()); + RuntimeException exception = new RuntimeException(); + //when + final Result result = error.inject(() -> {throw exception;}); + //then + result.match( + x -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + } - @Test - void okay_whenMaybe_wherePasses_isOkayJust() { - //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") - ); + @Nested + @DisplayName("andThen") + class AndThenTests { + @Test + void okay_whenAndThen_whereSuccess_isUpdatedSuccess() { + //given + final Result ok = Result.ok(1); + //when + final Result result = ok.andThen(v -> () -> "success"); + //then + result.match( + v -> assertThat(v).isEqualTo("success"), + e -> fail("not an error")); + } + + @Test + void okay_whenAndThen_whereError_isError() { + //given + final Result ok = Result.ok(1); + final RuntimeException exception = new RuntimeException(); + //when + final Result result = ok.andThen(v -> () -> { + throw exception; + }); + //then + result.match( + x -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + + @Test + void error_whereAndThen_whereSuccess_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + final Result result = error.andThen(v -> () -> "success"); + //then + result.match( + x -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + + @Test + void error_whenAndThen_whereError_isOriginalError() { + //given + final RuntimeException exception1 = new RuntimeException(); + final Result error = anError(exception1); + //when + final Result result = error.andThen(v -> () -> { + throw new RuntimeException(); + }); + //then + result.match( + x -> fail("not a success"), + e -> assertThat(e).isSameAs(exception1)); + } + + @Test + void okayVoid_whenAndThen_whereSuccess_isUpdatedSuccess() { + //given + final ResultVoid ok = Result.ok(); + //when + final ResultVoid result = ok.andThen(() -> { + // do nothing + }); + //then + assertThat(result.isOkay()).isTrue(); + } + + @Test + void okayVoid_whenAndThen_whereError_isError() { + //given + final ResultVoid ok = Result.ok(); + final RuntimeException exception = new RuntimeException(); + //when + final ResultVoid result = ok.andThen(() -> { + throw exception; + }); + //then + result.match( + () -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + + @Test + void errorVoid_whereAndThen_whereSuccess_isError() { + //given + final RuntimeException exception = new RuntimeException(); + final ResultVoid error = Result.error(exception); + //when + final ResultVoid result = error.andThen(() -> { + // do nothing + }); + //then + result.match( + () -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + + @Test + void errorVoid_whenAndThen_whereError_isOriginalError() { + //given + final RuntimeException exception1 = new RuntimeException(); + final ResultVoid error = Result.error(exception1); + //when + final ResultVoid result = error.andThen(() -> { + throw new RuntimeException(); + }); + //then + result.match( + () -> fail("not a success"), + e -> assertThat(e).isSameAs(exception1)); + } } - @Test - void okay_whenMaybe_whereFails_isOkayNothing() { - //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") - ); + @Nested + @DisplayName("thenWith") + class ThenWithTests { + @Test + void okay_whenThenWith_whereOkay_isOriginalSuccess() { + //given + final Result ok = Result.ok(1); + //when + final Result result = ok.thenWith(v -> () -> { + // do something with v + }); + //then + assertThat(result).isSameAs(ok); + } + + @Test + void okay_whenThenWith_whereError_thenError() { + //given + final Result ok = Result.ok(1); + final RuntimeException exception = new RuntimeException(); + //when + final Result result = ok.thenWith(v -> () -> { + throw exception; + }); + //then + result.match( + x -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + + @Test + void error_whenThenWith_whereOkay_thenOriginalError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + final Result result = error.thenWith(v -> () -> { + // do something with v + }); + //then + assertThat(result).isSameAs(error); + } + + @Test + void error_whenThenWith_whenError_thenOriginalError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + final Result result = error.thenWith(v -> () -> { + throw new RuntimeException(); + }); + //then + assertThat(result).isSameAs(error); + } + } - @Test - void error_whenMaybe_wherePasses_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) - ); + @Nested + @DisplayName("thenWithV") + class ThenWithVTests { + @Test + void okay_whenThenWithV_whereOkay_isSuccessVoid() { + //given + final Result ok = Result.ok(1); + //when + final ResultVoid result = ok.thenWithV(v -> () -> { + // do something with v + }); + //then + assertThat(result.isOkay()).isTrue(); + } + + @Test + void okay_whenThenWithV_whereError_thenErrorVoid() { + //given + final Result ok = Result.ok(1); + final RuntimeException exception = new RuntimeException(); + //when + final ResultVoid result = ok.thenWithV(v -> () -> { + throw exception; + }); + //then + result.match( + () -> fail("not a success"), + e -> assertThat(e).isSameAs(exception)); + } + + @Test + void error_whenThenWithV_whereOkay_thenErrorVoid() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + final ResultVoid result = error.thenWithV(v -> () -> { + // do something with v + }); + //then + assertThat(result.isError()).isTrue(); + } + + @Test + void error_whenThenWithV_whenError_thenErrorVoid() { + //given + final RuntimeException exception = new RuntimeException(); + final Result error = anError(exception); + //when + final ResultVoid result = error.thenWithV(v -> () -> { + throw new RuntimeException(); + }); + //then + assertThat(result.isError()).isTrue(); + } + } - @Test - void error_whenMaybe_whereFails_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) - ); + @Nested + @DisplayName("flatMapMaybe") + class FlatMapMaybeTests { + @Test + void okayJust_whenFlatMapMaybe_whereOkayJust_thenIsOkayJust() { + //given + final Result> okJust = Result.ok(Maybe.just(1)); + //when + final Result> result = Result.flatMapMaybe(okJust, maybe -> Result.ok(maybe.flatMap(v -> Maybe.just("2")))); + //then + result.match( + success -> assertThat(success.toOptional()).contains("2"), + error -> fail("Not an error") + ); + } + + @Test + void okayJust_whenFlatMapMaybe_whereOkayNothing_thenIsOkayNothing() { + //given + final Result> okJust = Result.ok(Maybe.just(1)); + //when + final Result> result = Result.flatMapMaybe(okJust, maybe -> Result.ok(maybe.flatMap(v -> Maybe.nothing()))); + //then + result.match( + success -> assertThat(success.toOptional()).isEmpty(), + error -> fail("Not an error") + ); + } + + @Test + void okayJust_whenFlatMapMaybe_whereError_thenIsError() { + //given + final Result> okJust = Result.ok(Maybe.just(1)); + final RuntimeException exception = new RuntimeException(); + //when + final Result> result = Result.flatMapMaybe(okJust, v -> + Result.error(TypeReference.create(), exception)); + //then + result.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + void okayNothing_whenFlatMapMaybe_thenDoNotApply() { + //given + final Result> okNothing = Result.ok(Maybe.nothing()); + //when + final Result> result = Result.flatMapMaybe(okNothing, maybe -> Result.ok(maybe.flatMap(v -> Maybe.just("2")))); + //then + result.match( + success -> assertThat(success.toOptional()).isEmpty(), + error -> fail("Not an error") + ); + } + + @Test + void error_whenFlatMapMaybe_thenDoNotApply() { + //given + final RuntimeException exception = new RuntimeException(); + final Result> maybeResult = Result.error(TypeReference.create(), exception); + //when + final Result> result = Result.flatMapMaybe(maybeResult, maybe -> Result.ok(maybe.flatMap(v -> Maybe.just("2")))); + //then + result.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } } - @Test - void just_whenFromMaybe_isOkay() { - //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") - ); + @Nested + @DisplayName("reduce") + class ReduceTests { + @Test + void okayOkay_whenReduce_thenCombine() { + //given + final Result result1 = Result.ok(1); + final Result result10 = Result.ok(10); + //when + final Result result11 = result1.reduce(result10, (a, b) -> a + b); + //then + result11.match( + success -> assertThat(success).isEqualTo(11), + error -> fail("Not an error") + ); + } + + @Test + void okayError_whenReduce_thenError() { + //given + final Result result1 = Result.ok(1); + final RuntimeException exception = new RuntimeException(); + final Result result10 = anError(exception); + //when + final Result result11 = result1.reduce(result10, (a, b) -> a + b); + //then + result11.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + void errorOkay_whenReduce_thenError() { + //given + final RuntimeException exception = new RuntimeException(); + final Result result1 = anError(exception); + final Result result10 = Result.ok(10); + //when + final Result result11 = result1.reduce(result10, (a, b) -> a + b); + //then + result11.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test + void errorError_whenReduce_thenError() { + //given + final RuntimeException exception1 = new RuntimeException(); + final Result result1 = anError(exception1); + final RuntimeException exception10 = new RuntimeException(); + final Result result10 = anError(exception10); + //when + final Result result11 = result1.reduce(result10, (a, b) -> a + b); + //then + result11.match( + success -> fail("Not a success"), + error -> assertThat(error).isSameAs(exception1) + ); + } } - @Test - void nothing_whenFromMaybe_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) - ); + @Nested + @DisplayName("applyOver - Result over a set") + class ApplyOverTests { + + @Test + @DisplayName("Empty List is Okay") + void emptyListIsOkay() { + //given + var stream = Stream.empty(); + Consumer doNothingWithoutError = x -> {}; + //when + var result = Result.applyOver(stream, doNothingWithoutError); + //then + assertThat(result.isOkay()).isTrue(); + } + + @Test + @DisplayName("Single item list with valid result - is valid result") + void singleItemValidIsValid() { + //given + var stream = Stream.of("foo"); + //when + var result = Result.applyOver(stream, String::length, 0, Integer::sum); + //then + result.match( + success -> assertThat(success).isEqualTo(3), + error -> fail("not an error") + ); + } + + @Test + @DisplayName("Two item list consumed without error - is valid result") + void twoItemsConsumedIsValid() { + //given + var acc = new AtomicInteger(0); + var stream = Stream.of(1, 2); + Consumer accumulate = x -> acc.accumulateAndGet(x, Integer::sum); + //when + var result = Result.applyOver(stream, accumulate); + //then + result.match( + () -> assertThat(acc).hasValue(3), + e -> fail("should pass") + ); + } + + @Test + @DisplayName("Two item list with valid results - is valid result") + void twoItemsValidIsValid() { + //given + var stream = Stream.of("aaa", "bb"); + //when + var result = Result.applyOver(stream, String::length, 0, Integer::sum); + //then + result.match( + success -> assertThat(success).isEqualTo(5), + error -> fail("not an error") + ); + } + + @Test + @DisplayName("Single item list with error is error") + void singleItemErrorIsError() { + //given + var stream = Stream.of("error"); + var exception = new RuntimeException(); + //when + var result = Result.applyOver(stream, s -> { + throw exception; + }, 0, Integer::sum); + //then + result.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Nested @DisplayName("Two item list with two errors") + class TwoErrorsTests { + + Stream stream = Stream.of("ccc", "ddd"); + List processed = new ArrayList<>(); + Result result = Result.applyOver(stream, s -> { + processed.add(s); + throw new RuntimeException(s); + }, 0, Integer::sum); + + @Test @DisplayName("is first error") + void isFirstError() { + result.match( + success -> fail("not a success"), + error -> assertThat(error).hasMessage("ccc") + ); + } + @Test + @DisplayName("processing stopped after first error") + void stoppedOnFirstError() { + assertThat(processed).contains("ccc"); + } + + @Test @DisplayName("second item is not consumed") + void secondErrorNotConsumed() { + assertThat(processed).doesNotContain("ddd"); + } + } + + @Nested @DisplayName("Two item list with okay then error") + class TwoItemsOkayThenErrorTests { + + Stream stream = Stream.of("ccccc", "ddd"); + List processed = new ArrayList<>(); + Result result = Result.applyOver(stream, s -> { + processed.add(s); + if ("ddd".equals(s)) { + throw new RuntimeException(s); + } + return s.length(); + }, 0, Integer::sum); + + @Test @DisplayName("is error") + void isError() { + result.match( + success -> fail("not a success"), + error -> assertThat(error).hasMessage("ddd") + ); + } + + } } - @Test - void okay_whenToMaybe_isJust() { - //given - final Result ok = Result.ok(1); - //when - final Maybe maybe = Result.toMaybe(ok); - //then - assertThat(maybe.toOptional()).contains(1); + @Nested + @DisplayName("flatApplyOver") + class flatApplyOverTests { + + @Test + @DisplayName("Single item list with valid result - is valid result") + void singleItemValidIsValid() { + //given + var stream = Stream.of("foo"); + Function> f = s -> Result.ok(s.length()); + //when + var result = Result.flatApplyOver(stream, + f, 0, Integer::sum); + //then + result.match( + success -> assertThat(success).isEqualTo(3), + error -> fail("not an error") + ); + } + + @Test + @DisplayName("Two item list with valid results - is valid result") + void twoItemsValidIsValid() { + //given + var stream = Stream.of("aaa", "bb"); + Function> f = s -> Result.ok(s.length()); + //when + var result = Result.flatApplyOver(stream, f, 0, Integer::sum); + //then + result.match( + success -> assertThat(success).isEqualTo(5), + error -> fail("not an error") + ); + } + + @Test + @DisplayName("Single item list with error is error") + void singleItemErrorIsError() { + //given + var stream = Stream.of("error"); + var exception = new RuntimeException(); + Function> f = s -> anError(exception); + //when + var result = Result.flatApplyOver(stream, f, 0, Integer::sum); + //then + result.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Nested @DisplayName("Two item list with two errors") + class TwoErrorsTests { + + Stream stream = Stream.of("ccc", "ddd"); + List processed = new ArrayList<>(); + Function> f = s -> Result.of(() -> { + processed.add(s); + throw new RuntimeException(s); + }); + Result result = Result.flatApplyOver(stream, f, 0, Integer::sum); + + @Test + @DisplayName("error in result is the first error raised") + void isFirstError() { + result.match( + success -> fail("not a success"), + error -> assertThat(error).hasMessage("ccc") + ); + } + @Test + @DisplayName("Processing stopped after first error") + void stoppedOnFirstError() { + assertThat(processed).contains("ccc"); + } + + @Test @DisplayName("second item is not consumed") + void secondErrorNotConsumed() { + assertThat(processed).doesNotContain("ddd"); + } + } + + @Nested @DisplayName("Two item list with okay then error") + class TwoItemsOkayTheErrorTests { + + Stream stream = Stream.of("ccccc", "ddd"); + List processed = new ArrayList<>(); + Function> f = s -> Result.of(() -> { + processed.add(s); + if ("ddd".equals(s)) { + throw new RuntimeException(s); + } + return s.length(); + }); + Result result = Result.flatApplyOver(stream, f, 0, Integer::sum); + + @Test @DisplayName("is error") + void isError() { + result.match( + success -> fail("not a success"), + error -> assertThat(error).hasMessage("ddd") + ); + } + + } } - @Test - void error_whenToMaybe_isNothing() { - //given - final Result error = Result.error(new RuntimeException()); - //when - final Maybe maybe = Result.toMaybe(error); - //then - assertThat(maybe.toOptional()).isEmpty(); + @Nested + @DisplayName("toEither") + class ToEitherTests { + + @Test + @DisplayName("Success becomes Right") + void successIsRight() { + //given + Result result = Result.ok("success"); + //when + Either either = result.toEither(); + //then + either.match( + left -> fail("not a left"), + right -> assertThat(right).isEqualTo("success") + ); + } + + @Test + @DisplayName("Error becomes Left") + void errorIsLeft() { + //given + RuntimeException exception = new RuntimeException(); + Result result = anError(exception); + //when + Either either = result.toEither(); + //then + either.match( + left -> assumeThat(left).isSameAs(exception), + right -> fail("not a right") + ); + } } - @Test - void okay_whenOrElseThrow_isValue() throws Throwable { - //given - final Result ok = Result.ok(1); - //when - final Integer value = ok.orElseThrow(); - //then - assertThat(value).isEqualTo(1); + @Nested + @DisplayName("from(Either)") + class FromEitherTests { + + @Test @DisplayName("left is error") + void leftIsError() { + //given + RuntimeException exception = new RuntimeException(); + Either either = Either.left(exception); + //when + Result result = Result.from(either); + //then + result.match( + success -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + + @Test @DisplayName("right is success") + void rightIsSuccess() { + //given + Either either = Either.right("foo"); + //when + Result result = Result.from(either); + //then + result.match( + success -> assertThat(success).isEqualTo("foo"), + error -> fail("not an error") + ); + } + } - @Test - void error_whenOrElseThrow_throws() { - //given - final RuntimeException exception = new RuntimeException(); - final Result error = Result.error(exception); - //when - assertThatThrownBy(() -> error.orElseThrow()) - .isInstanceOf(CheckedErrorResultException.class) - .hasCause(exception); + @Nested + @DisplayName("ofVoid") + class OfVoidTests { + + @Test + @DisplayName("no error is Success") + void okayIsSuccess() { + //given + VoidCallable voidCallable = () -> { + //do nothing + }; + //when + ResultVoid result = Result.ofVoid(voidCallable); + //then + result.match( + () -> assertThat(true).isTrue(), + error -> fail("not an error") + ); + } + + @Test + @DisplayName("throws exception is an Error") + void exceptionIsError() { + //given + RuntimeException exception = new RuntimeException(); + VoidCallable voidCallable = () -> { + throw exception; + }; + //when + ResultVoid result = Result.ofVoid(voidCallable); + //then + result.match( + () -> fail("not a success"), + error -> assertThat(error).isSameAs(exception) + ); + } + } - @Test - void okay_whenOrElseThrowT_isValue() throws Exception { - //given - final Result ok = Result.ok(1); - //when - final Integer value = ok.orElseThrow(Exception.class); - //then - assumeThat(value).isEqualTo(1); - } + /** + * These include snippets from the Javadocs and are meant to prove that the examples are valid. + */ + @Nested + @DisplayName("javadoc documentation") + class JavadocTests { - @Test void errorT_whenOrElseThrowT_throwsT() { - //given - final RuntimeException exception = new RuntimeException(); - final Result error = Result.error(exception); - //then - assertThatThrownBy(() -> error.orElseThrow(RuntimeException.class)).isSameAs(exception); - } + @Nested + @DisplayName("Result") + class ResultJavadocTests { - @Test - void errorR_whenOrElseThrowT_throwsWrappedR() { - //given - final IOException exception = new IOException(); - final Result error = Result.error(exception); - //then - assertThatThrownBy(() -> error.orElseThrow(RuntimeException.class)) - .isInstanceOf(UnexpectedErrorResultException.class) - .hasCause(exception); - } + @Nested + @DisplayName("Static constructors") + class StaticConstructors { + @Test + @DisplayName("ok") + void ok() { + ResultVoid okay = Result.ok(); + // + assertThat(okay.isOkay()).isTrue(); + } - @Test - void okay_whenOrElseThrowUnchecked_isValue() { - //given - final Result ok = Result.ok(1); - //when - final Integer value = ok.orElseThrowUnchecked(); - //then - assumeThat(value).isEqualTo(1); - } + @Test + @DisplayName("ok(value)") + void okValue() { + Result okay = Result.ok(1); + // + okay.match( + s -> assertThat(s).isEqualTo(1), + e -> fail("not an err") + ); + } - @Test - void error_whenOrElseThrowUnchecked_throwsWrapped() { - //given - final IOException exception = new IOException(); - final Result error = Result.error(exception); - //then - assertThatThrownBy(() -> error.orElseThrowUnchecked()) - .isInstanceOf(ErrorResultException.class) - .hasCause(exception); - } + @Test + @DisplayName("of") + void of() { + Result okay = Result.of(() -> 1); + Result error = Result.of(() -> { + throw new RuntimeException(); + }); + // + assertSoftly(s -> { + okay.match( + v -> s.assertThat(v).isEqualTo(1), + e -> fail("not an err") + ); + error.match( + v -> fail("not a success"), + e -> s.assertThat(e).isInstanceOf(RuntimeException.class) + ); + }); + } - @Test - void justOkay_whenInvert_thenOkayJust() { - //given - final Maybe> justSuccess = Maybe.just(Result.ok(1)); - //when - final Result> result = Result.swap(justSuccess); - //then - result.match( - success -> assertThat(success.toOptional()).contains(1), - error -> fail("Not an error") - ); - } + @Test + @DisplayName("ofVoid") + void ofVoid() { + ResultVoid okay = Result.ofVoid(() -> System.out.println("Hello, World!")); + ResultVoid error = Result.ofVoid(() -> { + throw new Exception(); + }); + // + assertSoftly(s -> { + s.assertThat(okay.isOkay()).isTrue(); + s.assertThat(error.isError()).isTrue(); + }); + } - @Test - void JustError_whenInvert_isError() { - //given - final RuntimeException exception = new RuntimeException(); - final Maybe> justError = Maybe.just(Result.error(exception)); - //when - final Result> result = Result.swap(justError); - //then - result.match( - success -> fail("Not a success"), - error -> assertThat(error).isSameAs(exception) - ); - } + @Test + @DisplayName("error(Throwable)") + void errorThrowable() { + ResultVoid error = Result.error(new RuntimeException()); + // + assertThat(error.isError()).isTrue(); + } - @Test - void nothing_whenInvert_thenOkayNothing() { - //given - final Maybe> nothing = Maybe.nothing(); - //when - final Result> result = Result.swap(nothing); - //then - result.match( - success -> assertThat(success.toOptional()).isEmpty(), - error -> fail("Not an error") - ); - } + @Test + @DisplayName("error(Class, Throwable)") + void errorClassThrowable() { + Result error = Result.error(TypeReference.create(), new RuntimeException()); + // + assertThat(error.isError()).isTrue(); + } - @Test - void useCase_whenOkay_thenReturnSuccess() { - //given - final UseCase useCase = UseCase.isOkay(); - //when - final Result doubleResult = useCase.businessOperation("file a", "file bc"); - //then - doubleResult.match( - success -> assertThat(success).isEqualTo(7.5), - error -> fail("not an error") - ); - } + @Test + @DisplayName("from Either") + void fromEither() { + Either eitherRight = Either.right("Hello, World!"); + Either eitherLeft = Either.left(new RuntimeException()); + Result success = Result.from(eitherRight); + Result error = Result.from(eitherLeft); + // + assertSoftly(s -> { + success.match( + v -> s.assertThat(v).isEqualTo("Hello, World!"), + e -> fail("not an err") + ); + error.match( + v -> fail("not a success"), + e -> s.assertThat(e).isInstanceOf(RuntimeException.class) + ); + }); + } - @Test - void useCase_whenFirstReadIsError_thenReturnError() { - //given - final UseCase useCase = UseCase.firstReadInvalid(); - //when - final Result doubleResult = useCase.businessOperation("file def", "file ghij"); - //then - doubleResult.match( - success -> fail("not okay"), - error -> assertThat(error) - .isInstanceOf(RuntimeException.class) - .hasMessage("file def") - ); - } + @Test + @DisplayName("from Maybe") + void fromMaybe() { + Maybe maybe = Maybe.maybe(getValue()); + Result result = Result.from(maybe, () -> new RuntimeException()); + // + assertSoftly(s -> { + result.match( + v -> s.assertThat(v).isEqualTo(getValue()), + e -> fail("not an err") + ); + }); + } - @Test - void useCase_whenSecondReadIsError_thenReturnError() { - //given - final UseCase useCase = UseCase.secondReadInvalid(); - //when - final Result doubleResult = useCase.businessOperation("file klmno", "file pqrstu"); - //then - assertThat(doubleResult.isOkay()).isFalse(); - doubleResult.match( - success -> fail("not okay"), - error -> assertThat(error) - .isInstanceOf(RuntimeException.class) - .hasMessage("file klmno") - ); - } + } - @Test - void okay_toString() { - //given - final Result ok = Result.ok(1); - //when - final String toString = ok.toString(); - //then - assertThat(toString).contains("Result.Success{value=1}"); - } + @Nested + @DisplayName("Static methods") + class StaticMethodsTests { + @Test + @DisplayName("toMaybe") + void toMaybe() { + Result result = Result.of(() -> getValue()); + Maybe maybe = Result.toMaybe(result); + // + assertSoftly(s -> { + maybe.match( + j -> s.assertThat(j).isEqualTo(getValue()), + () -> fail("not a nothing") + ); + }); + } - @Test - void error_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}"); - } + @Test + @DisplayName("flatMayMaybe") + void flatMapMaybe() { + Result> result = Result.of(() -> Maybe.maybe(getValue())); + Result> maybeResult = Result.flatMapMaybe(result, + maybe -> Result.of(() -> maybe.map(v -> v * 2))); + // + assertSoftly(s -> { + maybeResult.match( + m -> m.match( + v -> s.assertThat(v).isEqualTo(2 * getValue()), + () -> fail("not a nothing")), + e -> fail("not an err") + ); + }); + } - @Test - void value_whenResultOf_isOkay() { - //given - final Callable c = () -> "okay"; - //when - final Result result = Result.of(c); - //then - result.match( - success -> assertThat(success).isEqualTo("okay"), - error -> fail("not an error") - ); - } + @Test + @DisplayName("flatApply(Stream, Consumer") + void flatApplyOverStreamConsumer() { + List processed = new ArrayList<>(); + Consumer consumer = s -> { + if ("dd".equals(s)) { + throw new RuntimeException("Invalid input: " + s); + } + processed.add(s); + }; - @Test - void exception_whenResultOf_isError() { - //given - final Callable c = () -> { - throw new IOException(); - }; - //when - final Result result = Result.of(c); - //then - result.match( - success -> fail("not a success"), - error -> assertThat(error).isInstanceOf(IOException.class) - ); - } + Stream okayStream = Stream.of("aa", "bb"); + ResultVoid resultOkay = Result.applyOver(okayStream, consumer); + resultOkay.match( + () -> System.out.println("All processed okay."), + error -> System.out.println("Error: " + error.getMessage()) + ); + System.out.println("Processed: " + processed); + // All processed okay. + // Processed: [aa, bb] - @Test - void okay_whenPeek_isConsumed() { - //given - final Result result = Result.ok(1); - final AtomicReference consumed = new AtomicReference<>(0); - //when - final Result peeked = result.peek(consumed::set); - //then - assertThat(consumed).hasValue(1); - assertThat(peeked).isSameAs(result); - } + processed.add("--"); + Stream errorStream = Stream.of("cc", "dd", "ee");// fails at 'dd' + ResultVoid resultError = Result.applyOver(errorStream, consumer); + resultError.match( + () -> System.out.println("All processed okay."), + error -> System.out.println("Error: " + error.getMessage()) + ); + System.out.println("Processed: " + processed); + // Error: Invalid input: dd + // Processed: [aa, bb, --, cc] - @Test - void error_whenPeek_isNotConsumed() { - //given - final Result result = Result.error(new RuntimeException()); - final AtomicReference consumed = new AtomicReference<>(0); - //when - final Result peeked = result.peek(consumed::set); - //then - assertThat(consumed).hasValue(0); - assertThat(peeked).isSameAs(result); - } + assertSoftly(s -> { + s.assertThat(processed).containsExactly( + "aa", "bb", "--", + "cc" + ); + resultError.match( + () -> s.fail("not a success"), + e -> s.assertThat(e).isInstanceOf(RuntimeException.class) + .hasMessage("Invalid input: dd") + ); + }); + } - @Test - void okay_whenOnError_isIgnored() { - //given - final Result ok = Result.ok(1); - //when - ok.onError(e -> fail("not an error")); - } + @Test + @DisplayName("applyOver(Stream, Function, BiFunction") + void applyOverStreamFunctionBiFunction() { + Function f = s -> { + if ("dd".equals(s)) { + throw new RuntimeException("Invalid input: " + s); + } + return s.length(); + }; - @Test - void error_whenOnError_isConsumed() { - //given - final RuntimeException exception = new RuntimeException(); - final Result error = Result.error(exception); - final AtomicReference capture = new AtomicReference<>(); - //when - error.onError(capture::set); - //then - assertThat(capture).hasValue(exception); - } + assertSoftly(s -> { - @Test - void okay_whenRecover_thenNoChange() { - //given - final Result ok = Result.ok(1); - //when - final Result recovered = ok.recover(e -> Result.ok(2)); - //then - assertThat(recovered).isSameAs(ok); - } + Stream okayStream = Stream.of("aa", "bb"); + Result resultOkay = Result.applyOver(okayStream, f, 0, Integer::sum); + resultOkay.match( + success -> s.assertThat(success).isEqualTo(4), + error -> s.fail("not an err") + ); + // Total length: 4 - @Test - void error_whenRecover_isSuccess() { - //given - final Result error = Result.error(new RuntimeException()); - //when - final Result recovered = error.recover(e -> Result.ok(2)); - //then - recovered.peek(v -> assertThat(v).isEqualTo(2)); - } + Stream errorStream = Stream.of("cc", "dd"); + Result resultError = Result.applyOver(errorStream, f, 0, Integer::sum); + resultError.match( + success -> s.fail("not a success"), // will not match + error -> s.assertThat(error.getMessage()).isEqualTo("Invalid input: dd") + ); + // Error: Invalid input: dd - @Test - void error_whenRecover_whereError_isUpdatedError() { - //given - final Result error = Result.error(new RuntimeException("original")); - //when - final Result recovered = error.recover(e -> Result.error(new RuntimeException("updated"))); - //then - recovered.onError(e -> assertThat(e).hasMessage("updated")); - } + }); + } - @Test - void okay_whenAndThen_whereSuccess_isUpdatedSuccess() { - //given - final Result ok = Result.ok(1); - //when - final Result result = ok.andThen(v -> () -> "success"); - //then - assertThat(result.isOkay()).isTrue(); - result.peek(v -> assertThat(v).isEqualTo("success")); - } + @Test + @DisplayName("flatApplyOver") + void flatApplyOver() { + Function> f = s -> { + if ("dd".equals(s)) { + return Result.error(TypeReference.create(), new RuntimeException("Invalid input: " + s)); + } + return Result.ok(s.length()); + }; - @Test - void okay_whenAndThen_whereError_isError() { - //given - final Result ok = Result.ok(1); - final RuntimeException exception = new RuntimeException(); - //when - final Result result = ok.andThen(v -> () -> { - throw exception; - }); - //then - assertThat(result.isError()).isTrue(); - result.onError(e -> assertThat(e).isSameAs(exception)); - } + assertSoftly(s -> { - @Test - void error_whereAndThen_whereSuccess_isError() { - //given - final RuntimeException exception = new RuntimeException(); - final Result error = Result.error(exception); - //when - final Result result = error.andThen(v -> () -> "success"); - //then - assertThat(result.isError()).isTrue(); - result.onError(e -> assertThat(e).isSameAs(exception)); - } + Stream okayStream = Stream.of("aa", "bb"); + Result resultOkay = Result.flatApplyOver(okayStream, f, 0, Integer::sum); + resultOkay.match( + success -> s.assertThat(success).isEqualTo(4), + error -> s.fail("not an err") + ); + // Total length: 4 - @Test - void error_whenAndThen_whereError_isOriginalError() { - //given - final RuntimeException exception1 = new RuntimeException(); - final Result error = Result.error(exception1); - //when - final Result result = error.andThen(v -> () -> { - throw new RuntimeException(); - }); - //then - assertThat(result.isError()).isTrue(); - result.onError(e -> assertThat(e).isSameAs(exception1)); - } + Stream errorStream = Stream.of("cc", "dd"); + Result resultError = Result.flatApplyOver(errorStream, f, 0, Integer::sum); + resultError.match( + success -> s.fail("not a success"), // will not match + error -> s.assertThat(error.getMessage()).isEqualTo("Invalid input: dd") + ); + // Error: Invalid input: dd + }); + } - @Test - void okay_whenThenWith_whereOkay_isOriginalSuccess() { - //given - final Result ok = Result.ok(1); - //when - final Result result = ok.thenWith(v -> () -> { - // do something with v - }); - //then - assertThat(result).isSameAs(ok); - } + @Test + @DisplayName("thenWith") + void thenWith() { + AtomicInteger capture = new AtomicInteger(); + Supplier doSomething = () -> 1; + Consumer doSomethingElse = capture::set; + // + Result r = Result.of(() -> doSomething.get()) + .thenWith(value -> () -> doSomethingElse.accept(value)); + // + assertThat(capture).hasValue(1); + } + } - @Test - void okay_whenThenWith_whereError_thenError() { - //given - final Result ok = Result.ok(1); - final RuntimeException exception = new RuntimeException(); - //when - final Result result = ok.thenWith(v -> () -> { - throw exception; - }); - //then - assertThat(result.isError()).isTrue(); - result.onError(e -> assertThat(e).isSameAs(exception)); - } + @Nested + @DisplayName("default methods") + class DefaultMethodTests { - @Test - void error_whenThenWith_whereOkay_thenOriginalError() { - //given - final RuntimeException exception = new RuntimeException(); - final Result error = Result.error(exception); - //when - final Result result = error.thenWith(v -> () -> { - // do something with v - }); - //then - assertThat(result).isSameAs(error); - } + @Test + @DisplayName("result") + void result() { + Result start = Result.ok(1); + Result okay = start.result(() -> 1); + Result error = start.result(() -> { + throw new RuntimeException(); + }); + // + assertSoftly(s -> { + okay.match( + v -> s.assertThat(v).isEqualTo(1), + e -> fail("not an err") + ); + error.match( + v -> fail("not a success"), + e -> s.assertThat(e).isInstanceOf(RuntimeException.class) + ); + }); + } - @Test - void error_whenThenWith_whenError_thenOriginalError() { - //given - final RuntimeException exception = new RuntimeException(); - final Result error = Result.error(exception); - //when - final Result result = error.thenWith(v -> () -> { - throw new RuntimeException(); - }); - //then - assertThat(result).isSameAs(error); - } + @Test + @DisplayName("toEither") + void toEither() { + Result success = Result.ok("success"); + RuntimeException exception = new RuntimeException(); + Result error = Result.error(TypeReference.create(), exception); - @Test - void okayJust_whenFlatMapMaybe_whereOkayJust_thenIsOkayJust() { - //given - final Result> okJust = Result.ok(Maybe.just(1)); - //when - final Result> result = Result.flatMapMaybe(okJust, maybe -> Result.ok(maybe.flatMap(v -> Maybe.just("2")))); - //then - result.match( - success -> assertThat(success.toOptional()).contains("2"), - error -> fail("Not an error") - ); - } + Either eitherRight = success.toEither(); + Either eitherLeft = error.toEither(); + // + assertSoftly(s -> { + eitherRight.match( + left -> s.fail("not a left"), + right -> s.assertThat(right).isEqualTo("success") + ); + eitherLeft.match( + left -> s.assertThat(left).isSameAs(exception), + right -> s.fail("not a right") + ); + }); + } - @Test - void okayJust_whenFlatMapMaybe_whereOkayNothing_thenIsOkayNothing() { - //given - final Result> okJust = Result.ok(Maybe.just(1)); - //when - final Result> result = Result.flatMapMaybe(okJust, maybe -> Result.ok(maybe.flatMap(v -> Maybe.nothing()))); - //then - result.match( - success -> assertThat(success.toOptional()).isEmpty(), - error -> fail("Not an error") - ); - } + } - @Test - void okayJust_whenFlatMapMaybe_whereError_thenIsError() { - //given - final Result> okJust = Result.ok(Maybe.just(1)); - final RuntimeException exception = new RuntimeException(); - //when - final Result> result = Result.flatMapMaybe(okJust, v -> Result.error(exception)); - //then - result.match( - success -> fail("Not a success"), - error -> assertThat(error).isSameAs(exception) - ); - } + @Nested + @DisplayName("instance methods") + class InstanceMethods { - @Test - void okayNothing_whenFlatMapMaybe_thenDoNotApply() { - //given - final Result> okNothing = Result.ok(Maybe.nothing()); - //when - final Result> result = Result.flatMapMaybe(okNothing, maybe -> Result.ok(maybe.flatMap(v -> Maybe.just("2")))); - //then - result.match( - success -> assertThat(success.toOptional()).isEmpty(), - error -> fail("Not an error") - ); - } + @Test + @DisplayName("orElse") + void orElse() { + assertThatExceptionOfType(CheckedErrorResultException.class) + .isThrownBy(() -> Result.of(() -> getErrorValue()).orElseThrow()) + .withCauseInstanceOf(RuntimeException.class); + } - @Test - void error_whenFlatMapMaybe_thenDoNotApply() { - //given - final RuntimeException exception = new RuntimeException(); - final Result> maybeResult = Result.error(exception); - //when - final Result> result = Result.flatMapMaybe(maybeResult, maybe -> Result.ok(maybe.flatMap(v -> Maybe.just("2")))); - //then - result.match( - success -> fail("Not a success"), - error -> assertThat(error).isSameAs(exception) - ); - } + @Test + @DisplayName("toVoid") + void toVoid() { + ResultVoid result = Result.of(() -> getResultValue()).toVoid(); + // + assertThat(result).isInstanceOf(SuccessVoid.class); + } - @Test - void okayOkay_whenReduce_thenCombine() { - //given - final Result result1 = Result.ok(1); - final Result result10 = Result.ok(10); - //when - final Result result11 = result1.reduce(result10, (a, b) -> a + b); - //then - result11.match( - success -> assertThat(success).isEqualTo(11), - error -> fail("Not an error") - ); - } + @Test + @DisplayName("map") + void map() { + Result result = Result.of(() -> getValue()) + .map(v -> String.valueOf(v)); + // + result.match( + success -> assertThat(success).isEqualTo("1"), + error -> fail("not en err") + ); + } - @Test - void okayError_whenReduce_thenError() { - //given - final Result result1 = Result.ok(1); - final RuntimeException exception = new RuntimeException(); - final Result result10 = Result.error(exception); - //when - final Result result11 = result1.reduce(result10, (a, b) -> a + b); - //then - result11.match( - success -> fail("Not a success"), - error -> assertThat(error).isSameAs(exception) - ); - } + @Test + @DisplayName("isOkay") + void isOkay() { + boolean isOkay = Result.of(() -> getValue()).isOkay(); + // + assertThat(isOkay).isTrue(); + } - @Test - void errorOkay_whenReduce_thenError() { - //given - final RuntimeException exception = new RuntimeException(); - final Result result1 = Result.error(exception); - final Result result10 = Result.ok(10); - //when - final Result result11 = result1.reduce(result10, (a, b) -> a + b); - //then - result11.match( - success -> fail("Not a success"), - error -> assertThat(error).isSameAs(exception) - ); - } + @Test + @DisplayName("isError") + void isError() { + boolean isError = Result.of(() -> getValue()).isError(); + // + assertThat(isError).isFalse(); + } - @Test - void errorError_whenReduce_thenError() { - //given - final RuntimeException exception1 = new RuntimeException(); - final Result result1 = Result.error(exception1); - final RuntimeException exception10 = new RuntimeException(); - final Result result10 = Result.error(exception10); - //when - final Result result11 = result1.reduce(result10, (a, b) -> a + b); - //then - result11.match( - success -> fail("Not a success"), - error -> assertThat(error).isSameAs(exception1) - ); + @Test + @DisplayName("onErrorClassConsumer") + void onErrorClassConsumer() { + AtomicReference err = new AtomicReference<>(); + Exception exception = new UnsupportedOperationException(); + // + Result.of(() -> { + throw exception; + }) + .onError(UnsupportedOperationException.class, + e -> err.set(e)); + // + assertThat(err.get()).isSameAs(exception); + } + + @Test + @DisplayName("onErrorConsumer") + void onErrorConsumer() { + AtomicReference err = new AtomicReference<>(); + Exception exception = new UnsupportedOperationException(); + // + Result.of(() -> { + throw exception; + }) + .onError(e -> err.set(e)); + // + assertThat(err).hasValue(exception); + } + + @Test + @DisplayName("orElseThrowUnchecked") + void orElseThrowUnchecked() { + Integer result = Result.of(() -> getValue()) + .orElseThrowUnchecked(); + // + assertThat(result).isEqualTo(1); + } + + @Test + @DisplayName("orElseThrowClass") + void orElseThrowClass() throws IOException { + Integer result = Result.of(() -> getValue()) + .orElseThrow(IOException.class); + // + assertThat(result).isEqualTo(1); + } + + @Test + @DisplayName("peek") + void peek() { + AtomicInteger capture = new AtomicInteger(); + // + Result result = Result.of(() -> getValue()) + .peek(v -> capture.set(v)); + // + assertThat(capture).hasValue(getValue()); + } + + @Test + @DisplayName("recover") + void recover() { + Result result = Result.of(() -> getErrorValue()) + .recover(e -> Result.of(() -> getSafeValue(e))); + // + result.match( + s -> assertThat(s).isEqualTo(2), + e -> fail("not an error") + ); + } + + @Test + @DisplayName("match") + void match() { + AtomicReference capture = new AtomicReference<>(); + // + Result.of(() -> getValue()) + .match( + success -> capture.set(success), + error -> capture.set(error) + ); + // + assertThat(capture).hasValue(getValue()); + } + + @Test + @DisplayName("flatMap") + void flatMap() { + Result result = + Result.of(() -> getValue()) + .flatMap(v -> Result.of(() -> String.valueOf(v))); + // + result.match( + s -> assertThat(s).isEqualTo("1"), + e -> fail("not an err") + ); + } + + @Test + @DisplayName("flatMapV") + void flatMapV() { + ResultVoid result = Result.of(() -> getValue()) + .flatMapV(v -> Result.ok()); + // + assertThat(result.isOkay()).isTrue(); + } + + @Test + @DisplayName("onSuccess") + void onSuccess() { + AtomicInteger capture = new AtomicInteger(); + // + Result.of(() -> getValue()) + .onSuccess(v -> capture.set(v)); + // + assertThat(capture).hasValue(getValue()); + } + + } + + private Result getResultValue() { + return Result.ok(getValue()); + } + + private Integer getValue() { + return 1; + } + + private Integer getSafeValue(Throwable e) { + return 2; + } + + private Integer getErrorValue() { + throw new RuntimeException(); + } + + } + + @Nested + @DisplayName("ResultVoid") + class ResultVoidJavadocTests { + + @Test + @DisplayName("match") + void match() { + AtomicBoolean success = new AtomicBoolean(); + AtomicBoolean error = new AtomicBoolean(); + // + Result.ofVoid(() -> doSomethingSafe()) // ResultVoid + .match( + () -> success.set(true), // has no value + e -> error.set(true) + ); + // + assertSoftly(s -> { + s.assertThat(success).isTrue(); + s.assertThat(error).isFalse(); + }); + } + + @Test + @DisplayName("recover") + void recover() { + ResultVoid result = Result.ofVoid(() -> doSomethingRisky("first")) + .recover(e -> Result.ofVoid(() -> doSomethingRisky("second"))); + // + result.match( + () -> fail("not a success"), + e -> assertThat(e).isInstanceOf(RuntimeException.class) + .extracting("message").isEqualTo("second") + ); + } + + @Test + @DisplayName("onSuccess") + void onSuccess() { + AtomicBoolean capture = new AtomicBoolean(); + // + Result.ofVoid(() -> doSomethingRisky()) + .onSuccess(() -> capture.set(true)); + // + assertThat(capture).isFalse(); + } + + @Test + @DisplayName("onErrorClassConsumer") + void onErrorClassConsumer() { + AtomicReference err = new AtomicReference<>(); + Exception exception = new UnsupportedOperationException(); + // + Result.ofVoid(() -> { + throw exception; + }) + .onError(UnsupportedOperationException.class, + e -> err.set(e)); + // + assertThat(err.get()).isSameAs(exception); + } + + @Test + @DisplayName("andThen") + void andThen() { + AtomicBoolean capture = new AtomicBoolean(); + // + Result.ofVoid(() -> doSomethingRisky()) + .andThen(() -> capture.set(true)); + // + assertThat(capture).isFalse(); + } + + @Test + @DisplayName("inject") + void inject() { + Result result = Result.ofVoid(() -> doSomethingRisky()) + .inject(() -> 1); + // + result.match( + s -> assertThat(s).isEqualTo(1), + e -> fail("not an err") + ); + } + + private void doSomethingSafe() { + + } + + private void doSomethingRisky() throws RuntimeException { + throw new RuntimeException(); + } + + private void doSomethingRisky(String message) throws RuntimeException { + throw new RuntimeException(message); + } + + } } @RequiredArgsConstructor @@ -831,7 +2303,7 @@ class ResultTest implements WithAssertions { if (okay) { return Result.ok(fileName.length()); } - return Result.error(new RuntimeException(fileName)); + return Result.error(TypeReference.create(), new RuntimeException(fileName)); } private Result adjustValue(final Integer value) { @@ -843,4 +2315,4 @@ class ResultTest implements WithAssertions { } } -} \ No newline at end of file +}