Added filtered handler.

This commit is contained in:
Andrew Lalis 2025-07-07 22:40:29 -04:00
parent 75df2d17df
commit 555bcf54c6
3 changed files with 204 additions and 0 deletions

19
.gitea/workflows/ci.yaml Normal file
View File

@ -0,0 +1,19 @@
name: Build and Test Module
on:
push:
paths:
- 'source/**'
- '.gitea/workflows/ci.yaml'
pull_request:
types: [opened, reopened, synchronize]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup DLang
uses: dlang-community/setup-dlang@v2
with:
compiler: ldc-latest
- name: Build and Test
run: dub -q test

View File

@ -0,0 +1,176 @@
/**
* Defines a "filtered" request handler, that applies an ordered set of filters
* (otherwise known as a "filter chain") before and after handling a request,
* as a means of adding a simple middleware layer to HTTP request processing.
*/
module handy_http_handlers.filtered_handler;
import handy_http_primitives;
/**
* A filter that can be applied to an HTTP request. If the filter determines
* that it's okay to continue processing the request, it should call
* `filterChain.doFilter(request, response)` to continue the chain. If the
* chain is not continued, request processing ends at this filter, and the
* current response is sent back to the client.
*/
interface HttpRequestFilter {
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain);
}
/**
* A filter chain is a singly-linked list that represents a series of filters
* to be applied when processing a request.
*/
class FilterChain {
private HttpRequestFilter filter;
private FilterChain next;
this(HttpRequestFilter filter, FilterChain next) {
this.filter = filter;
this.next = next;
}
/**
* Applies this filter chain link's filter to the given request and
* response, and then if there's another link in the chain, calls it to
* apply its filter thereafter, and so on until the chain is complete or
* a filter has short-circuited without calling `filterChain.doFilter`.
* Params:
* request = The request.
* response = The response.
*/
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response) {
if (next !is null) {
filter.doFilter(request, response, next);
}
}
/**
* Builds a filter chain from a list of request filters.
* Params:
* filters = The filters to use to build the filter chain.
* Returns: The root of the filter chain that when called, executes for
* each of the filters provided.
*/
static FilterChain build(HttpRequestFilter[] filters) {
if (filters.length == 0) return null;
FilterChain root = new FilterChain(filters[0], null);
FilterChain currentLink = root;
for (size_t i = 1; i < filters.length; i++) {
currentLink.next = new FilterChain(filters[i], null);
currentLink = currentLink.next;
}
// Add an "end cap" to the chain to make sure the last filter gets called.
currentLink.next = new FilterChain(null, null);
return root;
}
}
unittest {
assert(FilterChain.build([]) is null);
class SimpleFilter : HttpRequestFilter {
int id;
bool shortCircuit;
this(int id, bool shortCircuit = false) {
this.id = id;
this.shortCircuit = shortCircuit;
}
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
import std.conv : to;
response.headers.add("filter-" ~ id.to!string, id.to!string);
if (!shortCircuit) filterChain.doFilter(request, response);
}
}
// Test that all filters are applied.
FilterChain fc = FilterChain.build([
new SimpleFilter(1),
new SimpleFilter(2),
new SimpleFilter(3)
]);
ServerHttpRequest request = ServerHttpRequestBuilder().build();
ServerHttpResponse response = ServerHttpResponseBuilder().build();
fc.doFilter(request, response);
assert(response.headers.contains("filter-1"));
assert(response.headers.contains("filter-2"));
assert(response.headers.contains("filter-3"));
// Test that if we short-circuit, any further filters are NOT applied.
FilterChain fc2 = FilterChain.build([
new SimpleFilter(1),
new SimpleFilter(2, true),
new SimpleFilter(3)
]);
ServerHttpRequest request2 = ServerHttpRequestBuilder().build();
ServerHttpResponse response2 = ServerHttpResponseBuilder().build();
fc2.doFilter(request2, response2);
assert(response2.headers.contains("filter-1"));
assert(response2.headers.contains("filter-2"));
assert(!response2.headers.contains("filter-3"));
}
/**
* A simple base filter that should always sit at the bottom of the filter
* chain, which just calls a request handler.
*/
class BaseHandlerRequestFilter : HttpRequestFilter {
/// The request handler that'll be called.
private HttpRequestHandler handler;
this(HttpRequestHandler handler) {
this.handler = handler;
}
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
handler.handle(request, response);
// Don't call filterChain.doFilter because this is always the last part of the filter chain.
}
}
/**
* The FilteredHandler is a request handler you can add to your server to apply
* a filter chain to an underlying request handler.
*/
class FilteredHandler : HttpRequestHandler {
/// The internal filter chain that this handler calls.
private FilterChain filterChain;
/**
* Constructs a filtered handler that applies the given filter chain. Note
* that you should probabconstly use the other constructor for most cases, but
* if you really want to provide a custom filter chain, you'll most likely
* want to add the `BaseHandlerRequestFilter` as the last one in the chain.
* Params:
* filterChain = The filter chain to use.
*/
this(FilterChain filterChain) {
this.filterChain = filterChain;
}
/**
* Constructs a filtered handler that applies the given set of filters, in
* order, before potentially calling the given base handler.
* Params:
* filters = The set of filters to apply to all requests.
* baseHandler = The base handler that'll be called if an incoming
* request is passed successfully through all filters.
*/
this(HttpRequestFilter[] filters, HttpRequestHandler baseHandler) {
HttpRequestFilter[] allFilters = filters ~ [cast(HttpRequestFilter) new BaseHandlerRequestFilter(baseHandler)];
this.filterChain = FilterChain.build(allFilters);
}
/**
* Handles an incoming request by simply calling the filter chain on it.
* Params:
* request = The request.
* response = The response.
*/
void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) {
filterChain.doFilter(request, response);
}
}

View File

@ -1,9 +1,18 @@
/**
* Defines a path-matching request handler, that will match incoming requests
* against a set of mappings based on the URL and HTTP method, and call a
* handler depending on what's matched.
*/
module handy_http_handlers.path_handler;
import handy_http_primitives;
import path_matcher;
import slf4d;
/**
* The key used to read and write path-handler related data to the request's
* context data attribute.
*/
private immutable REQUEST_CONTEXT_DATA_KEY = "pathHandler";
/// Internal struct holding details about a handler mapping.