Added initial set of packages in http-primitives
This commit is contained in:
parent
26adc5c52a
commit
ad0002fbeb
|
@ -0,0 +1,17 @@
|
|||
.dub
|
||||
docs.json
|
||||
__dummy.html
|
||||
docs/
|
||||
/handy-http-2
|
||||
handy-http-2.so
|
||||
handy-http-2.dylib
|
||||
handy-http-2.dll
|
||||
handy-http-2.a
|
||||
handy-http-2.lib
|
||||
handy-http-2-test-*
|
||||
*.exe
|
||||
*.pdb
|
||||
*.o
|
||||
*.obj
|
||||
*.lst
|
||||
*-test-library
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"authors": [
|
||||
"Andrew Lalis"
|
||||
],
|
||||
"copyright": "Copyright © 2024, Andrew Lalis",
|
||||
"description": "Improved HTTP server based on handy-http.",
|
||||
"license": "MIT",
|
||||
"name": "handy-http-2",
|
||||
"buildRequirements": ["allowWarnings"],
|
||||
"subPackages": [
|
||||
"./sub-packages/http-parser/",
|
||||
"./sub-packages/http-primitives/"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env rdmd
|
||||
|
||||
|
||||
module run;
|
||||
|
||||
import std.stdio;
|
||||
import std.process;
|
||||
import std.string;
|
||||
|
||||
int main(string[] args) {
|
||||
if (args.length < 2) {
|
||||
stderr.writeln("Missing required sub-command. Should be one of the following:");
|
||||
stderr.writeln(" - \"unit-test\": Build and run unit tests for the entire project.");
|
||||
stderr.writeln(" - \"integration-test\": Build and run integration tests for the entire project.");
|
||||
return 1;
|
||||
}
|
||||
string command = args[1].toLower.strip;
|
||||
if (command == "unit-test") {
|
||||
return doUnitTests(args[2..$]);
|
||||
} else if (command == "integration-test") {
|
||||
return doIntegrationTests(args[2..$]);
|
||||
}
|
||||
stderr.writefln!"Invalid sub-command: %s."(command);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int doUnitTests(string[] args) {
|
||||
const subPackages = ["http-primitives", "http-parser"];
|
||||
uint subPackagesSuccessful = 0;
|
||||
foreach (subPackage; subPackages) {
|
||||
writefln!"Running unit tests for sub-package \"%s\"..."(subPackage);
|
||||
int exitCode = wait(spawnProcess(["dub", "test", ":" ~ subPackage]));
|
||||
if (exitCode == 0) subPackagesSuccessful++;
|
||||
writeln();
|
||||
}
|
||||
writefln!"Unit tests were successful in %d / %d sub-packages.\n"(subPackagesSuccessful, subPackages.length);
|
||||
if (subPackagesSuccessful != subPackages.length) {
|
||||
writeln("Skipping testing main package because sub-packages have errors.");
|
||||
return 1;
|
||||
}
|
||||
writeln("Running unit tests for main package...");
|
||||
return wait(spawnProcess(["dub", "test"]));
|
||||
}
|
||||
|
||||
int doIntegrationTests(string[] args) {
|
||||
writeln("Integration tests not yet implemented.");
|
||||
return 1;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import std.stdio;
|
||||
|
||||
void main()
|
||||
{
|
||||
writeln("Edit source/app.d to start your project.");
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# http-parser
|
||||
|
||||
This sub-package is responsible for defining the functions that parse the
|
||||
HTTP request information from data the client has sent.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "http-parser",
|
||||
"targetType": "library"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module http_parser;
|
|
@ -0,0 +1,4 @@
|
|||
# http-primitives
|
||||
|
||||
This sub-package defines the primitive HTTP types that are used throughout the
|
||||
project, like `HttpRequest`, `HttpResponse`, `HttpStatus`, and so on.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "http-primitives",
|
||||
"targetType": "library"
|
||||
}
|
|
@ -0,0 +1,433 @@
|
|||
/**
|
||||
* An implementation of a multi-valued mapping, where one key may map to one
|
||||
* or more values.
|
||||
*/
|
||||
module http_primitives.multivalue_map;
|
||||
|
||||
import http_primitives.optional;
|
||||
|
||||
/**
|
||||
* A multi-valued mapping, where a key is mapped to one or more values. The map
|
||||
* can optionally be sorted by keys for O(log(n)) lookup and retrieval, and
|
||||
* O(n*log(n)) insertion, instead of the default linear search.
|
||||
*/
|
||||
struct MultiValueMap(KeyType, ValueType, bool Sorted = true, alias KeySort = (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;
|
||||
}
|
||||
static if (Sorted) {
|
||||
size_t startIdx = 0;
|
||||
size_t endIdx = entries.length - 1;
|
||||
while (startIdx <= endIdx) {
|
||||
size_t mid = startIdx + (endIdx - startIdx) / 2;
|
||||
if (entries[mid].key == k) return mid;
|
||||
if (KeySort(entries[mid].key, k)) {
|
||||
startIdx = mid + 1;
|
||||
} else {
|
||||
endIdx = mid - 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
} else {
|
||||
for (size_t idx = 0; idx < entries.length; idx++) {
|
||||
if (entries[idx].key == k) return idx;
|
||||
}
|
||||
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]);
|
||||
static if (Sorted) {
|
||||
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 fromAssociativeArray(ValueType[][KeyType] aa) {
|
||||
MultiValueMap 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 fromAssociativeArray(ValueType[KeyType] aa) {
|
||||
MultiValueMap 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;
|
||||
static if (Sorted) {
|
||||
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 (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);
|
||||
|
||||
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);
|
||||
}
|
|
@ -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 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);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module http_primitives;
|
||||
|
||||
public import http_primitives.request;
|
||||
public import http_primitives.optional;
|
||||
public import http_primitives.multivalue_map;
|
|
@ -0,0 +1,410 @@
|
|||
/**
|
||||
* This module defines input and output ranges that map onto sockets, to enable
|
||||
* easy reading and writing of data using all the benefits of ranges. It also
|
||||
* defines the interfaces used by HttpRequest and HttpResponse to allow
|
||||
* pluggable range implementations, useful for testing.
|
||||
*/
|
||||
module http_primitives.ranges;
|
||||
|
||||
import std.exception;
|
||||
import std.format;
|
||||
import std.algorithm : min;
|
||||
import std.socket;
|
||||
|
||||
import std.stdio;
|
||||
|
||||
/**
|
||||
* An input range for reading from a Socket.
|
||||
*/
|
||||
struct SocketInputRange(size_t BufferSize) {
|
||||
/// The internal socket that serves as this range's data source.
|
||||
Socket socket;
|
||||
/// The internal (stack-allocated) buffer that this range uses.
|
||||
ubyte[BufferSize] buffer;
|
||||
/// The index representing the end (exclusive) of the data in the buffer.
|
||||
size_t bufferIdx = 0;
|
||||
/// Internal flag used to mark the socket as closed and thus this range empty.
|
||||
bool closed = false;
|
||||
|
||||
this(Socket socket, bool initialRead = true) {
|
||||
this.socket = socket;
|
||||
if (initialRead) {
|
||||
popFront();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this socket input range is empty, which is true only
|
||||
* if we determine that the socket has been closed. Note that calling
|
||||
* `front()` may return an empty slice of the buffer if no data has been
|
||||
* received yet.
|
||||
* Returns: True if the socket has been closed and no more data can be read.
|
||||
*/
|
||||
bool empty() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a slice to the data currently held in this range's buffer, which
|
||||
* may be empty (length of 0), even if `empty` returns false.
|
||||
* Returns: A slice to the data currently in this range's buffer.
|
||||
*/
|
||||
ubyte[] front() {
|
||||
return buffer[0 .. bufferIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the current contents of this range's buffer, and attempts to
|
||||
* receive more data from the socket if it's still alive. Warning! This
|
||||
* method will BLOCK if the underlying socket is blocking!
|
||||
*/
|
||||
void popFront() {
|
||||
if (!socket.isAlive) {
|
||||
closed = true;
|
||||
return;
|
||||
}
|
||||
ptrdiff_t bytesRead = socket.receive(buffer);
|
||||
if (bytesRead == 0) {
|
||||
closed = true;
|
||||
} else if (bytesRead == Socket.ERROR) {
|
||||
closed = true;
|
||||
throw new SocketRangeException(lastSocketError());
|
||||
} else {
|
||||
bufferIdx = bytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An output range for writing to a Socket. Serves as an output range for both
|
||||
* `ubyte` and `ubyte[]`, by using a template-defined buffer size.
|
||||
*/
|
||||
struct SocketOutputRange(size_t BufferSize) {
|
||||
/// The internal socket that's written to.
|
||||
Socket socket;
|
||||
/// The buffer to which data is first written before flushing to the socket.
|
||||
ubyte[BufferSize] buffer;
|
||||
/// The index of the buffer at which new data is written.
|
||||
size_t bufferIdx = 0;
|
||||
|
||||
/**
|
||||
* Writes an array of bytes to the range. If this range's internal buffer
|
||||
* becomes full as a result of this method call, it will `flush()` and
|
||||
* write to the underlying socket.
|
||||
* Params:
|
||||
* bytes = The bytes to write.
|
||||
* Throws: `SocketRangeException` if flushing to the underlying socket fails.
|
||||
*/
|
||||
void put(ubyte[] bytes) {
|
||||
size_t dataIdx = 0;
|
||||
while (dataIdx < bytes.length) {
|
||||
const size_t bytesLeftToSend = bytes.length - dataIdx;
|
||||
const size_t bufferSpace = BufferSize - bufferIdx;
|
||||
const size_t bytesToCopy = min(bufferSpace, bytesLeftToSend);
|
||||
buffer[bufferIdx .. (bufferIdx + bytesToCopy)] = bytes[dataIdx .. (dataIdx + bytesToCopy)];
|
||||
dataIdx += bytesToCopy;
|
||||
bufferIdx += bytesToCopy;
|
||||
if (bufferIdx == BufferSize) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single byte to the range. If this range's internal buffer
|
||||
* becomes full as a result of this method call, it will `flush()` and
|
||||
* write to the underlying socket.
|
||||
* Params:
|
||||
* singleByte = The byte to write.
|
||||
* Throws: `SocketRangeException` if flushing to the underlying socket fails.
|
||||
*/
|
||||
void put(ubyte singleByte) {
|
||||
buffer[bufferIdx++] = singleByte;
|
||||
if (bufferIdx == BufferSize) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a value to the range as a big-endian (network byte order) set of
|
||||
* bytes. Only works for integrals, chars, booleans, and float/double. See
|
||||
* `std.bitmanip.nativeToBigEndian` for details on the conversion.
|
||||
* Params:
|
||||
* value = The value to write.
|
||||
*/
|
||||
void put(T)(const T value) {
|
||||
import std.bitmanip : nativeToBigEndian;
|
||||
auto bytes = nativeToBigEndian(value);
|
||||
this.put(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes any data in the buffer to the underlying socket.
|
||||
* Throws: `SocketRangeException` if sending data fails.
|
||||
*/
|
||||
void flush() {
|
||||
if (bufferIdx == 0) return;
|
||||
const ptrdiff_t bytesSent = socket.send(buffer[0..bufferIdx]);
|
||||
if (bytesSent == Socket.ERROR) {
|
||||
throw new SocketRangeException(lastSocketError());
|
||||
} else if (bytesSent != bufferIdx) {
|
||||
throw new SocketRangeException(
|
||||
format!"Failed to send all %d bytes. Only sent %d."(bufferIdx, bytesSent)
|
||||
);
|
||||
}
|
||||
bufferIdx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception representing a socket IO error.
|
||||
*/
|
||||
class SocketRangeException : Exception {
|
||||
mixin basicExceptionCtors;
|
||||
}
|
||||
|
||||
version(unittest) {
|
||||
/**
|
||||
* A convenience for unit tests, this test instance contains initialized
|
||||
* sockets and ranges with a configured buffer size.
|
||||
*/
|
||||
struct TestInstance(size_t outBufferSize, size_t inBufferSize) {
|
||||
Socket outputSocket;
|
||||
Socket inputSocket;
|
||||
SocketOutputRange!(outBufferSize) outputRange;
|
||||
SocketInputRange!(inBufferSize) inputRange;
|
||||
|
||||
static TestInstance create() {
|
||||
Socket[2] pair = socketPair();
|
||||
return TestInstance(
|
||||
pair[0],
|
||||
pair[1],
|
||||
SocketOutputRange!(outBufferSize)(pair[0]),
|
||||
SocketInputRange!(inBufferSize)(pair[1], false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
alias StandardTestInstance = TestInstance!(4096, 4096);
|
||||
}
|
||||
|
||||
// Test basic reading and writing.
|
||||
unittest {
|
||||
auto t = StandardTestInstance.create();
|
||||
t.outputRange.put(cast(ubyte) 42);
|
||||
assert(t.outputRange.bufferIdx == 1); // Assert that the byte was put into the range's buffer.
|
||||
t.outputRange.flush();
|
||||
assert(t.outputRange.bufferIdx == 0); // Assert that the data was written and the buffer reset.
|
||||
t.outputSocket.close();
|
||||
|
||||
assert(!t.inputRange.empty); // The input range should initially not be empty because the socket is not detected as dead yet.
|
||||
assert(t.inputRange.front.length == 0); // Because the test instance's input range has `initialRead` as false, no data has been read yet.
|
||||
t.inputRange.popFront();
|
||||
assert(!t.inputRange.empty);
|
||||
assert(t.inputRange.front.length == 1);
|
||||
assert(t.inputRange.front[0] == 42);
|
||||
t.inputSocket.close();
|
||||
t.inputRange.popFront(); // We need to attempt to read once more to determine if the socket has closed.
|
||||
assert(t.inputRange.empty); // Assert that the input range is indeed empty now.
|
||||
}
|
||||
|
||||
// Test reading and writing big chunks that exceed the size limits.
|
||||
unittest {
|
||||
import std.file;
|
||||
import std.path;
|
||||
import std.array;
|
||||
|
||||
auto t = TestInstance!(128, 128).create();
|
||||
string filePath = buildPath("sub-packages", "http-primitives", "source", "http_primitives", "ranges.d");
|
||||
string content = readText(filePath);
|
||||
assert(content.length > 4096);
|
||||
|
||||
// Write the entire chunk of data to the output.
|
||||
t.outputRange.put(cast(ubyte[]) content);
|
||||
t.outputRange.flush();
|
||||
t.outputSocket.close();
|
||||
|
||||
// Now read and append the data to an appender so we can check it.
|
||||
Appender!string app;
|
||||
while (!t.inputRange.empty) {
|
||||
app ~= cast(string) t.inputRange.front;
|
||||
t.inputRange.popFront();
|
||||
}
|
||||
assert(content == app[]);
|
||||
}
|
||||
|
||||
// Test reading and writing non-byte types (integral, bool, etc.)
|
||||
unittest {
|
||||
import std.bitmanip : bigEndianToNative;
|
||||
|
||||
auto t = StandardTestInstance.create();
|
||||
|
||||
long value = 123_456_789_000;
|
||||
t.outputRange.put(value);
|
||||
t.outputRange.flush();
|
||||
t.inputRange.popFront();
|
||||
assert(t.inputRange.front.length == 8);
|
||||
ubyte[8] bytes = t.inputRange.front[0..8];
|
||||
long readValue = bigEndianToNative!long(bytes);
|
||||
assert(readValue == value);
|
||||
|
||||
bool bValue = false;
|
||||
t.outputRange.put(bValue);
|
||||
t.outputRange.flush();
|
||||
t.inputRange.popFront();
|
||||
assert(t.inputRange.front.length == 1);
|
||||
ubyte[1] bytes1 = t.inputRange.front[0..1];
|
||||
bool readBValue = bigEndianToNative!bool(bytes1);
|
||||
assert(readBValue == bValue);
|
||||
|
||||
t.outputSocket.close();
|
||||
t.inputSocket.close();
|
||||
}
|
||||
|
||||
// Polymorphic OOP-Style ranges:
|
||||
|
||||
/**
|
||||
* An interface for an output range to which bytes, and some other types
|
||||
* convertible to bytes, may be written. The underlying implementation is
|
||||
* likely buffered, so call `flush()` to write the buffered data once ready.
|
||||
*/
|
||||
interface ResponseOutputRange {
|
||||
void put(ubyte singleByte);
|
||||
void put(ubyte[] bytes);
|
||||
void put(T)(const T value);
|
||||
void flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for an input range from which chunks of bytes can be read.
|
||||
*/
|
||||
interface RequestInputRange {
|
||||
bool empty();
|
||||
ubyte[] front();
|
||||
void popFront();
|
||||
}
|
||||
|
||||
class SocketResponseOutputRange(size_t BufferSize) : ResponseOutputRange {
|
||||
private SocketOutputRange!BufferSize outputRange;
|
||||
|
||||
this(Socket socket) {
|
||||
this.outputRange = SocketOutputRange!BufferSize(socket);
|
||||
}
|
||||
|
||||
void put(ubyte singleByte) {
|
||||
outputRange.put(singleByte);
|
||||
}
|
||||
|
||||
void put(ubyte[] bytes) {
|
||||
outputRange.put(bytes);
|
||||
}
|
||||
|
||||
void put(T)(const T value) {
|
||||
outputRange.put!(T)(value);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
outputRange.flush();
|
||||
}
|
||||
}
|
||||
|
||||
class SocketRequestInputRange(size_t BufferSize) : RequestInputRange {
|
||||
private SocketInputRange!BufferSize inputRange;
|
||||
|
||||
this(Socket socket, bool initialRead = true) {
|
||||
this.inputRange = SocketInputRange!BufferSize(socket, initialRead);
|
||||
}
|
||||
|
||||
bool empty() {
|
||||
return inputRange.empty();
|
||||
}
|
||||
|
||||
ubyte[] front() {
|
||||
return inputRange.front();
|
||||
}
|
||||
|
||||
void popFront() {
|
||||
inputRange.popFront();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An output range that simply writes to an internal buffer, which is useful
|
||||
* for inspecting the data written to an HTTP response, for example.
|
||||
*/
|
||||
class ArrayResponseOutputRange : ResponseOutputRange {
|
||||
import std.array;
|
||||
Appender!(ubyte[]) app;
|
||||
|
||||
void put(ubyte singleByte) {
|
||||
app ~= singleByte;
|
||||
}
|
||||
|
||||
void put(ubyte[] bytes) {
|
||||
app ~= bytes;
|
||||
}
|
||||
|
||||
void put(T)(const T value) {
|
||||
import std.bitmanip : nativeToBigEndian;
|
||||
auto bytes = nativeToBigEndian(value);
|
||||
app ~= bytes[0..T.sizeof];
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
ubyte[] data() {
|
||||
return app[];
|
||||
}
|
||||
}
|
||||
|
||||
// Test basic operations of the ArrayResponseOutputRange
|
||||
unittest {
|
||||
import std.bitmanip : bigEndianToNative;
|
||||
|
||||
scope r = new ArrayResponseOutputRange();
|
||||
r.put(cast(ubyte) 1);
|
||||
assert(r.data.length == 1);
|
||||
assert(r.data[0] == 1);
|
||||
r.put!ulong(42);
|
||||
assert(r.data.length == ubyte.sizeof + ulong.sizeof, format!"%d"(r.data.length));
|
||||
|
||||
ubyte[ulong.sizeof] bytes = r.data[1..ulong.sizeof + ubyte.sizeof];
|
||||
ulong value = bigEndianToNative!ulong(bytes);
|
||||
assert(value == 42);
|
||||
|
||||
scope r2 = new ArrayResponseOutputRange();
|
||||
ubyte[] data = [1, 2, 3, 4, 5];
|
||||
r2.put(data);
|
||||
assert(r2.data.length == data.length);
|
||||
assert(r2.data == data);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input range that simply supplies data from an internal buffer, which is
|
||||
* useful for validating HTTP request logic against pre-written requests, for
|
||||
* example.
|
||||
*/
|
||||
class ArrayRequestInputRange : RequestInputRange {
|
||||
ubyte[] data;
|
||||
bool popped = false;
|
||||
|
||||
this(ubyte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
bool empty() {
|
||||
return !popped;
|
||||
}
|
||||
|
||||
ubyte[] front() {
|
||||
if (popped) return [];
|
||||
return data;
|
||||
}
|
||||
|
||||
void popFront() {
|
||||
popped = true;
|
||||
this.data = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
module http_primitives.request;
|
||||
|
||||
import http_primitives.multivalue_map;
|
||||
import http_primitives.ranges;
|
||||
|
||||
import std.socket : Address;
|
||||
|
||||
/**
|
||||
* A struct describing the contents of an HTTP request.
|
||||
*/
|
||||
struct HttpRequest {
|
||||
/**
|
||||
* The HTTP method, or verb, which was requested.
|
||||
*/
|
||||
Method method = Method.GET;
|
||||
|
||||
/**
|
||||
* The URL that was requested.
|
||||
*/
|
||||
string url = "";
|
||||
|
||||
/**
|
||||
* The HTTP version of this request.
|
||||
*/
|
||||
ubyte httpVersion = 1;
|
||||
|
||||
/**
|
||||
* A multi-valued map of headers that were provided to this request.
|
||||
*/
|
||||
StringMultiValueMap headers;
|
||||
|
||||
/**
|
||||
* A multi-valued map of query parameters that were provided to this
|
||||
* request, as parsed from the request's URL.
|
||||
*/
|
||||
StringMultiValueMap queryParams;
|
||||
|
||||
/**
|
||||
* The remote address that this request came from.
|
||||
*/
|
||||
Address remoteAddress;
|
||||
|
||||
/**
|
||||
* The input range from which the request body can be read.
|
||||
*/
|
||||
RequestInputRange inputRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration of all possible HTTP request methods as unsigned integer values
|
||||
* for efficient logic.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||
*/
|
||||
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
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
module http_primitives.response;
|
||||
|
||||
import http_primitives.multivalue_map;
|
||||
import http_primitives.ranges;
|
||||
|
||||
/**
|
||||
* A struct describing the contents of an HTTP response.
|
||||
*/
|
||||
struct HttpResponse {
|
||||
/**
|
||||
* The status of this response.
|
||||
*/
|
||||
HttpResponseStatusInfo status = HttpStatus.OK;
|
||||
|
||||
/**
|
||||
* A multi-valued map of headers to send with this response.
|
||||
*/
|
||||
MultiValueMap!(string, string, false) headers;
|
||||
|
||||
/**
|
||||
* The output range to write the response to.
|
||||
*/
|
||||
ResponseOutputRange outputRange;
|
||||
|
||||
/**
|
||||
* A private flag indicating whether this response has written its status
|
||||
* and headers. This is used to make sure they're only written once, no
|
||||
* matter how many times the included "write..." functions are called.
|
||||
* Use `response.isFlushed` to check the value.
|
||||
*/
|
||||
private bool statusAndHeadersWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* A struct containing basic information about a response status.
|
||||
*/
|
||||
struct HttpResponseStatusInfo {
|
||||
/**
|
||||
* The integer status code for this response status.
|
||||
*/
|
||||
ushort code;
|
||||
|
||||
/**
|
||||
* A textual description of this response status.
|
||||
*/
|
||||
string text;
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum defining all valid HTTP response statuses:
|
||||
* See here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
*/
|
||||
enum HttpStatus : HttpResponseStatusInfo {
|
||||
// Information
|
||||
CONTINUE = HttpResponseStatusInfo(100, "Continue"),
|
||||
SWITCHING_PROTOCOLS = HttpResponseStatusInfo(101, "Switching Protocols"),
|
||||
PROCESSING = HttpResponseStatusInfo(102, "Processing"),
|
||||
EARLY_HINTS = HttpResponseStatusInfo(103, "Early Hints"),
|
||||
|
||||
// Success
|
||||
OK = HttpResponseStatusInfo(200, "OK"),
|
||||
CREATED = HttpResponseStatusInfo(201, "Created"),
|
||||
ACCEPTED = HttpResponseStatusInfo(202, "Accepted"),
|
||||
NON_AUTHORITATIVE_INFORMATION = HttpResponseStatusInfo(203, "Non-Authoritative Information"),
|
||||
NO_CONTENT = HttpResponseStatusInfo(204, "No Content"),
|
||||
RESET_CONTENT = HttpResponseStatusInfo(205, "Reset Content"),
|
||||
PARTIAL_CONTENT = HttpResponseStatusInfo(206, "Partial Content"),
|
||||
MULTI_STATUS = HttpResponseStatusInfo(207, "Multi-Status"),
|
||||
ALREADY_REPORTED = HttpResponseStatusInfo(208, "Already Reported"),
|
||||
IM_USED = HttpResponseStatusInfo(226, "IM Used"),
|
||||
|
||||
// Redirection
|
||||
MULTIPLE_CHOICES = HttpResponseStatusInfo(300, "Multiple Choices"),
|
||||
MOVED_PERMANENTLY = HttpResponseStatusInfo(301, "Moved Permanently"),
|
||||
FOUND = HttpResponseStatusInfo(302, "Found"),
|
||||
SEE_OTHER = HttpResponseStatusInfo(303, "See Other"),
|
||||
NOT_MODIFIED = HttpResponseStatusInfo(304, "Not Modified"),
|
||||
TEMPORARY_REDIRECT = HttpResponseStatusInfo(307, "Temporary Redirect"),
|
||||
PERMANENT_REDIRECT = HttpResponseStatusInfo(308, "Permanent Redirect"),
|
||||
|
||||
// Client error
|
||||
BAD_REQUEST = HttpResponseStatusInfo(400, "Bad Request"),
|
||||
UNAUTHORIZED = HttpResponseStatusInfo(401, "Unauthorized"),
|
||||
PAYMENT_REQUIRED = HttpResponseStatusInfo(402, "Payment Required"),
|
||||
FORBIDDEN = HttpResponseStatusInfo(403, "Forbidden"),
|
||||
NOT_FOUND = HttpResponseStatusInfo(404, "Not Found"),
|
||||
METHOD_NOT_ALLOWED = HttpResponseStatusInfo(405, "Method Not Allowed"),
|
||||
NOT_ACCEPTABLE = HttpResponseStatusInfo(406, "Not Acceptable"),
|
||||
PROXY_AUTHENTICATION_REQUIRED = HttpResponseStatusInfo(407, "Proxy Authentication Required"),
|
||||
REQUEST_TIMEOUT = HttpResponseStatusInfo(408, "Request Timeout"),
|
||||
CONFLICT = HttpResponseStatusInfo(409, "Conflict"),
|
||||
GONE = HttpResponseStatusInfo(410, "Gone"),
|
||||
LENGTH_REQUIRED = HttpResponseStatusInfo(411, "Length Required"),
|
||||
PRECONDITION_FAILED = HttpResponseStatusInfo(412, "Precondition Failed"),
|
||||
PAYLOAD_TOO_LARGE = HttpResponseStatusInfo(413, "Payload Too Large"),
|
||||
URI_TOO_LONG = HttpResponseStatusInfo(414, "URI Too Long"),
|
||||
UNSUPPORTED_MEDIA_TYPE = HttpResponseStatusInfo(415, "Unsupported Media Type"),
|
||||
RANGE_NOT_SATISFIABLE = HttpResponseStatusInfo(416, "Range Not Satisfiable"),
|
||||
EXPECTATION_FAILED = HttpResponseStatusInfo(417, "Expectation Failed"),
|
||||
IM_A_TEAPOT = HttpResponseStatusInfo(418, "I'm a teapot"),
|
||||
MISDIRECTED_REQUEST = HttpResponseStatusInfo(421, "Misdirected Request"),
|
||||
UNPROCESSABLE_CONTENT = HttpResponseStatusInfo(422, "Unprocessable Content"),
|
||||
LOCKED = HttpResponseStatusInfo(423, "Locked"),
|
||||
FAILED_DEPENDENCY = HttpResponseStatusInfo(424, "Failed Dependency"),
|
||||
TOO_EARLY = HttpResponseStatusInfo(425, "Too Early"),
|
||||
UPGRADE_REQUIRED = HttpResponseStatusInfo(426, "Upgrade Required"),
|
||||
PRECONDITION_REQUIRED = HttpResponseStatusInfo(428, "Precondition Required"),
|
||||
TOO_MANY_REQUESTS = HttpResponseStatusInfo(429, "Too Many Requests"),
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE = HttpResponseStatusInfo(431, "Request Header Fields Too Large"),
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS = HttpResponseStatusInfo(451, "Unavailable For Legal Reasons"),
|
||||
|
||||
// Server error
|
||||
INTERNAL_SERVER_ERROR = HttpResponseStatusInfo(500, "Internal Server Error"),
|
||||
NOT_IMPLEMENTED = HttpResponseStatusInfo(501, "Not Implemented"),
|
||||
BAD_GATEWAY = HttpResponseStatusInfo(502, "Bad Gateway"),
|
||||
SERVICE_UNAVAILABLE = HttpResponseStatusInfo(503, "Service Unavailable"),
|
||||
GATEWAY_TIMEOUT = HttpResponseStatusInfo(504, "Gateway Timeout"),
|
||||
HTTP_VERSION_NOT_SUPPORTED = HttpResponseStatusInfo(505, "HTTP Version Not Supported"),
|
||||
VARIANT_ALSO_NEGOTIATES = HttpResponseStatusInfo(506, "Variant Also Negotiates"),
|
||||
INSUFFICIENT_STORAGE = HttpResponseStatusInfo(507, "Insufficient Storage"),
|
||||
LOOP_DETECTED = HttpResponseStatusInfo(508, "Loop Detected"),
|
||||
NOT_EXTENDED = HttpResponseStatusInfo(510, "Not Extended"),
|
||||
NETWORK_AUTHENTICATION_REQUIRED = HttpResponseStatusInfo(511, "Network Authentication Required")
|
||||
}
|
Loading…
Reference in New Issue