1 module skadi.core.router;
2 
3 import vibe.http.server;
4 import vibe.core.log;
5 import std.functional;
6 
7 version (VibeOldRouterImpl) {
8 	pragma(msg, "-version=VibeOldRouterImpl is deprecated and will be removed in the next release.");
9 }
10 
11 else version = VibeRouterTreeMatch;
12 
13 
14 /**
15 	An interface for HTTP request routers.
16 
17 	As of 0.7.24, the interface has been replaced with a deprecated alias to URLRouters.
18 	This will break any code relying on HTTPRouter being an interface, but won't
19 	break signatures.
20 
21 	Removal_notice:
22 
23 	Note that this is planned to be removed, due to interface/behavior considerations.
24 	In particular, the exact behavior of the router (most importantly, the route match
25 	string format) must be considered part of the interface. However, this removes the
26 	prime argument for having an interface in the first place.
27 */
28 deprecated("Will be removed in 0.7.25. See removal notice for more information.")
29 public alias HTTPRouter = URLRouters;
30 
31 /**
32 	Routes HTTP requests based on the request method and URL.
33 
34 	Routes are matched using a special URL match string that supports two forms
35 	of placeholders. See the sections below for more details.
36 
37 	Registered routes are matched according to the same sequence as initially
38 	specified using `match`, `get`, `post` etc. Matching ends as soon as a route
39 	handler writes a response using `res.writeBody()` or similar means. If no
40 	route matches or if no route handler writes a response, the router will
41 	simply not handle the request and the HTTP server will automatically
42 	generate a 404 error.
43 
44 	Match_patterns:
45 		Match patterns are character sequences that can optionally contain
46 		placeholders or raw wildcards ("*"). Raw wild cards match any character
47 		sequence, while placeholders match only sequences containing no slash
48 		("/") characters.
49 
50 		Placeholders are started using a colon (":") and are directly followed
51 		by their name. The first "/" character (or the end of the match string)
52 		denotes the end of the placeholder name. The part of the string that
53 		matches a placeholder will be stored in the `HTTPServerRequest.params`
54 		map using the placeholder name as the key.
55 
56 		Match strings are subject to the following rules:
57 		$(UL
58 			$(LI A raw wildcard ("*") may only occur at the end of the match string)
59 			$(LI At least one character must be placed between any two placeholders or wildcards)
60 			$(LI The maximum allowed number of placeholders in a single match string is 64)
61 		)
62 
63 	Match_String_Examples:
64 		$(UL
65 			$(LI `"/foo/bar"` matches only `"/foo/bar"` itself)
66 			$(LI `"/foo/*"` matches `"/foo/"`, `"/foo/bar"`, `"/foo/bar/baz"` or _any other string beginning with `"/foo/"`)
67 			$(LI `"/:x/"` matches `"/foo/"`, `"/bar/"` and similar strings (and stores `"foo"`/`"bar"` in `req.params["x"]`), but not `"/foo/bar/"`)
68 			$(LI Matching partial path entries with wildcards is possible: `"/foo:x"` matches `"/foo"`, `"/foobar"`, but not `"/foo/bar"`)
69 			$(LI Multiple placeholders and raw wildcards can be combined: `"/:x/:y/*"`)
70 		)
71 */
72 final class URLRouters : HTTPServerRequestHandler
73 {
74 	private {
75 		version (VibeRouterTreeMatch) MatchTree!Route m_routes;
76 		else Route[] m_routes;
77 		string m_prefix;
78 	}
79 
80 	this(string prefix = null)
81 	{
82 		m_prefix = prefix;
83 	}
84 
85 	@property string prefix() const
86     {
87         return m_prefix;
88     }
89 
90 	/// Returns a single route handle to conveniently register multiple methods.
91 	URLRoute route(string path)
92 	in { assert(path.length, "Cannot register null or empty path!"); }
93 	body { return URLRoute(this, path); }
94 
95 	/// Adds a new route for requests matching the specified HTTP method and pattern.
96 	URLRouters match(HTTPMethod method, string path, HTTPServerRequestDelegate cb)
97 	in { assert(path.length, "Cannot register null or empty path!"); }
98 	body {
99 		import std.algorithm;
100 		assert(count(path, ':') <= maxRouteParameters, "Too many route parameters");
101 		logDebug("add route %s %s", method, path);
102 		version (VibeRouterTreeMatch) m_routes.addTerminal(path, Route(method, path, cb));
103 		else m_routes ~= Route(method, path, cb);
104 		return this;
105 	}
106 	/// ditto
107 	URLRouters match(HTTPMethod method, string path, HTTPServerRequestHandler cb) { return match(method, path, &cb.handleRequest); }
108 	/// ditto
109 	URLRouters match(HTTPMethod method, string path, HTTPServerRequestFunction cb) { return match(method, path, toDelegate(cb)); }
110 	/// ditto
111 	URLRouters match(HTTPMethod method, string path, HTTPServerRequestDelegateS cb) { return match(method, path, cast(HTTPServerRequestDelegate)cb); }
112 	/// ditto
113 	URLRouters match(HTTPMethod method, string path, HTTPServerRequestHandlerS cb) { return match(method, path, &cb.handleRequest); }
114 	/// ditto
115 	URLRouters match(HTTPMethod method, string path, HTTPServerRequestFunctionS cb) { return match(method, path, toDelegate(cb)); }
116 
117 	/// Adds a new route for GET requests matching the specified pattern.
118 	URLRouters get(string url_match, HTTPServerRequestHandler cb) { return get(url_match, &cb.handleRequest); }
119 	/// ditto
120 	URLRouters get(string url_match, HTTPServerRequestFunction cb) { return get(url_match, toDelegate(cb)); }
121 	/// ditto
122 	URLRouters get(string url_match, HTTPServerRequestDelegate cb) { return match(HTTPMethod.GET, url_match, cb); }
123 	/// ditto
124 	URLRouters get(string url_match, HTTPServerRequestHandlerS cb) { return get(url_match, &cb.handleRequest); }
125 	/// ditto
126 	URLRouters get(string url_match, HTTPServerRequestFunctionS cb) { return get(url_match, toDelegate(cb)); }
127 	/// ditto
128 	URLRouters get(string url_match, HTTPServerRequestDelegateS cb) { return match(HTTPMethod.GET, url_match, cb); }
129 
130 	/// Adds a new route for POST requests matching the specified pattern.
131 	URLRouters post(string url_match, HTTPServerRequestHandler cb) { return post(url_match, &cb.handleRequest); }
132 	/// ditto
133 	URLRouters post(string url_match, HTTPServerRequestFunction cb) { return post(url_match, toDelegate(cb)); }
134 	/// ditto
135 	URLRouters post(string url_match, HTTPServerRequestDelegate cb) { return match(HTTPMethod.POST, url_match, cb); }
136 	/// ditto
137 	URLRouters post(string url_match, HTTPServerRequestHandlerS cb) { return post(url_match, &cb.handleRequest); }
138 	/// ditto
139 	URLRouters post(string url_match, HTTPServerRequestFunctionS cb) { return post(url_match, toDelegate(cb)); }
140 	/// ditto
141 	URLRouters post(string url_match, HTTPServerRequestDelegateS cb) { return match(HTTPMethod.POST, url_match, cb); }
142 
143 	/// Adds a new route for PUT requests matching the specified pattern.
144 	URLRouters put(string url_match, HTTPServerRequestHandler cb) { return put(url_match, &cb.handleRequest); }
145 	/// ditto
146 	URLRouters put(string url_match, HTTPServerRequestFunction cb) { return put(url_match, toDelegate(cb)); }
147 	/// ditto
148 	URLRouters put(string url_match, HTTPServerRequestDelegate cb) { return match(HTTPMethod.PUT, url_match, cb); }
149 	/// ditto
150 	URLRouters put(string url_match, HTTPServerRequestHandlerS cb) { return put(url_match, &cb.handleRequest); }
151 	/// ditto
152 	URLRouters put(string url_match, HTTPServerRequestFunctionS cb) { return put(url_match, toDelegate(cb)); }
153 	/// ditto
154 	URLRouters put(string url_match, HTTPServerRequestDelegateS cb) { return match(HTTPMethod.PUT, url_match, cb); }
155 
156 	/// Adds a new route for DELETE requests matching the specified pattern.
157 	URLRouters delete_(string url_match, HTTPServerRequestHandler cb) { return delete_(url_match, &cb.handleRequest); }
158 	/// ditto
159 	URLRouters delete_(string url_match, HTTPServerRequestFunction cb) { return delete_(url_match, toDelegate(cb)); }
160 	/// ditto
161 	URLRouters delete_(string url_match, HTTPServerRequestDelegate cb) { return match(HTTPMethod.DELETE, url_match, cb); }
162 	/// ditto
163 	URLRouters delete_(string url_match, HTTPServerRequestHandlerS cb) { return delete_(url_match, &cb.handleRequest); }
164 	/// ditto
165 	URLRouters delete_(string url_match, HTTPServerRequestFunctionS cb) { return delete_(url_match, toDelegate(cb)); }
166 	/// ditto
167 	URLRouters delete_(string url_match, HTTPServerRequestDelegateS cb) { return match(HTTPMethod.DELETE, url_match, cb); }
168 
169 	/// Adds a new route for PATCH requests matching the specified pattern.
170 	URLRouters patch(string url_match, HTTPServerRequestHandler cb) { return patch(url_match, &cb.handleRequest); }
171 	/// ditto
172 	URLRouters patch(string url_match, HTTPServerRequestFunction cb) { return patch(url_match, toDelegate(cb)); }
173 	/// ditto
174 	URLRouters patch(string url_match, HTTPServerRequestDelegate cb) { return match(HTTPMethod.PATCH, url_match, cb); }
175 	/// ditto
176 	URLRouters patch(string url_match, HTTPServerRequestHandlerS cb) { return patch(url_match, &cb.handleRequest); }
177 	/// ditto
178 	URLRouters patch(string url_match, HTTPServerRequestFunctionS cb) { return patch(url_match, toDelegate(cb)); }
179 	/// ditto
180 	URLRouters patch(string url_match, HTTPServerRequestDelegateS cb) { return match(HTTPMethod.PATCH, url_match, cb); }
181 
182 	/// Adds a new route for requests matching the specified pattern, regardless of their HTTP verb.
183 	URLRouters any(string url_match, HTTPServerRequestHandler cb) { return any(url_match, &cb.handleRequest); }
184 	/// ditto
185 	URLRouters any(string url_match, HTTPServerRequestFunction cb) { return any(url_match, toDelegate(cb)); }
186 	/// ditto
187 	URLRouters any(string url_match, HTTPServerRequestDelegate cb)
188 	{
189 		import std.traits;
190 		static HTTPMethod[] all_methods = [EnumMembers!HTTPMethod];
191 
192 		foreach(immutable method; all_methods)
193 			match(method, url_match, cb);
194 
195 		return this;
196 	}
197 	/// ditto
198 	URLRouters any(string url_match, HTTPServerRequestHandlerS cb) { return any(url_match, &cb.handleRequest); }
199 	/// ditto
200 	URLRouters any(string url_match, HTTPServerRequestFunctionS cb) { return any(url_match, toDelegate(cb)); }
201 	/// ditto
202 	URLRouters any(string url_match, HTTPServerRequestDelegateS cb) { return any(url_match, cast(HTTPServerRequestDelegate)cb); }
203 
204 
205 	/** Rebuilds the internal matching structures to account for newly added routes.
206 
207 		This should be used after a lot of routes have been added to the router, to
208 		force eager computation of the match structures. The alternative is to
209 		let the router lazily compute the structures when the first request happens,
210 		which can delay this request.
211 	*/
212 	void rebuild()
213 	{
214 		version (VibeRouterTreeMatch)
215 			m_routes.rebuildGraph();
216 	}
217 
218 	/// Handles a HTTP request by dispatching it to the registered route handlers.
219 	void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
220 	{
221 		auto method = req.method;
222 
223 		auto path = req.path;
224 		if (path.length < m_prefix.length || path[0 .. m_prefix.length] != m_prefix) return;
225 		path = path[m_prefix.length .. $];
226 
227 		version (VibeRouterTreeMatch) {
228 			while (true) {
229 				bool done = false;
230 				m_routes.match(path, (ridx, scope values) {
231 					if (done) return;
232 					auto r = &m_routes.getTerminalData(ridx);
233 					if (r.method == method) {
234 						logDebugV("route match: %s -> %s %s %s", req.path, r.method, r.pattern, values);
235 						// TODO: use a different map type that avoids allocations for small amounts of keys
236 						foreach (i, v; values) req.params[m_routes.getTerminalVarNames(ridx)[i]] = v;
237 						r.cb(req, res);
238 						done = res.headerWritten;
239 					}
240 				});
241 				if (done) return;
242 
243 				if (method == HTTPMethod.HEAD) method = HTTPMethod.GET;
244 				else break;
245 			}
246 		} else {
247 			while(true)
248 			{
249 				foreach (ref r; m_routes) {
250 					if (r.method == method && r.matches(path, req.params)) {
251 						logTrace("route match: %s -> %s %s", req.path, r.method, r.pattern);
252 						// .. parse fields ..
253 						r.cb(req, res);
254 						if (res.headerWritten) return;
255 					}
256 				}
257 				if (method == HTTPMethod.HEAD) method = HTTPMethod.GET;
258 				//else if (method == HTTPMethod.OPTIONS)
259 				else break;
260 			}
261 		}
262 
263 		logDebug("no route match: %s %s", req.method, req.requestURL);
264 	}
265 
266 	/// Returns all registered routes as const AA
267 	const(Route)[] getAllRoutes()
268 	{
269 		version (VibeRouterTreeMatch) {
270 			auto routes = new Route[m_routes.terminalCount];
271 			foreach (i, ref r; routes)
272 				r = m_routes.getTerminalData(i);
273 			return routes;
274 		} else return m_routes;
275 	}
276 }
277 
278 ///
279 unittest {
280 	import vibe.http.fileserver;
281 
282 	void addGroup(HTTPServerRequest req, HTTPServerResponse res)
283 	{
284 		// Route variables are accessible via the params map
285 		logInfo("Getting group %s for user %s.", req.params["groupname"], req.params["username"]);
286 	}
287 
288 	void deleteUser(HTTPServerRequest req, HTTPServerResponse res)
289 	{
290 		// ...
291 	}
292 
293 	void auth(HTTPServerRequest req, HTTPServerResponse res)
294 	{
295 		// TODO: check req.session to see if a user is logged in and
296 		//       write an error page or throw an exception instead.
297 	}
298 
299 	void setup()
300 	{
301 		auto router = new URLRouters;
302 		// Matches all GET requests for /users/*/groups/* and places
303 		// the place holders in req.params as 'username' and 'groupname'.
304 		router.get("/users/:username/groups/:groupname", &addGroup);
305 
306 		// Matches all requests. This can be useful for authorization and
307 		// similar tasks. The auth method will only write a response if the
308 		// user is _not_ authorized. Otherwise, the router will fall through
309 		// and continue with the following routes.
310 		router.any("*", &auth);
311 
312 		// Matches a POST request
313 		router.post("/users/:username/delete", &deleteUser);
314 
315 		// Matches all GET requests in /static/ such as /static/img.png or
316 		// /static/styles/sty.css
317 		router.get("/static/*", serveStaticFiles("public/"));
318 
319 		// Setup a HTTP server...
320 		auto settings = new HTTPServerSettings;
321 		// ...
322 
323 		// The router can be directly passed to the listenHTTP function as
324 		// the main request handler.
325 		listenHTTP(settings, router);
326 	}
327 }
328 
329 /** Using nested routers to map components to different sub paths. A component
330 	could for example be an embedded blog engine.
331 */
332 unittest {
333 	// some embedded component:
334 
335 	void showComponentHome(HTTPServerRequest req, HTTPServerResponse res)
336 	{
337 		// ...
338 	}
339 
340 	void showComponentUser(HTTPServerRequest req, HTTPServerResponse res)
341 	{
342 		// ...
343 	}
344 
345 	void registerComponent(URLRouters router)
346 	{
347 		router.get("/", &showComponentHome);
348 		router.get("/users/:user", &showComponentUser);
349 	}
350 
351 	// main application:
352 
353 	void showHome(HTTPServerRequest req, HTTPServerResponse res)
354 	{
355 		// ...
356 	}
357 
358 	void setup()
359 	{
360 		auto c1router = new URLRouters("/component1");
361 		registerComponent(c1router);
362 
363 		auto mainrouter = new URLRouters;
364 		mainrouter.get("/", &showHome);
365 		// forward all unprocessed requests to the component router
366 		mainrouter.any("*", c1router);
367 
368 		// now the following routes will be matched:
369 		// / -> showHome
370 		// /component1/ -> showComponentHome
371 		// /component1/users/:user -> showComponentUser
372 
373 		// Start the HTTP server
374 		auto settings = new HTTPServerSettings;
375 		// ...
376 		listenHTTP(settings, mainrouter);
377 	}
378 }
379 
380 unittest {
381 	import vibe.inet.url;
382 
383 	auto router = new URLRouters;
384 	string result;
385 	void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; }
386 	void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; }
387 	void c(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "x", "Wrong variable contents: "~req.params["test"]); result ~= "C"; }
388 	void d(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "y", "Wrong variable contents: "~req.params["test"]); result ~= "D"; }
389 	router.get("/test", &a);
390 	router.post("/test", &b);
391 	router.get("/a/:test", &c);
392 	router.get("/a/:test/", &d);
393 
394 	auto res = createTestHTTPServerResponse();
395 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/")), res);
396 	assert(result == "", "Matched for non-existent / path");
397 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res);
398 	assert(result == "A", "Didn't match a simple GET request");
399 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test"), HTTPMethod.POST), res);
400 	assert(result == "AB", "Didn't match a simple POST request");
401 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/"), HTTPMethod.GET), res);
402 	assert(result == "AB", "Matched empty variable. "~result);
403 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/x"), HTTPMethod.GET), res);
404 	assert(result == "ABC", "Didn't match a trailing 1-character var.");
405 	// currently fails due to Path not accepting "//"
406 	//router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a//"), HTTPMethod.GET), res);
407 	//assert(result == "ABC", "Matched empty string or slash as var. "~result);
408 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/y/"), HTTPMethod.GET), res);
409 	assert(result == "ABCD", "Didn't match 1-character infix variable.");
410 }
411 
412 unittest {
413 	import vibe.inet.url;
414 
415 	auto router = new URLRouters("/test");
416 
417 	string result;
418 	void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; }
419 	void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; }
420 	router.get("/x", &a);
421 	router.get("/y", &b);
422 
423 	auto res = createTestHTTPServerResponse();
424 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res);
425 	assert(result == "");
426 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/x")), res);
427 	assert(result == "A");
428 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/y")), res);
429 	assert(result == "AB");
430 }
431 
432 
433 /**
434 	Convenience abstraction for a single `URLRouters` route.
435 
436 	See `URLRouters.route` for a usage example.
437 */
438 struct URLRoute {
439 	URLRouters router;
440 	string path;
441 
442 	ref URLRoute get(Handler)(Handler h) { router.get(path, h); return this; }
443 	ref URLRoute post(Handler)(Handler h) { router.post(path, h); return this; }
444 	ref URLRoute put(Handler)(Handler h) { router.put(path, h); return this; }
445 	ref URLRoute delete_(Handler)(Handler h) { router.delete_(path, h); return this; }
446 	ref URLRoute patch(Handler)(Handler h) { router.patch(path, h); return this; }
447 	ref URLRoute any(Handler)(Handler h) { router.any(path, h); return this; }
448 	ref URLRoute match(Handler)(HTTPMethod method, Handler h) { router.match(method, path, h); return this; }
449 }
450 
451 
452 private enum maxRouteParameters = 64;
453 
454 private struct Route {
455 	HTTPMethod method;
456 	string pattern;
457 	HTTPServerRequestDelegate cb;
458 
459 	bool matches(string url, ref string[string] params)
460 	const {
461 		size_t i, j;
462 
463 		// store parameters until a full match is confirmed
464 		import std.typecons;
465 		Tuple!(string, string)[maxRouteParameters] tmpparams;
466 		size_t tmppparams_length = 0;
467 
468 		for (i = 0, j = 0; i < url.length && j < pattern.length;) {
469 			if (pattern[j] == '*') {
470 				foreach (t; tmpparams[0 .. tmppparams_length])
471 					params[t[0]] = t[1];
472 				return true;
473 			}
474 			if (url[i] == pattern[j]) {
475 				i++;
476 				j++;
477 			} else if(pattern[j] == ':') {
478 				j++;
479 				string name = skipPathNode(pattern, j);
480 				string match = skipPathNode(url, i);
481 				assert(tmppparams_length < maxRouteParameters, "Maximum number of route parameters exceeded.");
482 				tmpparams[tmppparams_length++] = tuple(name, match);
483 			} else return false;
484 		}
485 
486 		if ((j < pattern.length && pattern[j] == '*') || (i == url.length && j == pattern.length)) {
487 			foreach (t; tmpparams[0 .. tmppparams_length])
488 				params[t[0]] = t[1];
489 			return true;
490 		}
491 
492 		return false;
493 	}
494 }
495 
496 
497 private string skipPathNode(string str, ref size_t idx)
498 {
499 	size_t start = idx;
500 	while( idx < str.length && str[idx] != '/' ) idx++;
501 	return str[start .. idx];
502 }
503 
504 private string skipPathNode(ref string str)
505 {
506 	size_t idx = 0;
507 	auto ret = skipPathNode(str, idx);
508 	str = str[idx .. $];
509 	return ret;
510 }
511 
512 private struct MatchTree(T) {
513 	import std.algorithm : countUntil;
514 	import std.array : array;
515 
516 	private {
517 		struct Node {
518 			size_t terminalsStart; // slice into m_terminalTags
519 			size_t terminalsEnd;
520 			uint[ubyte.max+1] edges = uint.max; // character -> index into m_nodes
521 		}
522 		struct TerminalTag {
523 			size_t index; // index into m_terminals array
524 			size_t var; // index into Terminal.varNames/varValues or size_t.max
525 		}
526 		struct Terminal {
527 			string pattern;
528 			T data;
529 			string[] varNames;
530 			size_t[size_t] varMap; // maps node index to variable index
531 		}
532 		Node[] m_nodes; // all nodes as a single array
533 		TerminalTag[] m_terminalTags;
534 		Terminal[] m_terminals;
535 
536 		enum TerminalChar = 0;
537 		bool m_upToDate = false;
538 	}
539 
540 	@property size_t terminalCount() const { return m_terminals.length; }
541 
542 	void addTerminal(string pattern, T data)
543 	{
544 		m_terminals ~= Terminal(pattern, data, null, null);
545 		m_upToDate = false;
546 	}
547 
548 	void match(string text, scope void delegate(size_t terminal, scope string[] vars) del)
549 	{
550 		// lazily update the match graph
551 		if (!m_upToDate) rebuildGraph();
552 
553 		doMatch(text, del);
554 	}
555 
556 	const(string)[] getTerminalVarNames(size_t terminal) const { return m_terminals[terminal].varNames; }
557 	ref inout(T) getTerminalData(size_t terminal) inout { return m_terminals[terminal].data; }
558 
559 	void print()
560 	const {
561 		import std.algorithm : map;
562 		import std.array : join;
563 		import std.conv : to;
564 		import std.range : iota;
565 		import std.string : format;
566 
567 		logInfo("Nodes:");
568 		foreach (i, n; m_nodes) {
569 			logInfo("  %s %s", i, m_terminalTags[n.terminalsStart .. n.terminalsEnd]
570 				.map!(t => format("T%s%s", t.index, t.var != size_t.max ? "("~m_terminals[t.index].varNames[t.var]~")" : "")).join(" "));
571 			//logInfo("  %s %s-%s", i, n.terminalsStart, n.terminalsEnd);
572 
573 
574 			static string mapChar(ubyte ch) {
575 				if (ch == TerminalChar) return "$";
576 				if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch);
577 				if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch);
578 				if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch);
579 				if (ch == '/') return "/";
580 				if (ch == '^') return "^";
581 				return ch.to!string;
582 			}
583 
584 			void printRange(uint node, ubyte from, ubyte to)
585 			{
586 				if (to - from <= 10) logInfo("    %s -> %s", iota(from, cast(uint)to+1).map!(ch => mapChar(cast(ubyte)ch)).join("|"), node);
587 				else logInfo("    %s-%s -> %s", mapChar(from), mapChar(to), node);
588 			}
589 
590 			uint last_to = uint.max;
591 			ubyte last_ch = 0;
592 			foreach (ch, e; n.edges)
593 				if (e != last_to) {
594 					if (last_to != uint.max)
595 						printRange(last_to, last_ch, cast(ubyte)(ch-1));
596 					last_ch = cast(ubyte)ch;
597 					last_to = e;
598 				}
599 			if (last_to != uint.max)
600 				printRange(last_to, last_ch, ubyte.max);
601 		}
602 	}
603 
604 	private void doMatch(string text, scope void delegate(size_t terminal, scope string[] vars) del)
605 	const {
606 		string[maxRouteParameters] vars_buf = void;
607 
608 		import std.algorithm : canFind;
609 
610 		// first, determine the end node, if any
611 		auto n = matchTerminals(text);
612 		if (!n) return;
613 
614 		// then, go through the terminals and match their variables
615 		foreach (ref t; m_terminalTags[n.terminalsStart .. n.terminalsEnd]) {
616 			auto term = &m_terminals[t.index];
617 			auto vars = vars_buf[0 .. term.varNames.length];
618 			matchVars(vars, term, text);
619 			if (vars.canFind!(v => v.length == 0)) continue; // all variables must be non-empty to match
620 			del(t.index, vars);
621 		}
622 	}
623 
624 	private inout(Node)* matchTerminals(string text)
625 	inout {
626 		if (!m_nodes.length) return null;
627 
628 		auto n = &m_nodes[0];
629 
630 		// follow the path through the match graph
631 		foreach (i, char ch; text) {
632 			auto nidx = n.edges[cast(ubyte)ch];
633 			if (nidx == uint.max) return null;
634 			n = &m_nodes[nidx];
635 		}
636 
637 		// finally, find a matching terminal node
638 		auto nidx = n.edges[TerminalChar];
639 		if (nidx == uint.max) return null;
640 		n = &m_nodes[nidx];
641 		return n;
642 	}
643 
644 	private void matchVars(string[] dst, in Terminal* term, string text)
645 	const {
646 		auto nidx = 0;
647 		size_t activevar = size_t.max;
648 		size_t activevarstart;
649 
650 		dst[] = null;
651 
652 		// folow the path throgh the match graph
653 		foreach (i, char ch; text) {
654 			auto var = term.varMap[nidx];
655 
656 			// detect end of variable
657 			if (var != activevar && activevar != size_t.max) {
658 				dst[activevar] = text[activevarstart .. i-1];
659 				activevar = size_t.max;
660 			}
661 
662 			// detect beginning of variable
663 			if (var != size_t.max && activevar == size_t.max) {
664 				activevar = var;
665 				activevarstart = i;
666 			}
667 
668 			nidx = m_nodes[nidx].edges[cast(ubyte)ch];
669 			assert(nidx != uint.max);
670 		}
671 
672 		// terminate any active varible with the end of the input string or with the last character
673 		auto var = term.varMap[nidx];
674 		if (activevar != size_t.max) dst[activevar] = text[activevarstart .. (var == activevar ? $ : $-1)];
675 	}
676 
677 	private void rebuildGraph()
678 	{
679 		if (m_upToDate) return;
680 		m_upToDate = true;
681 
682 		m_nodes = null;
683 		m_terminalTags = null;
684 
685 		if (!m_terminals.length) return;
686 
687 		MatchGraphBuilder builder;
688 		foreach (i, ref t; m_terminals)
689 			t.varNames = builder.insert(t.pattern, i);
690 		//builder.print();
691 		builder.disambiguate();
692 
693 		auto nodemap = new uint[builder.m_nodes.length];
694 		nodemap[] = uint.max;
695 
696 		uint process(size_t n)
697 		{
698 			import std.algorithm : canFind;
699 
700 			if (nodemap[n] != uint.max) return nodemap[n];
701 			auto nmidx = cast(uint)m_nodes.length;
702 			nodemap[n] = nmidx;
703 			m_nodes.length++;
704 
705 			Node nn;
706 			nn.terminalsStart = m_terminalTags.length;
707 			foreach (t; builder.m_nodes[n].terminals) {
708 				auto var = t.var.length ? m_terminals[t.index].varNames.countUntil(t.var) : size_t.max;
709 				assert(!m_terminalTags[nn.terminalsStart .. $].canFind!(u => u.index == t.index && u.var == var));
710 				m_terminalTags ~= TerminalTag(t.index, var);
711 				m_terminals[t.index].varMap[nmidx] = var;
712 			}
713 			nn.terminalsEnd = m_terminalTags.length;
714 			foreach (e; builder.m_nodes[n].edges)
715 				nn.edges[e.ch] = process(e.to);
716 
717 			m_nodes[nmidx] = nn;
718 
719 			return nmidx;
720 		}
721 		assert(builder.m_nodes[0].edges.length == 1, "Graph must be disambiguated before purging.");
722 		process(builder.m_nodes[0].edges[0].to);
723 
724 		logDebug("Match tree has %s nodes, %s terminals", m_nodes.length, m_terminals.length);
725 	}
726 }
727 
728 unittest {
729 	import std.string : format;
730 	MatchTree!int m;
731 
732 	void testMatch(string str, size_t[] terms, string[] vars)
733 	{
734 		size_t[] mterms;
735 		string[] mvars;
736 		m.match(str, (t, scope vals) {
737 			mterms ~= t;
738 			mvars ~= vals;
739 		});
740 		assert(mterms == terms, format("Mismatched terminals: %s (expected %s)", mterms, terms));
741 		assert(mvars == vars, format("Mismatched variables; %s (expected %s)", mvars, vars));
742 	}
743 
744 	m.addTerminal("a", 0);
745 	m.addTerminal("b", 0);
746 	m.addTerminal("ab", 0);
747 	m.rebuildGraph();
748 	assert(m.getTerminalVarNames(0) == []);
749 	assert(m.getTerminalVarNames(1) == []);
750 	assert(m.getTerminalVarNames(2) == []);
751 	testMatch("a", [0], []);
752 	testMatch("ab", [2], []);
753 	testMatch("abc", [], []);
754 	testMatch("b", [1], []);
755 
756 	m = MatchTree!int.init;
757 	m.addTerminal("ab", 0);
758 	m.addTerminal("a*", 0);
759 	m.rebuildGraph();
760 	assert(m.getTerminalVarNames(0) == []);
761 	assert(m.getTerminalVarNames(1) == []);
762 	testMatch("a", [1], []);
763 	testMatch("ab", [0, 1], []);
764 	testMatch("abc", [1], []);
765 
766 	m = MatchTree!int.init;
767 	m.addTerminal("ab", 0);
768 	m.addTerminal("a:var", 0);
769 	m.rebuildGraph();
770 	assert(m.getTerminalVarNames(0) == []);
771 	assert(m.getTerminalVarNames(1) == ["var"], format("%s", m.getTerminalVarNames(1)));
772 	testMatch("a", [], []); // vars may not be empty
773 	testMatch("ab", [0, 1], ["b"]);
774 	testMatch("abc", [1], ["bc"]);
775 
776 	m = MatchTree!int.init;
777 	m.addTerminal(":var1/:var2", 0);
778 	m.addTerminal("a/:var3", 0);
779 	m.addTerminal(":var4/b", 0);
780 	m.rebuildGraph();
781 	assert(m.getTerminalVarNames(0) == ["var1", "var2"]);
782 	assert(m.getTerminalVarNames(1) == ["var3"]);
783 	assert(m.getTerminalVarNames(2) == ["var4"]);
784 	testMatch("a", [], []);
785 	testMatch("a/a", [0, 1], ["a", "a", "a"]);
786 	testMatch("a/b", [0, 1, 2], ["a", "b", "b", "a"]);
787 	testMatch("a/bc", [0, 1], ["a", "bc", "bc"]);
788 	testMatch("ab/b", [0, 2], ["ab", "b", "ab"]);
789 	testMatch("ab/bc", [0], ["ab", "bc"]);
790 
791 	m = MatchTree!int.init;
792 	m.addTerminal(":var1/", 0);
793 	m.rebuildGraph();
794 	assert(m.getTerminalVarNames(0) == ["var1"]);
795 	testMatch("ab/", [0], ["ab"]);
796 	testMatch("ab", [], []);
797 	testMatch("/ab", [], []);
798 	testMatch("a/b", [], []);
799 	testMatch("ab//", [], []);
800 }
801 
802 
803 private struct MatchGraphBuilder {
804 	import std.array : array;
805 	import std.algorithm : filter;
806 	import std.string : format;
807 
808 	private {
809 		enum TerminalChar = 0;
810 		struct TerminalTag {
811 			size_t index;
812 			string var;
813 			bool opEquals(in ref TerminalTag other) const { return index == other.index && var == other.var; }
814 		}
815 		struct Node {
816 			TerminalTag[] terminals;
817 			Edge[] edges;
818 		}
819 		struct Edge {
820 			ubyte ch;
821 			size_t to;
822 		}
823 		Node[] m_nodes;
824 	}
825 
826 	string[] insert(string pattern, size_t terminal)
827 	{
828 		import std.algorithm : canFind;
829 
830 		auto full_pattern = pattern;
831 		string[] vars;
832 		if (!m_nodes.length) addNode();
833 
834 		// create start node and connect to zero node
835 		auto nidx = addNode();
836 		addEdge(0, nidx, '^', terminal, null);
837 
838 		while (pattern.length) {
839 			auto ch = pattern[0];
840 			if (ch == '*') {
841 				assert(pattern.length == 1, "Asterisk is only allowed at the end of a pattern: "~full_pattern);
842 				pattern = null;
843 
844 				foreach (v; ubyte.min .. ubyte.max+1) {
845 					if (v == TerminalChar) continue;
846 					addEdge(nidx, nidx, cast(ubyte)v, terminal, null);
847 				}
848 			} else if (ch == ':') {
849 				pattern = pattern[1 .. $];
850 				auto name = skipPathNode(pattern);
851 				assert(name.length > 0, "Missing placeholder name: "~full_pattern);
852 				assert(!vars.canFind(name), "Duplicate placeholder name ':"~name~"': '"~full_pattern~"'");
853 				vars ~= name;
854 				assert(!pattern.length || (pattern[0] != '*' && pattern[0] != ':'),
855 					"Cannot have two placeholders directly follow each other.");
856 
857 				foreach (v; ubyte.min .. ubyte.max+1) {
858 					if (v == TerminalChar || v == '/') continue;
859 					addEdge(nidx, nidx, cast(ubyte)v, terminal, name);
860 				}
861 			} else {
862 				nidx = addEdge(nidx, ch, terminal, null);
863 				pattern = pattern[1 .. $];
864 			}
865 		}
866 
867 		addEdge(nidx, TerminalChar, terminal, null);
868 		return vars;
869 	}
870 
871 	void disambiguate()
872 	{
873 //logInfo("Disambiguate");
874 		if (!m_nodes.length) return;
875 
876 		import vibe.utils.hashmap;
877 		HashMap!(immutable(size_t)[], size_t) combined_nodes;
878 		auto visited = new bool[m_nodes.length * 2];
879 		size_t[] node_stack = [0];
880 		while (node_stack.length) {
881 			auto n = node_stack[$-1];
882 			node_stack.length--;
883 
884 			while (n >= visited.length) visited.length = visited.length * 2;
885 			if (visited[n]) continue;
886 //logInfo("Disambiguate %s", n);
887 			visited[n] = true;
888 
889 			Edge[] newedges;
890 			immutable(size_t)[][ubyte.max+1] edges;
891 			foreach (e; m_nodes[n].edges) edges[e.ch] ~= e.to;
892 			foreach (ch_; ubyte.min .. ubyte.max+1) {
893 				ubyte ch = cast(ubyte)ch_;
894 				auto chnodes = edges[ch_];
895 
896 				// handle trivial cases
897 				if (!chnodes.length) continue;
898 				if (chnodes.length == 1) { addToArray(newedges, Edge(ch, chnodes[0])); continue; }
899 
900 				// generate combined state for ambiguous edges
901 				if (auto pn = chnodes in combined_nodes) { addToArray(newedges, Edge(ch, *pn)); continue; }
902 
903 				// for new combinations, create a new node
904 				size_t ncomb = addNode();
905 				combined_nodes[chnodes] = ncomb;
906 				bool[ubyte][size_t] nc_edges;
907 				foreach (chn; chnodes) {
908 					foreach (e; m_nodes[chn].edges) {
909 						if (auto pv = e.to in nc_edges) {
910 							if (auto pw = e.ch in *pv)
911 								continue;
912 							else (*pv)[e.ch] = true;
913 						} else nc_edges[e.to][e.ch] = true;
914 						m_nodes[ncomb].edges ~= e;
915 					}
916 					addToArray(m_nodes[ncomb].terminals, m_nodes[chn].terminals);
917 				}
918 				foreach (i; 1 .. m_nodes[ncomb].terminals.length)
919 					assert(m_nodes[ncomb].terminals[0] != m_nodes[ncomb].terminals[i]);
920 				newedges ~= Edge(ch, ncomb);
921 			}
922 			m_nodes[n].edges = newedges;
923 
924 			// process nodes recursively
925 			node_stack.assumeSafeAppend();
926 			foreach (e; newedges) node_stack ~= e.to;
927 		}
928 //logInfo("Disambiguate done");
929 	}
930 
931 	void print()
932 	const {
933 		import std.algorithm : map;
934 		import std.array : join;
935 		import std.conv : to;
936 		import std.string : format;
937 
938 		logInfo("Nodes:");
939 		foreach (i, n; m_nodes) {
940 			string mapChar(ubyte ch) {
941 				if (ch == TerminalChar) return "$";
942 				if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch);
943 				if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch);
944 				if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch);
945 				if (ch == '/') return "/";
946 				return ch.to!string;
947 			}
948 			logInfo("  %s %s", i, n.terminals.map!(t => format("T%s%s", t.index, t.var.length ? "("~t.var~")" : "")).join(" "));
949 			foreach (e; n.edges)
950 				logInfo("    %s -> %s", mapChar(e.ch), e.to);
951 		}
952 	}
953 
954 	private void addEdge(size_t from, size_t to, ubyte ch, size_t terminal, string var)
955 	{
956 		m_nodes[from].edges ~= Edge(ch, to);
957 		addTerminal(to, terminal, var);
958 	}
959 
960 	private size_t addEdge(size_t from, ubyte ch, size_t terminal, string var)
961 	{
962 		import std.algorithm : canFind;
963 		import std.string : format;
964 		assert(!m_nodes[from].edges.canFind!(e => e.ch == ch), format("%s is in %s", ch, m_nodes[from].edges));
965 		auto nidx = addNode();
966 		addEdge(from, nidx, ch, terminal, var);
967 		return nidx;
968 	}
969 
970 	private void addTerminal(size_t node, size_t terminal, string var)
971 	{
972 		foreach (ref t; m_nodes[node].terminals) {
973 			if (t.index == terminal) {
974 				assert(t.var.length == 0 || t.var == var, "Ambiguous route var match!? '"~t.var~"' vs. '"~var~"'");
975 				t.var = var;
976 				return;
977 			}
978 		}
979 		m_nodes[node].terminals ~= TerminalTag(terminal, var);
980 	}
981 
982 	private size_t addNode()
983 	{
984 		auto idx = m_nodes.length;
985 		m_nodes ~= Node(null, null);
986 		return idx;
987 	}
988 
989 	private static addToArray(T)(ref T[] arr, T[] elems) { foreach (e; elems) addToArray(arr, e); }
990 	private static addToArray(T)(ref T[] arr, T elem)
991 	{
992 		import std.algorithm : canFind;
993 		if (!arr.canFind(elem)) arr ~= elem;
994 	}
995 }