/** * 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; } // Optional support for ASDF serialization and deserialization. // See here: https://code.dlang.org/packages/asdf version (Have_asdf) { import asdf; static if ( __traits(compiles, deserialize!T(Asdf.init)) ) { /** * Deserializes an optional value from raw ASDF data. This function * is defined to allow for automatic deserialization of Optionals when * parsing JSON or other data types. * Params: * data = The data representing an optional value. * Returns: An exception if one is thrown. */ SerdeException deserializeFromAsdf(Asdf data) { static if (!__traits(compiles, {T t; t = T.init;})) { return new SerdeException( "Cannot deserialize Optional that contains immutable or otherwise unassignable value. " ~ "Ensure that the following code compiles: T t; t = T.init; for the given type T." ); } else { if (data == null) { this.isNull = true; this.value = T.init; } else { this.isNull = false; this.value = deserialize!T(data); } return null; } } /** * Serializes an optional value to allow for the automatic serialization * of Optionals when writing JSON or other data types. * Params: * serializer = The serializer to use (provided by ASDF). */ void serialize(S)(ref S serializer) const { if (this.isNull) { serializer.putValue(null); } else { serializeValue(serializer, this.value); } } } } } /** * 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); // Additional tests if the ASDF library is present: version (Have_asdf) { import asdf; assert(deserialize!(Optional!string)(`null`).isNull); assert(!deserialize!(Optional!bool)(`true`).isNull); assert(deserialize!(Optional!bool)(`true`).value == true); struct Sd { int x; int y; } struct S { Optional!bool a; Optional!string b; Optional!float c; Optional!Sd d; } string json = `{ "a": true, "b": "hello world!", "c": "3.14", "d": { "x": 42, "y": 67 } }`; S sa = deserialize!S(json); assert(sa.a && sa.a.value == true); assert(sa.b && sa.b.value == "hello world!"); assert(sa.c && sa.c.value == 3.14f); assert(sa.d && sa.d.value == Sd(42, 67)); // Check that optionals containing immutable data aren't supported for serialization & deserialization: struct Invalid { immutable int x; } Optional!Invalid opt = Optional!(Invalid).of(Invalid(42)); static assert(__traits(compiles, serializeToJson(opt))); try { deserialize!(Optional!(Invalid))(`{"x": 123}`); assert( false, "Failed to ensure that an exception is thrown when attempting to deserialize an incompatible Optional." ); } catch (SerdeException exc) { // pass. } } }