From 799da3ff340760ccb503eefb22255dfb5fa86e98 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Wed, 26 Mar 2025 14:31:18 -0400 Subject: [PATCH] Added builder module, made client address base types non-const. --- source/handy_http_primitives/address.d | 18 +-- source/handy_http_primitives/builder.d | 158 +++++++++++++++++++++++++ source/handy_http_primitives/package.d | 1 + 3 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 source/handy_http_primitives/builder.d diff --git a/source/handy_http_primitives/address.d b/source/handy_http_primitives/address.d index 1335edc..546fef8 100644 --- a/source/handy_http_primitives/address.d +++ b/source/handy_http_primitives/address.d @@ -8,8 +8,8 @@ module handy_http_primitives.address; * that the connection was assigned to. */ struct IPv4InternetAddress { - const ubyte[4] bytes; - const ushort port; + ubyte[4] bytes; + ushort port; } /** @@ -17,8 +17,8 @@ struct IPv4InternetAddress { * machine that the connection was assigned to. */ struct IPv6InternetAddress { - const ubyte[16] bytes; - const ushort port; + ubyte[16] bytes; + ushort port; } /** @@ -26,7 +26,7 @@ struct IPv6InternetAddress { * IO operations take place. */ struct UnixSocketAddress { - const string path; + string path; } /// Defines the different possible address types, used by `ClientAddress`. @@ -42,10 +42,10 @@ enum ClientAddressType { * request. Use `type` to determine which information is available. */ struct ClientAddress { - const ClientAddressType type; - const IPv4InternetAddress ipv4InternetAddress; - const IPv6InternetAddress ipv6InternetAddress; - const UnixSocketAddress unixSocketAddress; + ClientAddressType type; + IPv4InternetAddress ipv4InternetAddress; + IPv6InternetAddress ipv6InternetAddress; + UnixSocketAddress unixSocketAddress; /** * Serializes this address in a human-readable string representation. diff --git a/source/handy_http_primitives/builder.d b/source/handy_http_primitives/builder.d new file mode 100644 index 0000000..14a40ee --- /dev/null +++ b/source/handy_http_primitives/builder.d @@ -0,0 +1,158 @@ +/** + * Defines builder types to more easily construct various HTTP objects, often + * useful for testing scenarios. + */ +module handy_http_primitives.builder; + +import streams; + +import handy_http_primitives.request; +import handy_http_primitives.response; +import handy_http_primitives.address; +import handy_http_primitives.multivalue_map; + +/** + * Fluent interface for building ServerHttpRequest objects. + */ +struct ServerHttpRequestBuilder { + HttpVersion httpVersion = HttpVersion.V1; + ClientAddress clientAddress = ClientAddress.unknown; + string method = HttpMethod.GET; + string url = ""; + string[][string] headers; + QueryParameter[] queryParams; + InputStream!ubyte inputStream = inputStreamObjectFor(NoOpInputStream!ubyte()); + + ref withVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + ref withClientAddress(ClientAddress addr) { + this.clientAddress = addr; + return this; + } + + ref withMethod(string method) { + this.method = method; + return this; + } + + ref withUrl(string url) { + this.url = url; + return this; + } + + ref withHeader(string headerName, string value) { + if (headerName !in this.headers) { + this.headers[headerName] = []; + } + this.headers[headerName] ~= value; + return this; + } + + ref withQueryParam(string paramName, string value) { + foreach (ref param; this.queryParams) { + if (param.key == paramName) { + param.values ~= value; + return this; + } + } + this.queryParams ~= QueryParameter(paramName, [value]); + return this; + } + + ref withInputStream(S)(S stream) if (isByteInputStream!S) { + this.inputStream = inputStreamObjectFor(stream); + return this; + } + + ref withBody(ubyte[] bodyBytes) { + return withInputStream(arrayInputStreamFor(bodyBytes)); + } + + ref withBody(string bodyStr) { + return withBody(cast(ubyte[]) bodyStr); + } + + ServerHttpRequest build() { + return ServerHttpRequest( + httpVersion, + clientAddress, + method, + url, + headers, + queryParams, + inputStream + ); + } +} + +unittest { + ServerHttpRequest r1 = ServerHttpRequestBuilder() + .withUrl("/test-url") + .withVersion(HttpVersion.V2) + .withMethod(HttpMethod.PATCH) + .withBody("Hello world!") + .withClientAddress(ClientAddress.ofUnixSocket(UnixSocketAddress("/tmp/socket"))) + .withHeader("Content-Type", "text/plain") + .withHeader("Content-Length", "12") + .withQueryParam("idx", "42") + .build(); + assert(r1.httpVersion == HttpVersion.V2); + assert(r1.url == "/test-url"); + assert(r1.method == HttpMethod.PATCH); + string r1Body = r1.readBodyAsString(); + assert(r1Body == "Hello world!"); + assert(r1.clientAddress.type == ClientAddressType.UNIX); + assert(r1.clientAddress.unixSocketAddress.path == "/tmp/socket"); + assert(r1.getHeaderAs!string("Content-Type") == "text/plain"); + assert(r1.getHeaderAs!ulong("Content-Length") == 12); + assert(r1.getParamAs!ulong("idx") == 42); +} + +/** + * Fluent interface for building ServerHttpResponse objects. + */ +struct ServerHttpResponseBuilder { + StatusInfo initialStatus = HttpStatus.OK; + StringMultiValueMap initialHeaders; + OutputStream!ubyte outputStream = outputStreamObjectFor(NoOpOutputStream!ubyte()); + + ref withStatus(StatusInfo status) { + this.initialStatus = status; + return this; + } + + ref withHeader(string headerName, string value) { + this.initialHeaders.add(headerName, value); + return this; + } + + ref withOutputStream(S)(S stream) if (isByteOutputStream!S) { + this.outputStream = outputStreamObjectFor(stream); + return this; + } + + ServerHttpResponse build() { + return ServerHttpResponse( + initialStatus, + initialHeaders, + outputStream + ); + } +} + +unittest { + ArrayOutputStream!ubyte bufferOut = byteArrayOutputStream(); + ServerHttpResponse r1 = ServerHttpResponseBuilder() + .withStatus(HttpStatus.BAD_REQUEST) + .withHeader("Test", "okay") + .withOutputStream(&bufferOut) + .build(); + + assert(r1.status == HttpStatus.BAD_REQUEST); + assert(r1.headers.getFirst("Test").value == "okay"); + r1.outputStream.writeToStream(cast(ubyte[]) "Hello world!"); + assert(bufferOut.toArray() == cast(ubyte[]) "Hello world!"); +} diff --git a/source/handy_http_primitives/package.d b/source/handy_http_primitives/package.d index b8ef78d..10b28fe 100644 --- a/source/handy_http_primitives/package.d +++ b/source/handy_http_primitives/package.d @@ -5,3 +5,4 @@ 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; +public import handy_http_primitives.builder;