diff --git a/source/handy_http_primitives/handler.d b/source/handy_http_primitives/handler.d index 44ab86d..c211f22 100644 --- a/source/handy_http_primitives/handler.d +++ b/source/handy_http_primitives/handler.d @@ -3,6 +3,10 @@ module handy_http_primitives.handler; import handy_http_primitives.request; import handy_http_primitives.response; +/** + * Defines the request handler interface, which is called upon to handle an + * incoming HTTP request. + */ interface HttpRequestHandler { - void handle(in HttpRequest request, ref HttpResponse response); -} \ No newline at end of file + void handle(ref ServerHttpRequest request, ref ServerHttpResponse response); +} diff --git a/source/handy_http_primitives/package.d b/source/handy_http_primitives/package.d index 5b1b3bf..b8ef78d 100644 --- a/source/handy_http_primitives/package.d +++ b/source/handy_http_primitives/package.d @@ -1,2 +1,7 @@ module handy_http_primitives; +public import handy_http_primitives.request; +public import handy_http_primitives.response; +public import handy_http_primitives.handler; +public import handy_http_primitives.optional; +public import handy_http_primitives.multivalue_map; diff --git a/source/handy_http_primitives/request.d b/source/handy_http_primitives/request.d index dc572e6..f758c81 100644 --- a/source/handy_http_primitives/request.d +++ b/source/handy_http_primitives/request.d @@ -1,25 +1,47 @@ module handy_http_primitives.request; -import streams; +import streams : InputStream; +import std.traits : isSomeString, EnumMembers; import handy_http_primitives.multivalue_map; +import handy_http_primitives.optional; -struct HttpRequest { - const ubyte httpVersion = 1; - const Method method = Method.GET; - const string url = ""; +/** + * The HTTP request struct which represents the content of an HTTP request as + * received by a server. + */ +struct ServerHttpRequest { + /// The HTTP version of the request. + const HttpVersion httpVersion = HttpVersion.V1_1; + /// The HTTP verb used in the request. + const HttpMethod method = HttpMethod.GET; + /// The URL that was requested. + const(char[]) url = ""; + /// A case-insensitive map of all request headers. const(CaseInsensitiveStringMultiValueMap) headers; + /// A case-sensitive map of all URL query parameters. const(StringMultiValueMap) queryParams; + /// The underlying stream used to read the body from the request. InputStream!ubyte inputStream; } +/** + * Enumeration of all possible HTTP request versions, as an unsigned byte for + * efficient storage. + */ +public enum HttpVersion : ubyte { + V1_1 = 1 << 1, + V2 = 1 << 2, + V3 = 1 << 3 +} + /** * 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 { +public enum HttpMethod : ushort { GET = 1 << 0, HEAD = 1 << 1, POST = 1 << 2, @@ -30,3 +52,33 @@ public enum Method : ushort { TRACE = 1 << 7, PATCH = 1 << 8 } + +/** + * Attempts to parse an HttpMethod from a string. + * Params: + * s = The string to parse. + * Returns: An optional which may contain an HttpMethod, if one was parsed. + */ +Optional!HttpMethod parseHttpMethod(S)(S s) if (isSomeString!S) { + import std.uni : toUpper; + import std.string : strip; + import std.conv : to; + static foreach (m; EnumMembers!HttpMethod) { + if (s == to!string(m)) return Optional!HttpMethod.of(m); + } + const cleanStr = strip(toUpper(s)); + static foreach (m; EnumMembers!HttpMethod) { + if (cleanStr == to!string(m)) return Optional!HttpMethod.of(m); + } + return Optional!HttpMethod.empty; +} + +unittest { + alias R = Optional!HttpMethod; + assert(parseHttpMethod("GET") == R.of(HttpMethod.GET)); + assert(parseHttpMethod("get") == R.of(HttpMethod.GET)); + assert(parseHttpMethod(" geT ") == R.of(HttpMethod.GET)); + assert(parseHttpMethod("PATCH") == R.of(HttpMethod.PATCH)); + assert(parseHttpMethod(" not a method!") == R.empty); + assert(parseHttpMethod("") == R.empty); +} diff --git a/source/handy_http_primitives/response.d b/source/handy_http_primitives/response.d index b622a22..a787232 100644 --- a/source/handy_http_primitives/response.d +++ b/source/handy_http_primitives/response.d @@ -4,9 +4,16 @@ import streams; import handy_http_primitives.multivalue_map; -struct HttpResponse { +/** + * The response that's sent by a server back to a client after processing the + * client's HTTP request. + */ +struct ServerHttpResponse { + /// The response status. StatusInfo status = HttpStatus.OK; + /// A multi-valued map containing all headers. StringMultiValueMap headers; + /// The stream to which the response body is written. OutputStream!ubyte outputStream; } @@ -24,73 +31,73 @@ struct StatusInfo { */ enum HttpStatus : StatusInfo { // Information - CONTINUE = StatusInfo(100, "Continue"), - SWITCHING_PROTOCOLS = StatusInfo(101, "Switching Protocols"), - PROCESSING = StatusInfo(102, "Processing"), - EARLY_HINTS = StatusInfo(103, "Early Hints"), + 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"), + 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"), + 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"), + 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"), + 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"), + 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") }