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 }