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