handlers/source/handy_http_handlers/path_handler.d

286 lines
10 KiB
D

module handy_http_handlers.path_handler;
import handy_http_primitives;
import path_matcher;
import slf4d;
private immutable REQUEST_CONTEXT_DATA_KEY = "pathHandler";
/// Internal struct holding details about a handler mapping.
private struct HandlerMapping {
/// The handler that will handle requests that match this mapping.
HttpRequestHandler handler;
/// A bitmask with bits enabled for the HTTP methods that this mapping matches to.
immutable ushort methodsMask;
/// A list of string patterns that this mapping matches to.
immutable(string[]) patterns;
}
/// Maps each HTTP method to a bit value, so we can use bit-masking for handler mappings.
immutable ushort[HttpMethod] HTTP_METHOD_BITS = [
HttpMethod.GET: 1 >> 0,
HttpMethod.HEAD: 1 >> 1,
HttpMethod.POST: 1 >> 2,
HttpMethod.PUT: 1 >> 3,
HttpMethod.DELETE: 1 >> 4,
HttpMethod.CONNECT: 1 >> 5,
HttpMethod.OPTIONS: 1 >> 6,
HttpMethod.TRACE: 1 >> 7,
HttpMethod.PATCH: 1 >> 8
];
/**
* Computes a bitmask from a list of HTTP methods.
* Params:
* methods = The methods to make a bitmask from.
* Returns: A bitmask that matches all the given methods.
*/
ushort methodMaskFromMethods(HttpMethod[] methods) {
ushort mask = 0;
foreach (method; methods) {
mask |= HTTP_METHOD_BITS[method];
}
return mask;
}
/**
* Gets a bitmask that matches all HTTP methods.
* Returns: The bitmask.
*/
ushort methodMaskFromAll() {
return ushort.max;
}
/**
* Context data that may be attached to a request to provide additional data
* from path matching results, like any path variables that were found.
*/
class PathHandlerContextData {
immutable PathParam[] params;
this(immutable(PathParam[]) params){
this.params = params;
}
}
/**
* Gets the set of path variables that were matched when the given request was
* handled by the path handler.
* Params:
* request = The request to get path variables for.
* Returns: The list of path variables.
*/
immutable(PathParam[]) getPathParams(in ServerHttpRequest request) {
if (REQUEST_CONTEXT_DATA_KEY in request.contextData) {
return (cast(PathHandlerContextData) request.contextData[REQUEST_CONTEXT_DATA_KEY]).params;
}
return [];
}
/**
* Gets a specific path variable's value.
* Params:
* request = The request to get the path variable value from.
* name = The name of the path variable.
* defaultValue = The default value to use if no path variables are present.
* Returns: The path variable's value.
*/
T getPathParamAs(T)(in ServerHttpRequest request, string name, T defaultValue = T.init) {
foreach (p; getPathParams(request)) {
if (p.name == name) {
return p.getAs!T;
}
}
return defaultValue;
}
/**
* A request handler that maps incoming requests to a particular handler based
* on the request's URL path and/or HTTP method (GET, POST, etc.).
*
* Use the various overloaded versions of the `addMapping(...)` method to add
* handlers to this path handler. When handling requests, this path handler
* will look for matches deterministically in the order you add them. Therefore,
* adding mappings with conflicting or duplicate paths will cause the first one
* to always be called.
*
* Path patterns should be defined according to the rules from the path-matcher
* library, found here: https://github.com/andrewlalis/path-matcher
*/
class PathHandler : HttpRequestHandler {
/// The internal list of all mapped handlers.
private HandlerMapping[] mappings;
/// The handler to use when no mapping is found for a request.
private HttpRequestHandler notFoundHandler;
/**
* Constructs a new path handler with initially no mappings, and a default
* notFoundHandler that simply sets a 404 status.
*/
this() {
this.mappings = [];
this.notFoundHandler = HttpRequestHandler.of((ref request, ref response) {
response.status = HttpStatus.NOT_FOUND;
});
}
/**
* Adds a mapping to this handler, such that requests which match the given
* method and pattern will be handed off to the given handler.
*
* Overloaded variations of this method are defined for your convenience,
* which allow you to add a mapping for multiple HTTP methods and/or path
* patterns.
*
* Params:
* method = The HTTP method to match against.
* pattern = The path pattern to match against. See https://github.com/andrewlalis/path-matcher
* for more details on the pattern's format.
* handler = The handler that will handle matching requests.
* Returns: This path handler, for method chaining.
*/
PathHandler addMapping(HttpMethod method, string pattern, HttpRequestHandler handler) {
this.mappings ~= HandlerMapping(handler, HTTP_METHOD_BITS[method], [pattern]);
return this;
}
///
PathHandler addMapping(HttpMethod[] methods, string pattern, HttpRequestHandler handler) {
this.mappings ~= HandlerMapping(handler, methodMaskFromMethods(methods), [pattern]);
return this;
}
///
PathHandler addMapping(HttpMethod method, string[] patterns, HttpRequestHandler handler) {
this.mappings ~= HandlerMapping(handler, HTTP_METHOD_BITS[method], patterns.idup);
return this;
}
///
PathHandler addMapping(HttpMethod[] methods, string[] patterns, HttpRequestHandler handler) {
this.mappings ~= HandlerMapping(handler, methodMaskFromMethods(methods), patterns.idup);
return this;
}
///
PathHandler addMapping(string pattern, HttpRequestHandler handler) {
this.mappings ~= HandlerMapping(handler, methodMaskFromAll(), [pattern]);
return this;
}
/**
* Sets the handler that will be called for requests that don't match any
* pre-configured mappings.
* Params:
* handler = The handler to use.
* Returns: This path handler, for method chaining.
*/
PathHandler setNotFoundHandler(HttpRequestHandler handler) {
if (handler is null) throw new Exception("Cannot set PathHandler's notFoundHandler to null.");
this.notFoundHandler = handler;
return this;
}
/**
* Handles a request by looking for a mapped handler whose method and pattern
* match the request's, and letting that handler handle the request. If no
* match is found, the notFoundHandler will take care of it.
* Params:
* request = The request.
* response = The response.
*/
void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) {
HttpRequestHandler mappedHandler = findMappedHandler(request);
if (mappedHandler !is null) {
mappedHandler.handle(request, response);
} else {
notFoundHandler.handle(request, response);
}
}
/**
* Finds the handler to use to handle a given request, using our list of
* pre-configured mappings.
* Params:
* request = The request to find a handler for.
* Returns: The handler that matches the request, or null if none is found.
*/
private HttpRequestHandler findMappedHandler(ref ServerHttpRequest request) {
ushort methodBit = HTTP_METHOD_BITS[request.method];
foreach (HandlerMapping mapping; mappings) {
if ((mapping.methodsMask & methodBit) > 0) {
foreach (string pattern; mapping.patterns) {
PathMatchResult result = matchPath(request.url, pattern);
if (result.matches) {
debugF!"Found matching handler for %s %s: %s via pattern \"%s\""(
request.method,
request.url,
mapping.handler,
pattern
);
request.contextData[REQUEST_CONTEXT_DATA_KEY] = new PathHandlerContextData(result.pathParams);
return mapping.handler;
}
}
}
}
debugF!("No handler found for %s %s.")(request.method, request.url);
return null;
}
}
// Test PathHandler.setNotFoundHandler
unittest {
import std.exception;
auto handler = new PathHandler();
assertThrown!Exception(handler.setNotFoundHandler(null));
auto notFoundHandler = HttpRequestHandler.of((ref request, ref response) {
response.status = HttpStatus.NOT_FOUND;
});
assertNotThrown!Exception(handler.setNotFoundHandler(notFoundHandler));
}
// Test PathHandler.handle
unittest {
class SimpleOkHandler : HttpRequestHandler {
void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) {
response.status = HttpStatus.OK;
}
}
PathHandler handler = new PathHandler()
.addMapping(HttpMethod.GET, "/home", new SimpleOkHandler())
.addMapping(HttpMethod.GET, "/users", new SimpleOkHandler())
.addMapping(HttpMethod.GET, "/users/:id:ulong", new SimpleOkHandler())
.addMapping(HttpMethod.GET, "/api/*", new SimpleOkHandler());
struct RequestAndResponse {
ServerHttpRequest request;
ServerHttpResponse response;
}
RequestAndResponse generateHandledData(HttpMethod method, string url) {
ServerHttpRequest request = ServerHttpRequestBuilder()
.withMethod(method)
.withUrl(url)
.build();
ServerHttpResponse response = ServerHttpResponseBuilder().build();
handler.handle(request, response);
return RequestAndResponse(request, response);
}
auto result1 = generateHandledData(HttpMethod.GET, "/home");
assert(result1.response.status == HttpStatus.OK);
auto result2 = generateHandledData(HttpMethod.GET, "/home-not-exists");
assert(result2.response.status == HttpStatus.NOT_FOUND);
auto result3 = generateHandledData(HttpMethod.GET, "/users");
assert(result3.response.status == HttpStatus.OK);
auto result4 = generateHandledData(HttpMethod.GET, "/users/34");
assert(result4.response.status == HttpStatus.OK);
assert(getPathParamAs!ulong(result4.request, "id") == 34);
auto result5 = generateHandledData(HttpMethod.GET, "/api/test");
assert(result5.response.status == HttpStatus.OK);
auto result6 = generateHandledData(HttpMethod.GET, "/api/test/bleh");
assert(result6.response.status == HttpStatus.NOT_FOUND);
auto result7 = generateHandledData(HttpMethod.GET, "/api");
assert(result7.response.status == HttpStatus.NOT_FOUND);
auto result8 = generateHandledData(HttpMethod.GET, "/");
assert(result8.response.status == HttpStatus.NOT_FOUND);
}