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