1 module handy_httpd.handlers.path_delegating_handler; 2 3 import std.stdio; 4 5 import handy_httpd.handler; 6 import handy_httpd.request; 7 import handy_httpd.response; 8 import handy_httpd.responses; 9 10 /** 11 * A request handler that delegates handling of requests to other handlers, 12 * based on a configured Ant-style path pattern. 13 */ 14 class PathDelegatingHandler : HttpRequestHandler { 15 /** 16 * The associative array that maps path patterns to request handlers. 17 */ 18 private HttpRequestHandler[string] handlers; 19 20 this(HttpRequestHandler[string] handlers = null) { 21 this.handlers = handlers; 22 } 23 24 /** 25 * Adds a new path/handler to this delegating handler. 26 * Params: 27 * path = The path pattern to match against requests. 28 * handler = The handler that will handle requests to the given path. 29 * Returns: This handler, for method chaining. 30 */ 31 public PathDelegatingHandler addPath(string path, HttpRequestHandler handler) { 32 this.handlers[path] = handler; 33 return this; 34 } 35 36 void handle(ref HttpRequest request, ref HttpResponse response) { 37 foreach (pattern, handler; handlers) { 38 if (pathMatches(pattern, request.url)) { 39 if (request.server.isVerbose()) { 40 writefln!"Found matching handler for url %s (pattern: %s)"(request.url, pattern); 41 } 42 request.pathParams = parsePathParams(pattern, request.url); 43 handler.handle(request, response); 44 return; // Exit once we handle the request. 45 } 46 } 47 if (request.server.isVerbose()) { 48 writefln!"No matching handler found for url %s"(request.url); 49 } 50 response.notFound(); 51 } 52 } 53 54 unittest { 55 import handy_httpd.server; 56 import handy_httpd.responses; 57 import core.thread; 58 59 auto handler = new PathDelegatingHandler() 60 .addPath("/home", simpleHandler((ref request, ref response) {response.okResponse();})) 61 .addPath("/users", simpleHandler((ref request, ref response) {response.okResponse();})) 62 .addPath("/users/{id}", simpleHandler((ref request, ref response) {response.okResponse();})); 63 64 HttpServer server = new HttpServer(handler).setVerbose(true); 65 new Thread(() {server.start();}).start(); 66 while (!server.isReady()) Thread.sleep(msecs(10)); 67 68 import std.net.curl; 69 import std.string; 70 import std.exception; 71 72 assert(get("http://localhost:8080/home") == ""); 73 assert(get("http://localhost:8080/home/") == ""); 74 assert(get("http://localhost:8080/users") == ""); 75 assert(get("http://localhost:8080/users/andrew") == ""); 76 assertThrown!CurlException(get("http://localhost:8080/not-home")); 77 assertThrown!CurlException(get("http://localhost:8080/users/andrew/data")); 78 79 server.stop(); 80 } 81 82 /** 83 * Checks if a url matches an Ant-style path pattern. We do this by doing some 84 * pre-processing on the pattern to convert it to a regular expression that can 85 * be matched against the given url. 86 * Params: 87 * pattern = The url pattern to check for a match with. 88 * url = The url to check against. 89 * Returns: True if the given url matches the pattern, or false otherwise. 90 */ 91 private bool pathMatches(string pattern, string url) { 92 import std.regex; 93 auto multiSegmentRegex = ctRegex!(`\*\*`); 94 auto singleSegmentRegex = ctRegex!(`(?<!\.)\*`); 95 auto singleCharRegex = ctRegex!(`\?`); 96 auto pathParamRegex = ctRegex!(`\{[^/]+\}`); 97 98 string s = pattern.replaceAll(multiSegmentRegex, ".*") 99 .replaceAll(singleSegmentRegex, "[^/]+") 100 .replaceAll(singleCharRegex, "[^/]") 101 .replaceAll(pathParamRegex, "[^/]+"); 102 Captures!string c = matchFirst(url, s); 103 return !c.empty() && c.front() == url; 104 } 105 106 unittest { 107 assert(pathMatches("/**", "/help")); 108 assert(pathMatches("/**", "/")); 109 assert(pathMatches("/*", "/help")); 110 assert(pathMatches("/help", "/help")); 111 assert(pathMatches("/help/*", "/help/other")); 112 assert(pathMatches("/help/**", "/help/other")); 113 assert(pathMatches("/help/**", "/help/other/another")); 114 assert(pathMatches("/?elp", "/help")); 115 assert(pathMatches("/**/test", "/hello/world/test")); 116 assert(pathMatches("/users/{id}", "/users/1")); 117 118 assert(!pathMatches("/help", "/Help")); 119 assert(!pathMatches("/help", "/help/other")); 120 assert(!pathMatches("/*", "/")); 121 assert(!pathMatches("/help/*", "/help/other/other")); 122 assert(!pathMatches("/users/{id}", "/users")); 123 assert(!pathMatches("/users/{id}", "/users/1/2/3")); 124 } 125 126 /** 127 * Parses a set of named path parameters from a url, according to a given path 128 * pattern where named path parameters are indicated by curly braces. 129 * 130 * For example, the pattern string "/users/{id}" can be used to parse the url 131 * "/users/123" to obtain ["id": "123"] as the path parameters. 132 * Params: 133 * pattern = The path pattern to use to parse params from. 134 * url = The url to parse parameters from. 135 * Returns: An associative array containing the path parameters. 136 */ 137 private string[string] parsePathParams(string pattern, string url) { 138 import std.regex; 139 import std.container.dlist; 140 141 // First collect an ordered list of the names of all path parameters to look for. 142 auto pathParamRegex = ctRegex!(`\{([^/]+)\}`); 143 DList!string pathParams = DList!string(); 144 auto m = matchAll(pattern, pathParamRegex); 145 while (!m.empty) { 146 auto c = m.front(); 147 pathParams.insertFront(c[1]); 148 m.popFront(); 149 } 150 151 string[string] params; 152 153 // Now parse all path parameters in order, and add them to the array. 154 string preparedPathPattern = replaceAll(pattern, pathParamRegex, "([^/]+)"); 155 auto c = matchFirst(url, regex(preparedPathPattern)); 156 if (c.empty) return params; // If there's complete no matching, just exit. 157 c.popFront(); // Pop the first capture group, which contains the full match. 158 while (!c.empty) { 159 if (pathParams.empty()) break; 160 string expectedParamName = pathParams.back(); 161 pathParams.removeBack(); 162 params[expectedParamName] = c.front(); 163 c.popFront(); 164 } 165 return params; 166 } 167 168 unittest { 169 assert(parsePathParams("/users/{id}", "/users/1") == ["id": "1"]); 170 assert(parsePathParams("/users/{id}/{name}", "/users/123/andrew") == ["id": "123", "name": "andrew"]); 171 assert(parsePathParams("/users/{id}", "/users") == null); 172 assert(parsePathParams("/users", "/users") == null); 173 assert(parsePathParams("/{a}/b/{c}", "/one/b/two") == ["a": "one", "c": "two"]); 174 }