From eae8fd6c0e58b5b95964b23dea440c31e539967e Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 15 Aug 2024 19:21:48 -0400 Subject: [PATCH] Added initial types. --- .gitignore | 16 + dub.json | 12 + dub.selections.json | 6 + source/handy_http_primitives/handler.d | 8 + source/handy_http_primitives/multivalue_map.d | 448 ++++++++++++++++++ source/handy_http_primitives/optional.d | 133 ++++++ source/handy_http_primitives/package.d | 2 + source/handy_http_primitives/request.d | 32 ++ source/handy_http_primitives/response.d | 96 ++++ 9 files changed, 753 insertions(+) create mode 100644 .gitignore create mode 100644 dub.json create mode 100644 dub.selections.json create mode 100644 source/handy_http_primitives/handler.d create mode 100644 source/handy_http_primitives/multivalue_map.d create mode 100644 source/handy_http_primitives/optional.d create mode 100644 source/handy_http_primitives/package.d create mode 100644 source/handy_http_primitives/request.d create mode 100644 source/handy_http_primitives/response.d diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc93e58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.dub +docs.json +__dummy.html +docs/ +/handy-http-primitives +handy-http-primitives.so +handy-http-primitives.dylib +handy-http-primitives.dll +handy-http-primitives.a +handy-http-primitives.lib +handy-http-primitives-test-* +*.exe +*.pdb +*.o +*.obj +*.lst diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..acb0565 --- /dev/null +++ b/dub.json @@ -0,0 +1,12 @@ +{ + "authors": [ + "Andrew Lalis" + ], + "copyright": "Copyright © 2024, Andrew Lalis", + "dependencies": { + "streams": "~>3.5.0" + }, + "description": "Basic HTTP types that can be shared among various projects.", + "license": "MIT", + "name": "handy-http-primitives" +} \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..88fd59c --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,6 @@ +{ + "fileVersion": 1, + "versions": { + "streams": "3.5.0" + } +} diff --git a/source/handy_http_primitives/handler.d b/source/handy_http_primitives/handler.d new file mode 100644 index 0000000..44ab86d --- /dev/null +++ b/source/handy_http_primitives/handler.d @@ -0,0 +1,8 @@ +module handy_http_primitives.handler; + +import handy_http_primitives.request; +import handy_http_primitives.response; + +interface HttpRequestHandler { + void handle(in HttpRequest request, ref HttpResponse response); +} \ No newline at end of file diff --git a/source/handy_http_primitives/multivalue_map.d b/source/handy_http_primitives/multivalue_map.d new file mode 100644 index 0000000..8567a6b --- /dev/null +++ b/source/handy_http_primitives/multivalue_map.d @@ -0,0 +1,448 @@ +/** + * An implementation of a multi-valued mapping, where one key may map to one + * or more values. + */ +module handy_http_primitives.multivalue_map; + +import handy_http_primitives.optional; + +/** + * A multi-valued mapping, where a key is mapped to one or more values. The map + * is sorted by keys for O(log(n)) lookup and retrieval, and O(n*log(n)) + * insertion. + * + * `KeyType` is the type used for the map's keys. + * `ValueType` is the type used for the map's values. + * `KeySort` is a function that takes two operands and returns true if the + * first one is less than the second. + * `KeyEquals` is a function that takes two operands and returns true if they + * are equal. + */ +struct MultiValueMap( + KeyType, + ValueType, + alias KeySort = (a, b) => a < b, + alias KeyEquals = (a, b) => a == b +) { + /// The internal structure used to store each key and set of values. + static struct Entry { + /// The key for this entry. + KeyType key; + + /** + * The list of values associated with this entry's key. This always + * contains at least one value. + */ + ValueType[] values; + + /** + * Gets a human-readable string representation of this entry. + * Returns: A string representation of this entry. + */ + string toString() const { + import std.conv : to; + import std.algorithm : map, joiner; + import std.array : array; + + string keyStr = key.to!string; + string valuesStr = values + .map!(v => "\""~v.to!string~"\"") + .joiner(", ").array.to!string; + return "\"" ~ keyStr ~ "\": " ~ valuesStr; + } + } + + /// The internal, sorted array of entries. + private Entry[] entries; + + /** + * Finds the index of the entry with a given key in the internal array. + * Params: + * k = The key to search for. + * Returns: The index if it was found, or -1 if it doesn't exist. + */ + private long indexOf(KeyType k) const { + if (entries.length == 0) return -1; + if (entries.length == 1) { + return entries[0].key == k ? 0 : -1; + } + size_t startIdx = 0; + size_t endIdx = entries.length - 1; + while (startIdx <= endIdx) { + size_t mid = startIdx + (endIdx - startIdx) / 2; + const key = entries[mid].key; + if (KeyEquals(key, k)) return mid; + if (KeySort(entries[mid].key, k)) { + startIdx = mid + 1; + } else { + if (mid == 0) return -1; + endIdx = mid - 1; + } + } + return -1; + } + + /** + * Attempts to get the entry for a given key. Complexity is O(log(keyCount)). + * Params: + * k = The key to look for. + * Returns: An optional that may contain the entry that was found. + */ + private Optional!Entry getEntry(KeyType k) { + long idx = indexOf(k); + if (idx == -1) return Optional!Entry.empty(); + return Optional!Entry.of(entries[cast(size_t) idx]); + } + + /** + * Gets the number of unique keys in this map. + * Returns: The number of unique keys in this map. + */ + size_t length() const { + return entries.length; + } + + /** + * Determines if this map contains a value for the given key. + * Params: + * k = The key to search for. + * Returns: True if at least one value exists for the given key. + */ + bool contains(KeyType k) const { + return indexOf(k) != -1; + } + + /** + * Gets a list of all keys in this map, allocated in a new array. + * Returns: The list of keys in this map. + */ + KeyType[] keys() const { + KeyType[] keysArray = new KeyType[this.length()]; + foreach (size_t i, const Entry e; entries) { + keysArray[i] = e.key; + } + return keysArray; + } + + /** + * Gets all values associated with a given key, allocated in a new array. + * Params: + * k = The key to get the values of. + * Returns: The values associated with the given key, or an empty array if + * no values exist for the key. + */ + ValueType[] getAll(KeyType k) const { + long idx = indexOf(k); + if (idx == -1) return []; + return entries[cast(size_t) idx].values.dup; + } + + /** + * Gets the first value associated with a given key, as per the order in + * which the values were inserted. + * Params: + * k = The key to get the first value of. + * Returns: An optional contains the value, if there is at least one value + * for the given key. + */ + Optional!ValueType getFirst(KeyType k) const { + long idx = indexOf(k); + if (idx == -1) return Optional!ValueType.empty(); + return Optional!ValueType.of(entries[cast(size_t) idx].values[0]); + } + + /** + * Adds a single key -> value pair to the map, with time complexity of + * O(n*log(n)) due to sorting the new entry by its key. + * Params: + * k = The key. + * v = The value associated with the key. + */ + void add(KeyType k, ValueType v) { + long idx = indexOf(k); + if (idx == -1) { + entries ~= Entry(k, [v]); + import std.algorithm.sorting : sort; + sort!((a, b) => KeySort(a.key, b.key))(entries); + } else { + entries[cast(size_t) idx].values ~= v; + } + } + + /** + * Clears this map of all values. + */ + void clear() { + entries.length = 0; + } + + /** + * Removes a key from the map, thus removing all values associated with + * that key. + * Params: + * k = The key to remove. + */ + void remove(KeyType k) { + long idx = indexOf(k); + if (idx == -1) return; + if (entries.length == 1) { + clear(); + return; + } + if (idx + 1 < entries.length) { + const i = cast(size_t) idx; + entries[i .. $ - 1] = entries[i + 1 .. $]; + } + entries.length = entries.length - 1; + } + + /** + * Gets this multivalue map as an associative array, where each key is + * mapped to a list of values. + * Returns: The associative array. + */ + ValueType[][KeyType] asAssociativeArray() const { + ValueType[][KeyType] aa; + foreach (const Entry entry; entries) { + aa[entry.key] = entry.values.dup; + } + return aa; + } + + /** + * Constructs a multivalued map from an associative array. + * Params: + * aa = The associative array to use. + * Returns: The multivalued map. + */ + static MultiValueMap!(KeyType, ValueType, KeySort) fromAssociativeArray(ValueType[][KeyType] aa) { + MultiValueMap!(KeyType, ValueType, KeySort) m; + foreach (KeyType k, ValueType[] values; aa) { + foreach (ValueType v; values) { + m.add(k, v); + } + } + return m; + } + + /** + * Constructs a multivalued map from an associative array of single values. + * Params: + * aa = The associative array to use. + * Returns: The multivalued map. + */ + static MultiValueMap!(KeyType, ValueType, KeySort) fromAssociativeArray(ValueType[KeyType] aa) { + MultiValueMap!(KeyType, ValueType, KeySort) m; + foreach (KeyType k, ValueType v; aa) { + m.add(k, v); + } + return m; + } + + /** + * An efficient builder that can be used to construct a multivalued map + * with successive `add` calls, which is more efficient than doing so + * directly due to the builder's deferred sorting. + */ + static struct Builder { + import std.array; + + private MultiValueMap m; + private RefAppender!(Entry[]) entryAppender; + + /** + * Adds a key -> value pair to the builder's map. + * Params: + * k = The key. + * v = The value associated with the key. + * Returns: A reference to the builder, for method chaining. + */ + ref Builder add(KeyType k, ValueType v) { + if (entryAppender.data is null) entryAppender = appender(&m.entries); + long idx = this.indexOf(k); + if (idx == -1) { + entryAppender ~= Entry(k, [v]); + } else { + m.entries[cast(size_t) idx].values ~= v; + } + return this; + } + + /** + * Builds the multivalued map. + * Returns: The map that was created. + */ + MultiValueMap build() { + if (m.entries.length == 0) return m; + import std.algorithm.sorting : sort; + sort!((a, b) => KeySort(a.key, b.key))(m.entries); + return m; + } + + private long indexOf(KeyType k) { + foreach (i, entry; m.entries) { + if (KeyEquals(entry.key, k)) return i; + } + return -1; + } + } + + // OPERATOR OVERLOADS below here + + /** + * Implements the empty index operator, which just returns the entire list + * of entries in this map. + * Returns: The list of entries in this map. + */ + inout(Entry)[] opIndex() inout { + return entries; + } + + /** + * Convenience overload to get the first value for a given key. Note: this + * will throw an exception if no values exist for the given key. To avoid + * this, use `getFirst` and deal with the missing value yourself. + * Params: + * key = The key to get the value of. + * Returns: The first value for the given key. + */ + ValueType opIndex(KeyType key) const { + import std.conv : to; + return getFirst(key).orElseThrow("No values exist for key " ~ key.to!string ~ "."); + } + + /** + * `opApply` implementation to allow iterating over this map by all pairs + * of keys and values. + * Params: + * dg = The foreach body that uses each key -> value pair. + * Returns: The result of the delegate call. + */ + int opApply(int delegate(const ref KeyType, const ref ValueType) dg) const { + int result = 0; + foreach (const Entry entry; entries) { + foreach (ValueType value; entry.values) { + result = dg(entry.key, value); + if (result) break; + } + } + return result; + } + + /** + * Implements opBinaryRight for the "in" operator, such that `k in m` will + * resolve to the list of values for key `k` in the multivalue map `m` if + * that key exists, or `null` if not. + * + * Params: + * lhs = The key to use. + * Returns: A list of values for the given key, or null if no such key exists. + * --- + * StringMultiValueMap m; + * m.add("a", "hello"); + * assert("a" in m); + * assert(("a" in m) == ["hello"]); + * assert("b" !in m); + * assert(("b" in m) is null); + * --- + */ + ValueType[] opBinaryRight(string op : "in")(string lhs) { + Optional!Entry optionalEntry = this.getEntry(lhs); + if (optionalEntry) { + Entry entry = optionalEntry.value; + return entry.values; + } + return null; + } + + /** + * Converts this map into a human-readable string which lists each key and + * all of the values for that key. + * Returns: A string representation of this map. + */ + string toString() const { + import std.format : format; + return format!"%(%s\n%)"(entries); + } +} + +/** + * A multivalued map of strings, where each string key refers to zero or more + * string values. All keys are case-sensitive. + */ +alias StringMultiValueMap = MultiValueMap!(string, string); + +/** + * A multivalued map of strings, where keys are NOT case sensitive. + */ +alias CaseInsensitiveStringMultiValueMap = MultiValueMap!( + string, string, + (a, b) => a < b, + (a, b) { + import std.string : toLower; + return toLower(a) == toLower(b); + } +); + +unittest { + StringMultiValueMap m; + m.add("a", "hello"); + assert(m.getFirst("a").orElseThrow == "hello"); + m.add("b", "bye"); + assert(m.getFirst("b").orElseThrow == "bye"); + assert(m.asAssociativeArray == ["a": ["hello"], "b": ["bye"]]); + assert(m["b"] == "bye"); + m.remove("a"); + assert(!m.contains("a")); + m.add("b", "hello"); + assert(m.getAll("b") == ["bye", "hello"]); + m.clear(); + assert(m.length == 0); + assert(!m.contains("a")); + assert(!m.contains("b")); + + auto m2 = StringMultiValueMap.fromAssociativeArray(["a": "123", "b": "abc"]); + assert(m2["a"] == "123"); + assert(m2["b"] == "abc"); + + auto m3 = StringMultiValueMap.fromAssociativeArray(["a": [""], "b": [""], "c": ["hello"]]); + assert(m3.contains("a")); + assert(m3["a"] == ""); + assert(m3.contains("b")); + assert(m3["b"] == ""); + assert(m3.contains("c")); + assert(m3["c"] == "hello"); + + // Test that opApply works: + int n = 0; + foreach (key, value; m3) { + n++; + } + assert(n == 3); + + // Test opBinaryRight with "in" operator. + StringMultiValueMap m4; + m4.add("a", "1"); + assert("a" in m4); + assert("b" !in m4); + auto valuesA = "a" in m4; + assert(valuesA == ["1"]); + auto valuesB = "b" in m4; + assert(valuesB is null); + + // Test opIndex with an empty index. + StringMultiValueMap m5; + assert(m5[] == []); + m5.add("a", "123"); + assert(m5[] == [StringMultiValueMap.Entry("a", ["123"])]); + + // test on a const instance + const(StringMultiValueMap) m6 = m5; + assert(m6[] == [StringMultiValueMap.Entry("a", ["123"])]); + + // test the builder with multi-values + StringMultiValueMap.Builder builder; + builder.add("a", "123"); + builder.add("a", "456"); + assert(builder.build()[] == [StringMultiValueMap.Entry("a", ["123", "456"])], builder.build().toString); +} diff --git a/source/handy_http_primitives/optional.d b/source/handy_http_primitives/optional.d new file mode 100644 index 0000000..b28c9e4 --- /dev/null +++ b/source/handy_http_primitives/optional.d @@ -0,0 +1,133 @@ +/** + * 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; + +import std.typecons : Nullable; + +/** + * 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 value using a Phobos nullable. + * Params: + * nullableValue = The nullable value to use. + * Returns: An optional that contains the given nullable value. + */ + static Optional!T of (Nullable!T nullableValue) { + if (nullableValue.isNull) return Optional!T.empty(); + return Optional!T.of(nullableValue.get); + } + + /** + * Constructs an optional that's empty. + * Returns: An optional that is empty. + */ + static Optional!T empty() { + return Optional!T(T.init, true); + } + + /** + * Converts this optional to a Phobos-style Nullable. + * Returns: A `Nullable!T` representing this optional. + */ + Nullable!T asNullable() { + Nullable!T n; + if (!this.isNull) { + n = this.value; + } + return n; + } + + /** + * 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); +} diff --git a/source/handy_http_primitives/package.d b/source/handy_http_primitives/package.d new file mode 100644 index 0000000..5b1b3bf --- /dev/null +++ b/source/handy_http_primitives/package.d @@ -0,0 +1,2 @@ +module handy_http_primitives; + diff --git a/source/handy_http_primitives/request.d b/source/handy_http_primitives/request.d new file mode 100644 index 0000000..dc572e6 --- /dev/null +++ b/source/handy_http_primitives/request.d @@ -0,0 +1,32 @@ +module handy_http_primitives.request; + +import streams; + +import handy_http_primitives.multivalue_map; + +struct HttpRequest { + const ubyte httpVersion = 1; + const Method method = Method.GET; + const string url = ""; + const(CaseInsensitiveStringMultiValueMap) headers; + const(StringMultiValueMap) queryParams; + InputStream!ubyte inputStream; +} + +/** + * Enumeration of all possible HTTP request methods as unsigned integer values + * for efficient logic. + * + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods + */ +public enum Method : ushort { + GET = 1 << 0, + HEAD = 1 << 1, + POST = 1 << 2, + PUT = 1 << 3, + DELETE = 1 << 4, + CONNECT = 1 << 5, + OPTIONS = 1 << 6, + TRACE = 1 << 7, + PATCH = 1 << 8 +} diff --git a/source/handy_http_primitives/response.d b/source/handy_http_primitives/response.d new file mode 100644 index 0000000..b622a22 --- /dev/null +++ b/source/handy_http_primitives/response.d @@ -0,0 +1,96 @@ +module handy_http_primitives.response; + +import streams; + +import handy_http_primitives.multivalue_map; + +struct HttpResponse { + StatusInfo status = HttpStatus.OK; + StringMultiValueMap headers; + OutputStream!ubyte outputStream; +} + +/** + * A struct containing basic information about a response status. + */ +struct StatusInfo { + const ushort code; + const string text; +} + +/** + * An enum defining all valid HTTP response statuses: + * See here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + */ +enum HttpStatus : StatusInfo { + // Information + CONTINUE = StatusInfo(100, "Continue"), + SWITCHING_PROTOCOLS = StatusInfo(101, "Switching Protocols"), + PROCESSING = StatusInfo(102, "Processing"), + EARLY_HINTS = StatusInfo(103, "Early Hints"), + + // Success + OK = StatusInfo(200, "OK"), + CREATED = StatusInfo(201, "Created"), + ACCEPTED = StatusInfo(202, "Accepted"), + NON_AUTHORITATIVE_INFORMATION = StatusInfo(203, "Non-Authoritative Information"), + NO_CONTENT = StatusInfo(204, "No Content"), + RESET_CONTENT = StatusInfo(205, "Reset Content"), + PARTIAL_CONTENT = StatusInfo(206, "Partial Content"), + MULTI_STATUS = StatusInfo(207, "Multi-Status"), + ALREADY_REPORTED = StatusInfo(208, "Already Reported"), + IM_USED = StatusInfo(226, "IM Used"), + + // Redirection + MULTIPLE_CHOICES = StatusInfo(300, "Multiple Choices"), + MOVED_PERMANENTLY = StatusInfo(301, "Moved Permanently"), + FOUND = StatusInfo(302, "Found"), + SEE_OTHER = StatusInfo(303, "See Other"), + NOT_MODIFIED = StatusInfo(304, "Not Modified"), + TEMPORARY_REDIRECT = StatusInfo(307, "Temporary Redirect"), + PERMANENT_REDIRECT = StatusInfo(308, "Permanent Redirect"), + + // Client error + BAD_REQUEST = StatusInfo(400, "Bad Request"), + UNAUTHORIZED = StatusInfo(401, "Unauthorized"), + PAYMENT_REQUIRED = StatusInfo(402, "Payment Required"), + FORBIDDEN = StatusInfo(403, "Forbidden"), + NOT_FOUND = StatusInfo(404, "Not Found"), + METHOD_NOT_ALLOWED = StatusInfo(405, "Method Not Allowed"), + NOT_ACCEPTABLE = StatusInfo(406, "Not Acceptable"), + PROXY_AUTHENTICATION_REQUIRED = StatusInfo(407, "Proxy Authentication Required"), + REQUEST_TIMEOUT = StatusInfo(408, "Request Timeout"), + CONFLICT = StatusInfo(409, "Conflict"), + GONE = StatusInfo(410, "Gone"), + LENGTH_REQUIRED = StatusInfo(411, "Length Required"), + PRECONDITION_FAILED = StatusInfo(412, "Precondition Failed"), + PAYLOAD_TOO_LARGE = StatusInfo(413, "Payload Too Large"), + URI_TOO_LONG = StatusInfo(414, "URI Too Long"), + UNSUPPORTED_MEDIA_TYPE = StatusInfo(415, "Unsupported Media Type"), + RANGE_NOT_SATISFIABLE = StatusInfo(416, "Range Not Satisfiable"), + EXPECTATION_FAILED = StatusInfo(417, "Expectation Failed"), + IM_A_TEAPOT = StatusInfo(418, "I'm a teapot"), + MISDIRECTED_REQUEST = StatusInfo(421, "Misdirected Request"), + UNPROCESSABLE_CONTENT = StatusInfo(422, "Unprocessable Content"), + LOCKED = StatusInfo(423, "Locked"), + FAILED_DEPENDENCY = StatusInfo(424, "Failed Dependency"), + TOO_EARLY = StatusInfo(425, "Too Early"), + UPGRADE_REQUIRED = StatusInfo(426, "Upgrade Required"), + PRECONDITION_REQUIRED = StatusInfo(428, "Precondition Required"), + TOO_MANY_REQUESTS = StatusInfo(429, "Too Many Requests"), + REQUEST_HEADER_FIELDS_TOO_LARGE = StatusInfo(431, "Request Header Fields Too Large"), + UNAVAILABLE_FOR_LEGAL_REASONS = StatusInfo(451, "Unavailable For Legal Reasons"), + + // Server error + INTERNAL_SERVER_ERROR = StatusInfo(500, "Internal Server Error"), + NOT_IMPLEMENTED = StatusInfo(501, "Not Implemented"), + BAD_GATEWAY = StatusInfo(502, "Bad Gateway"), + SERVICE_UNAVAILABLE = StatusInfo(503, "Service Unavailable"), + GATEWAY_TIMEOUT = StatusInfo(504, "Gateway Timeout"), + HTTP_VERSION_NOT_SUPPORTED = StatusInfo(505, "HTTP Version Not Supported"), + VARIANT_ALSO_NEGOTIATES = StatusInfo(506, "Variant Also Negotiates"), + INSUFFICIENT_STORAGE = StatusInfo(507, "Insufficient Storage"), + LOOP_DETECTED = StatusInfo(508, "Loop Detected"), + NOT_EXTENDED = StatusInfo(510, "Not Extended"), + NETWORK_AUTHENTICATION_REQUIRED = StatusInfo(511, "Network Authentication Required") +}