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 }