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 }