001package com.identityworksllc.iiq.common;
002
003import com.identityworksllc.iiq.common.annotation.Experimental;
004
005import java.util.NoSuchElementException;
006import java.util.function.Consumer;
007import java.util.function.Function;
008import java.util.function.Predicate;
009
010/**
011 * A functional "monad" that contains either a non-null value of the given type
012 * or an exception, but never both. The idea here is:
013 *
014 * ```
015 * objectList
016 *  .stream()
017 *  .map(obj -> Maybe.wrap(String.class, some::functionThatCanFail))
018 *  .filter(Maybe.fnHasValue())
019 *  .forEach(items);
020 * ```
021 *
022 * @param <T> The type that this object might contain
023 */
024@Experimental
025public final class Maybe<T> {
026    /**
027     * A consumer extension that handles the Maybe concept. If the Maybe has a
028     * value, it will be passed to the wrapped Consumer, and if it does not,
029     * no action will be taken.
030     * @param <T> The type contained within the Maybe, maybe.
031     */
032    public static final class MaybeConsumer<T> implements Consumer<Maybe<T>> {
033        public static <T> MaybeConsumer<T> from(Consumer<T> wrappedConsumer) {
034            return new MaybeConsumer<>(wrappedConsumer);
035        }
036        private final Consumer<T> wrapped;
037
038        private MaybeConsumer(Consumer<T> wrappedConsumer) {
039            if (wrappedConsumer == null) {
040                throw new IllegalArgumentException("Consumer passed to MaybeConsumer must not be null");
041            }
042            this.wrapped = wrappedConsumer;
043        }
044
045        /**
046         * Performs this operation on the given argument.
047         *
048         * @param tMaybe the input argument
049         */
050        @Override
051        public void accept(Maybe<T> tMaybe) {
052            if (tMaybe.hasValue()) {
053                wrapped.accept(tMaybe.get());
054            }
055        }
056    }
057
058    /**
059     * Creates a Predicate that returns true if the Maybe object has an error
060     * @param <F> The arbitrary input type
061     * @return The predicate
062     */
063    public static <F> Predicate<Maybe<F>> fnHasError() {
064        return Maybe::hasError;
065    }
066
067    /**
068     * Creates a Predicate that returns true if the Maybe object has a value
069     * @param <F> The arbitrary input type
070     * @return The predicate
071     */
072    public static <F> Predicate<Maybe<F>> fnHasValue() {
073        return Maybe::hasValue;
074    }
075
076    /**
077     * Returns a Maybe object containing the given value
078     * @param value The value
079     * @param <A> The type of the value
080     * @return A Maybe object containing the given value
081     */
082    public static <A> Maybe<A> of(A value) {
083        return new Maybe<>(value, null);
084    }
085
086    /**
087     * Returns a Maybe that is either a value or an error, depending on the outcome of
088     * the supplier in question.
089     *
090     * @param aClass The class expected, used solely to distinguish this method from the other of() implementations
091     * @param supplier The supplier of a value to wrap in a Maybe
092     * @return A Maybe repesenting the outcome of the Supplier's action
093     * @param <A> The content type of the Maybe, if it has a value
094     */
095    public static <A> Maybe<A> of(Class<A> aClass, Functions.SupplierWithError<A> supplier) {
096        try {
097            return Maybe.of(supplier.getWithError());
098        } catch(Throwable t) {
099            return Maybe.of(t);
100        }
101    }
102
103    /**
104     * Returns a Maybe object containing an exception. The class parameter is only used for casting the output type.
105     * @param value The exception
106     * @param otherwiseExpectedType The parameterized type that we would be expecting if this was not an exception
107     * @param <A> The parameterized type that we would be expecting if this was not an exception
108     * @return a Maybe object containing the exception
109     */
110    public static <A> Maybe<A> of(Throwable value, Class<A> otherwiseExpectedType) {
111        return new Maybe<>(null, value);
112    }
113
114    /**
115     * Chains a Maybe object by passing along the Throwable from an existing Maybe into the next. This is for use with streams.
116     * @param value An existing Maybe in error state
117     * @param otherwiseExpectedType The parameterized type that we would be expecting if this was not an exception
118     * @param <A> The parameterized type that we would be expecting if this was not an exception
119     * @return a Maybe object containing the exception
120     */
121    public static <A> Maybe<A> of(Maybe<?> value, Class<A> otherwiseExpectedType) {
122        if (!value.hasError()) {
123            throw new IllegalArgumentException("The chained 'Maybe' implementation can only be used for errors");
124        }
125        return new Maybe<>(null, value.contents.getRight());
126    }
127
128    /**
129     * Chains a Maybe object by passing along the Throwable from an existing Maybe into the next. This is for use with streams.
130     * @param value An existing Maybe in error state
131     * @param <A> The parameterized type that we would be expecting if this was not an exception
132     * @return a Maybe object containing the exception
133     */
134    public static <A> Maybe<A> of(Maybe<?> value) {
135        if (!value.hasError()) {
136            throw new IllegalArgumentException("The chained 'Maybe' implementation can only be used for errors");
137        }
138        return new Maybe<>(null, value.contents.getRight());
139    }
140
141    /**
142     * Creates a new maybe from the given throwable with an inferred type by context
143     * @param e The throwable
144     * @param <R> The inferred type
145     * @return A chained Maybe
146     */
147    @SuppressWarnings("unchecked")
148    public static <R> Maybe<R> of(Throwable e) {
149        return (Maybe<R>)new Maybe<>(null, e);
150    }
151
152    /**
153     * Returns a function wrapping the input function. The wrapper will resolve the
154     * output of the input function by invoking {@link com.identityworksllc.iiq.common.Functions.FunctionWithError#applyWithError(I)}
155     * and wrapping the output (whether a value or an exception) in a Maybe.
156     *
157     * @param aClass The output class expected
158     * @param func The function that will be wrapped in a Maybe producer
159     * @return A Maybe repesenting the outcome of the Supplier's action
160     * @param <I> The input type to the function
161     * @param <O> The content type of the Maybe, assuming it was to have a value
162     */
163    public static <I, O> Function<I, Maybe<O>> wrap(Class<O> aClass, Functions.FunctionWithError<I, O> func) {
164        return (x) -> {
165            try {
166                return Maybe.of(func.applyWithError(x));
167            } catch (Throwable t) {
168                return Maybe.of(t);
169            }
170        };
171    }
172
173    /**
174     * The contents of the Maybe object, which contain either a T or a Throwable
175     */
176    private final Either<T, Throwable> contents;
177
178    @SuppressWarnings("unchecked")
179    private Maybe(T left, Throwable right) {
180        if (left != null) {
181            contents = (Either<T, Throwable>) Either.left(left);
182        } else {
183            contents = (Either<T, Throwable>) Either.right(right);
184        }
185    }
186
187    /**
188     * Gets the value or throws the exception wrapped in an ExecutionException
189     * @return The value if it exists
190     * @throws IllegalStateException If this Maybe was an exception instead
191     */
192    public T get() throws IllegalStateException {
193        if (contents.hasRight()) {
194            throw new IllegalStateException(contents.getRight());
195        }
196        return contents.getLeft();
197    }
198
199    /**
200     * Returns the error if there is one, or else throws a {@link java.util.NoSuchElementException}.
201     * @return The error if one exists
202     * @throws java.util.NoSuchElementException if the error doesn't exist
203     */
204    public Throwable getError() throws NoSuchElementException {
205        return contents.getRight();
206    }
207
208    /**
209     * If this Maybe has an error and not a value
210     * @return True if this Maybe has an error
211     */
212    public boolean hasError() {
213        return contents.hasRight();
214    }
215
216    /**
217     * If this Maybe has a value and not an error
218     * @return True if this Maybe does not have an error
219     */
220    public boolean hasValue() {
221        return contents.hasLeft();
222    }
223
224    /**
225     * Chains a Maybe object by invoking the given function on it.
226     *
227     * There are three possible outcomes:
228     *
229     * 1. This object already has an error ({@link Maybe#hasError()} returns true), in which case this method will return a new Maybe with that error.
230     *
231     * 2. Applying the function to this Maybe's value results in an exception, in which case this method will return a new Maybe with that exception.
232     *
233     * 3. Applying the function is successful and produces an object of type 'B', in which case this method returns a new Maybe containing that object.
234     *
235     * @param downstream The mapping function to apply to this Maybe
236     * @param <B> The output type
237     * @return a Maybe object containing the exception
238     */
239    public <B> Maybe<B> map(Functions.FunctionWithError<T, B> downstream) {
240        if (this.hasError()) {
241            return Maybe.of(this.getError());
242        }
243
244        T input = this.get();
245
246        try {
247            B output = downstream.applyWithError(input);
248            return Maybe.of(output);
249        } catch(Throwable e) {
250            return Maybe.of(e);
251        }
252    }
253}