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