/**
 * Module that defines an `Optional` type, which is a simplified version of
 * Phobos' Nullable, that also supports mapping the underlying data.
 */
module handy_http_primitives.optional;

/**
 * A simple wrapper around a value to make it optionally present.
 */
struct Optional(T) {
    /// The internal value of this optional.
    T value;

    /// Whether this optional is empty.
    bool isNull = true;

    /**
     * Constructs an optional value using a given value.
     * Params:
     *   value = The value to use.
     * Returns: An optional that contains the given value.
     */
    static Optional!T of(T value) {
        return Optional!T(value, false);
    }

    /**
     * Constructs an optional that's empty.
     * Returns: An optional that is empty.
     */
    static Optional!T empty() {
        return Optional!T(T.init, true);
    }

    /**
     * Gets the value of this optional if it exists, otherwise uses a given
     * default value.
     * Params:
     *   defaultValue = The value to return if no default value exists.
     * Returns: The value of the optional, or the default value if this
     * optional is empty.
     */
    T orElse(T defaultValue) {
        if (this.isNull) return defaultValue;
        return this.value;
    }

    /**
     * Gets the value of this optional if it exists, or throws an exception.
     * Params:
     *   msg = A message to put in the exception.
     * Returns: The value of this optional.
     */
    T orElseThrow(string msg = "Optional value is null.") {
        if (this.isNull) throw new Exception(msg);
        return this.value;
    }

    /**
     * Gets the value of this optional if it exists, or throws an exception as
     * produced by the given delegate.
     * Params:
     *   exceptionSupplier = A delegate that returns an exception to throw if
     *                       this optional is null.
     * Returns: The value of this optional.
     */
    T orElseThrow(Exception delegate() exceptionSupplier) {
        if (this.isNull) throw exceptionSupplier();
        return this.value;
    }

    /**
     * Provides a mechanism to allow usage in boolean expressions.
     * Returns: true if non-null, false if null
     * ---
     * auto optInt = Optional!int.empty();
     * assert(!optInt);
     * auto optStr = Optional!string.of("Hello");
     * assert(optStr);
     * ---
     */
    bool opCast(B : bool)() const {
        return !this.isNull;
    }
}

/**
 * Maps the value of a given optional to another type using a given function.
 * Params:
 *   opt = The optional to map.
 * Returns: An optional whose type is the return-type of the given `fn`
 * template argument function.
 */
auto mapIfPresent(alias fn, T)(Optional!T opt) {
    alias U = typeof(fn(T.init));
    if (opt.isNull) return Optional!U.empty();
    return Optional!U.of(fn(opt.value));
}

unittest {
    Optional!string s = Optional!string.of("hello");
    assert(!s.isNull);
    assert(s.value == "hello");
    assert(s); // test boolean conversion
    Optional!int mapped = s.mapIfPresent!(str => 1);
    assert(!mapped.isNull);
    assert(mapped.value == 1);
}