All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.diversionmc.error.Result Maven / Gradle / Ivy

There is a newer version: 1.29.2
Show newest version
package net.diversionmc.error;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;

import static java.util.Collections.reverse;
import static net.diversionmc.error.Functionals.*;
import static net.diversionmc.error.Success.tryRun;

/**
 * Result immutable type: Success or Error.
 *
 * @param  Success type.
 * @param  Error type.
 * @see Success
 * @see Optional
 */
public final class Result {
    //
    // Factory
    //

    /**
     * Create a success result.
     *
     * @param s   Success value.
     * @param  Success type.
     * @param  Error type.
     * @return Success result. Use {@link Optional} if you do not want error type.
     */
    public static  Result ok(S s) {
        if (s == null) throw new IllegalArgumentException("Supplied null to result");
        return new Result<>(s, null);
    }

    /**
     * Create an error result.
     *
     * @param e   Error value.
     * @param  Success type.
     * @param  Error type.
     * @return Error result. Use {@link Success} if you do not want success type.
     */
    public static  Result error(E e) {
        if (e == null) throw new IllegalArgumentException("Supplied null to result");
        return new Result<>(null, e);
    }

    /**
     * Convert a boolean into Result.
     *
     * @param test  Boolean to convert.
     * @param ok    Object supplied if true.
     * @param error Object supplied if false.
     * @param    Success type.
     * @param    Error type.
     * @return Ok if true, else error.
     */
    public static  Result check(boolean test, Supplier ok, Supplier error) {
        return test ? ok(ok.get()) : error(error.get());
    }

    /**
     * Convert a boolean into Result.
     *
     * @param test    Boolean to convert.
     * @param ifTrue  Result supplied if true.
     * @param ifFalse Result supplied if false.
     * @param      Success type.
     * @param      Error type.
     * @return Obvious.
     */
    public static  Result flatCheck(boolean test, Supplier> ifTrue, Supplier> ifFalse) {
        return (test ? ifTrue : ifFalse).get();
    }

    /**
     * Convert an optional into Result.
     *
     * @param ok    Optional to check and turn into success result.
     * @param error Object supplied if optional is empty.
     * @param    Success type.
     * @param    Error type.
     * @return Ok with the optional's value if present, else error.
     */
    public static  Result from(Optional ok, Supplier error) {
        return ok
            .map(Result::ok)
            .orElseGet(applyFS(Result::error, error));
    }

    /**
     * Convert an optional into Result.
     *
     * @param check Optional to check and turn into success result.
     * @param other Object supplied if optional is empty.
     * @param    Success type.
     * @param    Error type.
     * @return Ok with the optional's value if present, else other.
     */
    public static  Result flatFrom(Optional check, Supplier> other) {
        return check
            .map(Result::ok)
            .orElseGet(other);
    }

    //
    // Boilerplate
    //

    private final S ok;
    private final E error;

    private Result(S ok, E error) {
        this.ok = ok;
        this.error = error;
    }

    /**
     * Get success value.
     *
     * @return Success value if result is successful or empty.
     */
    public Optional ok() {
        return optional(ok);
    }

    /**
     * Get success value.
     *
     * @return Success value if result is successful or empty.
     */
    public Stream okStream() {
        return stream(ok);
    }

    /**
     * Get error value.
     *
     * @return Error value if result is unsuccessful or empty.
     */
    public Optional error() {
        return optional(error);
    }

    /**
     * Get error value.
     *
     * @return Error value if result is unsuccessful or empty.
     */
    public Stream errorStream() {
        return stream(error);
    }

    /**
     * Check if result is successful.
     *
     * @return True if success result.
     */
    public boolean isOk() {
        return ok != null;
    }

    /**
     * Check if result is unsuccessful.
     *
     * @return True if error result.
     */
    public boolean isError() {
        return error != null;
    }

    /**
     * Convert result to success (throw out success value).
     *
     * @return Success object.
     */
    public Success success() {
        return isOk()
            ? Success.ok()
            : Success.error(error);
    }

    /**
     * Debug representation of the result.
     *
     * @return Ok or error, converted to string.
     */
    public String toString() {
        return (isOk() ? ok : error) + "";
    }

