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