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 }