    /**
     * Compare to either {@link Result}, {@link Success} or {@link Optional}.
     *
     * @param obj Object to compare to.
     * @return True if underlying objects under those 3 types match this result (ok or error) or are missing.
     */
    public boolean equals(Object obj) {
        return (obj instanceof Result r2 && isOk() == r2.isOk() && (isOk() ? ok.equals(r2.ok) : error.equals(r2.error)))
            || (obj instanceof Success s2 && isOk() == s2.isOk() && (isOk() || error.equals(s2.error().get())))
            || (obj instanceof Optional o2 && isOk() == o2.isPresent() && (!isOk() || ok.equals(o2.get())));
    }

    /**
     * This hashCode method refers to either hashCode of ok or error.
     *
     * @return Ok hashCode if this result is successful, error hashCode otherwise.
     */
    public int hashCode() {
        return (isOk() ? ok : error).hashCode();
    }

    //
    // Function
    //

    /**
     * Convert a result into another result.
     *
     * @param mapper Converter.
     * @param    New success type.
     * @param    New error type.
     * @return New result.
     */
    public  Result map(Function, Result> mapper) {
        return mapper.apply(this);
    }

    /**
     * Convert the success value.
     *
     * @param sMapper Success converter.
     * @param eMapper Error converter.
     * @param     New success type.
     * @param     New error type.
     * @return New result.
     */
    public  Result map(Function sMapper, Function eMapper) {
        return isOk()
            ? ok(sMapper.apply(ok))
            : error(eMapper.apply(error));
    }

    /**
     * Convert the success value.
     *
     * @param mapper Converter.
     * @param    New success type.
     * @return New result.
     */
    public  Result mapOk(Function mapper) {
        return isOk()
            ? ok(mapper.apply(ok))
            : error(error);
    }

    /**
     * Convert the error value.
     *
     * @param mapper Converter.
     * @param    New error type.
     * @return New result.
     */
    public  Result mapError(Function mapper) {
        return isOk()
            ? ok(ok)
            : error(mapper.apply(error));
    }

    /**
     * Convert the success value to a new result.
     *
     * @param mapper Converter.
     * @param    New success type.
     * @return New result.
     */
    public  Result flatMapResult(Function> mapper) {
        return isOk()
            ? mapper.apply(ok)
            : error(error);
    }

    /**
     * Convert the success value to a new success.
     *
     * @param mapper Converter.
     * @return New success.
     */
    public Success flatMapSuccess(Function> mapper) {
        return isOk()
            ? mapper.apply(ok)
            : Success.error(error);
    }

    /**
     * Peek at the result without changing it.
     *
     * @param peeker Peeker function.
     */
    public Result peek(Consumer> peeker) {
        peeker.accept(this);
        return this;
    }

    /**
     * Peek at the success value without changing it, if it is present.
     *
     * @param peeker Peeker function.
     */
    public Result peekOk(Consumer peeker) {
        if (isOk()) peeker.accept(ok);
        return this;
    }

    /**
     * Peek at the error value without changing it, if it is present.
     *
     * @param peeker Peeker function.
     */
    public Result peekError(Consumer peeker) {
        if (isError()) peeker.accept(error);
        return this;
    }

    /**
     * Filter the ok value, do an error if not passed.
     *
     * @param filter Filter function.
     * @param error  Error supplier in case ok does not pass filter.
     */
    public Result filter(Predicate filter, Supplier error) {
        // don't do anything if error or filter passed
        // error if ok but filter not passed
        return isError()
            || filter.test(ok)
            ? this
            : error(error.get());
    }

    /**
     * Try to receive value from another result.
     *
     * @param other Other result to use if this result is unsuccessful.
     * @return Either this or other, but if other result fails, this error is used, ignoring other's error.
     */
    public Result or(Result other) {
        return isOk()
            ? this
            : other
            .mapError(ignoreS(applyS(error)));
    }

    /**
     * Try to receive value from an optional.
     *
     * @param other Optional to use if this result is unsuccessful.
     * @return Either this or other, but if other result is empty, this error is used, ignoring other's error.
     */
    public Result or(Optional other) {
        return isOk()
            ? this
            : from(other, applyS(error));
    }

    /**
     * Try to receive value from another result.
     *
     * @param other Other result to use if this result is unsuccessful.
     * @return Either this or other, but if other result fails, this error is used, ignoring other's error.
     */
    public Result flatOr(Supplier> other) {
        return isOk()
            ? this
            : other.get()
            .mapError(ignoreS(applyS(error)));
    }

    //
    // Static Utils
    //

