Result.java
package com.guinetik.rr.result;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* A container object representing either a successful result or an error.
*
* <p>Inspired by Rust's {@code Result<T, E>} and Scala's {@code Either[L, R]}, this class
* provides a functional approach to error handling without exceptions. It forces explicit
* handling of both success and failure cases, leading to more robust code.
*
* <h2>Creating Results</h2>
* <pre class="language-java"><code>
* // Success case
* Result<User, ApiError> success = Result.success(new User("John"));
*
* // Failure case
* Result<User, ApiError> failure = Result.failure(new ApiError(404, "Not found"));
* </code></pre>
*
* <h2>Checking and Extracting Values</h2>
* <pre class="language-java"><code>
* Result<User, ApiError> result = client.fluent().get("/users/1", User.class);
*
* if (result.isSuccess()) {
* User user = result.getValue();
* System.out.println("Found: " + user.getName());
* } else {
* ApiError error = result.getError();
* System.err.println("Error: " + error.getMessage());
* }
* </code></pre>
*
* <h2>Pattern Matching with match()</h2>
* <pre class="language-java"><code>
* result.match(
* user -> System.out.println("Success: " + user.getName()),
* error -> System.err.println("Failed: " + error.getMessage())
* );
* </code></pre>
*
* <h2>Functional Transformations</h2>
* <pre class="language-java"><code>
* // Transform the success value
* Result<String, ApiError> nameResult = result.map(user -> user.getName());
*
* // Transform the error
* Result<User, String> stringError = result.mapError(err -> err.getMessage());
*
* // Chain operations
* String name = result
* .map(User::getName)
* .map(String::toUpperCase)
* .getOrElse("Unknown");
* </code></pre>
*
* <h2>Safe Value Extraction</h2>
* <pre class="language-java"><code>
* // With default value
* User user = result.getOrElse(User.anonymous());
*
* // With lazy default
* User user = result.getOrElseGet(() -> loadDefaultUser());
*
* // Throw on failure
* User user = result.getOrElseThrow(() -> new UserNotFoundException());
*
* // Unwrap (throws RuntimeException on failure)
* User user = result.unwrap();
*
* // Convert to Optional
* Optional<User> optional = result.toOptional();
* </code></pre>
*
* @param <T> the type of the success value
* @param <E> the type of the error value
* @author guinetik <guinetik@gmail.com>
* @see com.guinetik.rr.RocketRest.FluentApi
* @see ApiError
* @since 1.0.0
*/
public class Result<T, E> {
private final T value;
private final E error;
private final boolean isSuccess;
private Result(T value, E error, boolean isSuccess) {
this.value = value;
this.error = error;
this.isSuccess = isSuccess;
}
/**
* Creates a successful Result containing the given value.
*
* @param value the value
* @param <T> the type of the value
* @param <E> the type of the error
* @return a successful Result containing the value
*/
public static <T, E> Result<T, E> success(T value) {
return new Result<>(value, null, true);
}
/**
* Creates a failed Result containing the given error.
*
* @param error the error
* @param <T> the type of the value
* @param <E> the type of the error
* @return a failed Result containing the error
*/
public static <T, E> Result<T, E> failure(E error) {
return new Result<>(null, error, false);
}
/**
* Returns whether this Result is a success.
*
* @return true if this Result is a success, false otherwise
*/
public boolean isSuccess() {
return isSuccess;
}
/**
* Returns whether this Result is a failure.
*
* @return true if this Result is a failure, false otherwise
*/
public boolean isFailure() {
return !isSuccess;
}
/**
* Gets the value contained in this Result.
*
* @return the value
* @throws NoSuchElementException if this Result is a failure
*/
public T getValue() {
if (!isSuccess) {
throw new NoSuchElementException("Cannot get value from a failure Result");
}
return value;
}
/**
* Gets the error contained in this Result.
*
* @return the error
* @throws NoSuchElementException if this Result is a success
*/
public E getError() {
if (isSuccess) {
throw new NoSuchElementException("Cannot get error from a success Result");
}
return error;
}
/**
* Gets the value contained in this Result or the given default value if this Result is a failure.
*
* @param defaultValue the value to return if this Result is a failure
* @return the value if this Result is a success, otherwise the default value
*/
public T getOrElse(T defaultValue) {
return isSuccess ? value : defaultValue;
}
/**
* Gets the value contained in this Result or the value supplied by the given Supplier if this Result is a failure.
*
* @param supplier the Supplier to provide the default value
* @return the value if this Result is a success, otherwise the value from the supplier
*/
public T getOrElseGet(Supplier<? extends T> supplier) {
return isSuccess ? value : supplier.get();
}
/**
* Gets the value contained in this Result or throws the given exception if this Result is a failure.
*
* @param exceptionSupplier the Supplier to provide the exception to throw
* @param <X> the type of the exception to throw
* @return the value if this Result is a success
* @throws X if this Result is a failure
*/
public <X extends Throwable> T getOrElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (isSuccess) {
return value;
}
throw exceptionSupplier.get();
}
/**
* Returns the value if this Result is a success, or unwraps the error by throwing an exception.
* This is similar to Rust's unwrap() method.
*
* @return the contained value
* @throws RuntimeException if this is a failure, with the error toString() as the message
*/
public T unwrap() {
if (isSuccess) {
return value;
}
throw new RuntimeException("Unwrapped a failure Result: " + error);
}
/**
* Maps the value of this Result if it's a success, using the given mapping function.
*
* @param mapper the function to apply to the value
* @param <U> the type of the result of the mapping function
* @return a new Result with the mapped value if this Result is a success, otherwise a new Result with the same error
*/
public <U> Result<U, E> map(Function<? super T, ? extends U> mapper) {
if (isSuccess) {
return Result.success(mapper.apply(value));
}
return Result.failure(error);
}
/**
* Maps the error of this Result if it's a failure, using the given mapping function.
*
* @param mapper the function to apply to the error
* @param <F> the type of the result of the mapping function
* @return a new Result with the mapped error if this Result is a failure, otherwise a new Result with the same value
*/
public <F> Result<T, F> mapError(Function<? super E, ? extends F> mapper) {
if (isSuccess) {
return Result.success(value);
}
return Result.failure(mapper.apply(error));
}
/**
* Executes the given consumer if this Result is a success.
*
* @param consumer the consumer to execute
* @return this Result
*/
public Result<T, E> ifSuccess(Consumer<? super T> consumer) {
if (isSuccess) {
consumer.accept(value);
}
return this;
}
/**
* Executes the given consumer if this Result is a failure.
*
* @param consumer the consumer to execute
* @return this Result
*/
public Result<T, E> ifFailure(Consumer<? super E> consumer) {
if (!isSuccess) {
consumer.accept(error);
}
return this;
}
/**
* Converts this Result to an Optional containing the value if this Result is a success,
* or an empty Optional if this Result is a failure.
*
* @return an Optional containing the value if this Result is a success, otherwise an empty Optional
*/
public Optional<T> toOptional() {
return isSuccess ? Optional.ofNullable(value) : Optional.empty();
}
/**
* Pattern-matches over this Result, executing one of the consumers depending on success/failure.
* <pre>
* result.match(value -> System.out.println(value), err -> log.error(err));
* </pre>
*
* @param successConsumer runs if this result is a success (receives the value)
* @param errorConsumer runs if this result is a failure (receives the error)
* @return this Result for fluent chaining
*/
public Result<T, E> match(Consumer<? super T> successConsumer, Consumer<? super E> errorConsumer) {
if (isSuccess) {
successConsumer.accept(value);
} else {
errorConsumer.accept(error);
}
return this;
}
@Override
public String toString() {
if (isSuccess) {
return "Success[" + value + "]";
}
return "Failure[" + error + "]";
}
}