    /**
     * Catch an exception.
     *
     * @param ce  Exception type to catch
     * @param s   TryS to catch
     * @param  Success type
     * @param  Error type
     * @return Ok if successful, error if caught an exception
     */
    @SuppressWarnings("unchecked")
    public static  Result tryGet(Class ce, Supplier s) {
        try {
            return ok(s.get());
        } catch (ResultException e) {
            var ex = e.exception();
            if (ce.isInstance(ex))
                return error((E) ex);
            throw e;
        }
    }

    private static List tryCloseAll(List resources) {
        reverse(resources); // reversing a stream does not make sense, so reverse a list
        return resources.stream()
            .map(i -> tryRun(
                Exception.class,
                (TryR) i::close))
            .flatMap(Success::errorStream)
            .toList(); // this lets us close all resources ignoring errors until collect
    }

    @SuppressWarnings("unchecked")
    private static  Result errorCatchRethrow(Class ce, ResultException e) {
        if (ce.isInstance(e.exception()))
            return error((E) e);
        throw e;
    }

    /**
     * Catch an exception.
     *
     * @param ce  Exception type to catch
     * @param f   TryWithResources block to catch
     * @param  Success type
     * @param  Error type
     * @return Ok if successful, error if caught an exception
     */
    public static  Result tryGet(Class ce, TryF f) {
        var resources = new ArrayList();
        try {
            var res = f.apply(new TryWithResources(resources)); // this may throw...
            var closed = tryCloseAll(resources);

            if (closed.isEmpty()) return ok(res);
            var firstCloseFailure = closed.get(0);
            closed.stream()
                .skip(1)
                .forEach(firstCloseFailure::addSuppressed);
            return errorCatchRethrow(ce, new ResultException(firstCloseFailure));
        } catch (ResultException e) {
            var closed = tryCloseAll(resources); // ...so we need to close all resources before catchRethrow
            var caught = Result.errorCatchRethrow(ce, e);
            closed.forEach(caught.error::addSuppressed);
            return caught;
        }
    }

    /**
     * Catch an exception.
     *
     * @param ce  Exception type to catch
     * @param f   TryWithResources block to catch
     * @param  Success type
     * @param  Error type
     * @return Ok if successful, error if caught an exception
     */
    public static  Result flatTryGet(Class ce, TryF, E> f) {
        var resources = new ArrayList();
        try {
            var res = f.apply(new TryWithResources(resources)); // this may throw...
            var closed = tryCloseAll(resources);

            if (closed.isEmpty()) return res; // this res may be not ok
            var firstCloseFailure = closed.get(0);
            closed.stream()
                .skip(1)
                .forEach(firstCloseFailure::addSuppressed);
            return errorCatchRethrow(ce, new ResultException(firstCloseFailure));
        } catch (ResultException e) {
            var closed = tryCloseAll(resources); // ...so we need to close all resources before catchRethrow
            var caught = Result.errorCatchRethrow(ce, e);
            closed.forEach(caught.error::addSuppressed);
            return caught;
        }
    }

    /**
     * Collect all successful type to a list, if possible.
     *
     * @param  Success type
     * @param  Error type
     * @return Result of collected list or first error
     */
    public static  Collector, ?, Result, E>> toResultList() {
        return Collector.of(
            mapS(mapS(ArrayList::new, Result::, E>ok), AtomicReference::new),
            (results, result) -> results.set(result
                .flatMapResult(r -> results.get()
                    .peekOk(list -> list.add(r)))),
            (results, result) -> {
                results.set(result.get()
                    .flatMapResult(r -> results.get()
                        .peekOk(list -> list.addAll(r))));
                return results;
            },
            AtomicReference::get);
    }

    @SuppressWarnings("NewMethodNamingConvention")
    public  S Try(Function orReturn) {
        if (isError()) tryPropagationHelper(orReturn);
        return ok().get();
    }

    @SuppressWarnings("NewMethodNamingConvention")
    public  S Try(Supplier orReturn) {
        if (isError()) tryPropagationHelper(orReturn);
        return ok().get();
    }

    @SuppressWarnings("NewMethodNamingConvention")
    public  S Try(T orReturn) {
        if (isError()) tryPropagationHelper(orReturn);
        return ok().get();
    }

    @SuppressWarnings("NewMethodNamingConvention")
    public S Try() {
        return ok().get();
    }

    public S get() {
        if (isError()) throw new NoSuchElementException("Cannot unwrap error");
        return ok;
    }

    public  T tryPropagationHelper(Function orReturn) {
        return orReturn.apply(error);
    }

    public  T tryPropagationHelper(Supplier orReturn) {
        return orReturn.get();
    }

    public  T tryPropagationHelper(T orReturn) {
        return orReturn;
    }
}