1 /** 2 Pattern based URL router for HTTP request. 3 4 See `URLRouter` for more details. 5 6 Copyright: © 2012-2015 Sönke Ludwig 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 do { 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 import vibe.core.path : PosixPath; 197 198 auto method = req.method; 199 200 string calcBasePath() 201 @safe { 202 import vibe.core.path : InetPath, relativeToWeb; 203 auto p = InetPath(prefix.length ? prefix : "/"); 204 p.endsWithSlash = true; 205 return p.relativeToWeb(req.requestPath).toString(); 206 } 207 208 string path; 209 210 // NOTE: Instead of failing, we just ignore requests with invalid path 211 // segments (i.e. containing path separators) here. Any request 212 // handlers later in the queue may still choose to process them 213 // appropriately. 214 try path = (cast(PosixPath)req.requestPath).toString(); 215 catch (Exception e) return; 216 217 if (path.length < m_prefix.length || path[0 .. m_prefix.length] != m_prefix) return; 218 path = path[m_prefix.length .. $]; 219 220 while (true) { 221 bool done = m_routes.match(path, (ridx, scope values) @safe { 222 auto r = () @trusted { return &m_routes.getTerminalData(ridx); } (); 223 if (r.method != method) return false; 224 225 logDebugV("route match: %s -> %s %s %s", req.requestPath, r.method, r.pattern, values); 226 foreach (i, v; values) req.params[m_routes.getTerminalVarNames(ridx)[i]] = v; 227 if (m_computeBasePath) req.params["routerRootDir"] = calcBasePath(); 228 r.cb(req, res); 229 return res.headerWritten; 230 }); 231 if (done) return; 232 233 if (method == HTTPMethod.HEAD) method = HTTPMethod.GET; 234 else break; 235 } 236 237 logDebug("no route match: %s %s", req.method, req.requestURL); 238 } 239 240 /// Returns all registered routes as const AA 241 const(Route)[] getAllRoutes() 242 { 243 auto routes = new Route[m_routes.terminalCount]; 244 foreach (i, ref r; routes) 245 r = m_routes.getTerminalData(i); 246 return routes; 247 } 248 249 template isValidHandler(Handler) { 250 @system { 251 alias USDel = void delegate(HTTPServerRequest, HTTPServerResponse) @system; 252 alias USFun = void function(HTTPServerRequest, HTTPServerResponse) @system; 253 alias USDelS = void delegate(scope HTTPServerRequest, scope HTTPServerResponse) @system; 254 alias USFunS = void function(scope HTTPServerRequest, scope HTTPServerResponse) @system; 255 } 256 257 static if ( 258 is(Handler : HTTPServerRequestDelegate) || 259 is(Handler : HTTPServerRequestFunction) || 260 is(Handler : HTTPServerRequestHandler) || 261 is(Handler : HTTPServerRequestDelegateS) || 262 is(Handler : HTTPServerRequestFunctionS) || 263 is(Handler : HTTPServerRequestHandlerS) 264 ) 265 { 266 enum isValidHandler = true; 267 } else static if ( 268 is(Handler : USDel) || is(Handler : USFun) || 269 is(Handler : USDelS) || is(Handler : USFunS) 270 ) 271 { 272 enum isValidHandler = true; 273 } else { 274 enum isValidHandler = false; 275 } 276 } 277 278 static void delegate(HTTPServerRequest, HTTPServerResponse) @safe handlerDelegate(Handler)(Handler handler) 279 { 280 import std.traits : isFunctionPointer; 281 static if (isFunctionPointer!Handler) return handlerDelegate(() @trusted { return toDelegate(handler); } ()); 282 else static if (is(Handler == class) || is(Handler == interface)) return &handler.handleRequest; 283 else static if (__traits(compiles, () @safe { handler(HTTPServerRequest.init, HTTPServerResponse.init); } ())) return handler; 284 else return (req, res) @trusted { handler(req, res); }; 285 } 286 287 unittest { 288 static assert(isValidHandler!HTTPServerRequestFunction); 289 static assert(isValidHandler!HTTPServerRequestDelegate); 290 static assert(isValidHandler!HTTPServerRequestHandler); 291 static assert(isValidHandler!HTTPServerRequestFunctionS); 292 static assert(isValidHandler!HTTPServerRequestDelegateS); 293 static assert(isValidHandler!HTTPServerRequestHandlerS); 294 static assert(isValidHandler!(void delegate(HTTPServerRequest req, HTTPServerResponse res) @system)); 295 static assert(isValidHandler!(void function(HTTPServerRequest req, HTTPServerResponse res) @system)); 296 static assert(isValidHandler!(void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @system)); 297 static assert(isValidHandler!(void function(scope HTTPServerRequest req, scope HTTPServerResponse res) @system)); 298 static assert(!isValidHandler!(int delegate(HTTPServerRequest req, HTTPServerResponse res) @system)); 299 static assert(!isValidHandler!(int delegate(HTTPServerRequest req, HTTPServerResponse res) @safe)); 300 void test(H)(H h) 301 { 302 static assert(isValidHandler!H); 303 } 304 test((HTTPServerRequest req, HTTPServerResponse res) {}); 305 } 306 } 307 308 /// 309 @safe unittest { 310 import vibe.http.fileserver; 311 312 void addGroup(HTTPServerRequest req, HTTPServerResponse res) 313 { 314 // Route variables are accessible via the params map 315 logInfo("Getting group %s for user %s.", req.params["groupname"], req.params["username"]); 316 } 317 318 void deleteUser(HTTPServerRequest req, HTTPServerResponse res) 319 { 320 // ... 321 } 322 323 void auth(HTTPServerRequest req, HTTPServerResponse res) 324 { 325 // TODO: check req.session to see if a user is logged in and 326 // write an error page or throw an exception instead. 327 } 328 329 void setup() 330 { 331 auto router = new URLRouter; 332 // Matches all GET requests for /users/*/groups/* and places 333 // the place holders in req.params as 'username' and 'groupname'. 334 router.get("/users/:username/groups/:groupname", &addGroup); 335 336 // Matches all requests. This can be useful for authorization and 337 // similar tasks. The auth method will only write a response if the 338 // user is _not_ authorized. Otherwise, the router will fall through 339 // and continue with the following routes. 340 router.any("*", &auth); 341 342 // Matches a POST request 343 router.post("/users/:username/delete", &deleteUser); 344 345 // Matches all GET requests in /static/ such as /static/img.png or 346 // /static/styles/sty.css 347 router.get("/static/*", serveStaticFiles("public/")); 348 349 // Setup a HTTP server... 350 auto settings = new HTTPServerSettings; 351 // ... 352 353 // The router can be directly passed to the listenHTTP function as 354 // the main request handler. 355 listenHTTP(settings, router); 356 } 357 } 358 359 /** Using nested routers to map components to different sub paths. A component 360 could for example be an embedded blog engine. 361 */ 362 @safe unittest { 363 // some embedded component: 364 365 void showComponentHome(HTTPServerRequest req, HTTPServerResponse res) 366 { 367 // ... 368 } 369 370 void showComponentUser(HTTPServerRequest req, HTTPServerResponse res) 371 { 372 // ... 373 } 374 375 void registerComponent(URLRouter router) 376 { 377 router.get("/", &showComponentHome); 378 router.get("/users/:user", &showComponentUser); 379 } 380 381 // main application: 382 383 void showHome(HTTPServerRequest req, HTTPServerResponse res) 384 { 385 // ... 386 } 387 388 void setup() 389 { 390 auto c1router = new URLRouter("/component1"); 391 registerComponent(c1router); 392 393 auto mainrouter = new URLRouter; 394 mainrouter.get("/", &showHome); 395 // forward all unprocessed requests to the component router 396 mainrouter.any("*", c1router); 397 398 // now the following routes will be matched: 399 // / -> showHome 400 // /component1/ -> showComponentHome 401 // /component1/users/:user -> showComponentUser 402 403 // Start the HTTP server 404 auto settings = new HTTPServerSettings; 405 // ... 406 listenHTTP(settings, mainrouter); 407 } 408 } 409 410 @safe unittest { // issue #1668 411 auto r = new URLRouter; 412 r.get("/", (req, res) { 413 if ("foo" in req.headers) 414 res.writeBody("bar"); 415 }); 416 417 r.get("/", (scope req, scope res) { 418 if ("foo" in req.headers) 419 res.writeBody("bar"); 420 }); 421 r.get("/", (req, res) {}); 422 r.post("/", (req, res) {}); 423 r.put("/", (req, res) {}); 424 r.delete_("/", (req, res) {}); 425 r.patch("/", (req, res) {}); 426 r.any("/", (req, res) {}); 427 } 428 429 @safe unittest { // issue #1866 430 auto r = new URLRouter; 431 r.match(HTTPMethod.HEAD, "/foo", (scope req, scope res) { res.writeVoidBody; }); 432 r.match(HTTPMethod.HEAD, "/foo", (req, res) { res.writeVoidBody; }); 433 r.match(HTTPMethod.HEAD, "/foo", (scope HTTPServerRequest req, scope HTTPServerResponse res) { res.writeVoidBody; }); 434 r.match(HTTPMethod.HEAD, "/foo", (HTTPServerRequest req, HTTPServerResponse res) { res.writeVoidBody; }); 435 436 auto r2 = new URLRouter; 437 r.match(HTTPMethod.HEAD, "/foo", r2); 438 } 439 440 @safe unittest { 441 import vibe.inet.url; 442 443 auto router = new URLRouter; 444 string result; 445 void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; } 446 void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; } 447 void c(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "x", "Wrong variable contents: "~req.params["test"]); result ~= "C"; } 448 void d(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "y", "Wrong variable contents: "~req.params["test"]); result ~= "D"; } 449 router.get("/test", &a); 450 router.post("/test", &b); 451 router.get("/a/:test", &c); 452 router.get("/a/:test/", &d); 453 454 auto res = createTestHTTPServerResponse(); 455 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/")), res); 456 assert(result == "", "Matched for non-existent / path"); 457 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res); 458 assert(result == "A", "Didn't match a simple GET request"); 459 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test"), HTTPMethod.POST), res); 460 assert(result == "AB", "Didn't match a simple POST request"); 461 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/"), HTTPMethod.GET), res); 462 assert(result == "AB", "Matched empty variable. "~result); 463 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/x"), HTTPMethod.GET), res); 464 assert(result == "ABC", "Didn't match a trailing 1-character var."); 465 // currently fails due to Path not accepting "//" 466 //router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a//"), HTTPMethod.GET), res); 467 //assert(result == "ABC", "Matched empty string or slash as var. "~result); 468 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/y/"), HTTPMethod.GET), res); 469 assert(result == "ABCD", "Didn't match 1-character infix variable."); 470 } 471 472 @safe unittest { 473 import vibe.inet.url; 474 475 auto router = new URLRouter("/test"); 476 477 string result; 478 void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; } 479 void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; } 480 router.get("/x", &a); 481 router.get("/y", &b); 482 483 auto res = createTestHTTPServerResponse(); 484 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res); 485 assert(result == ""); 486 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/x")), res); 487 assert(result == "A"); 488 router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/y")), res); 489 assert(result == "AB"); 490 } 491 492 @safe unittest { 493 string ensureMatch(string pattern, string local_uri, string[string] expected_params = null) 494 { 495 import vibe.inet.url : URL; 496 string ret = local_uri ~ " did not match " ~ pattern; 497 auto r = new URLRouter; 498 r.get(pattern, (req, res) { 499 ret = null; 500 foreach (k, v; expected_params) { 501 if (k !in req.params) { ret = "Parameter "~k~" was not matched."; return; } 502 if (req.params[k] != v) { ret = "Parameter "~k~" is '"~req.params[k]~"' instead of '"~v~"'."; return; } 503 } 504 }); 505 auto req = createTestHTTPServerRequest(URL("http://localhost"~local_uri)); 506 auto res = createTestHTTPServerResponse(); 507 r.handleRequest(req, res); 508 return ret; 509 } 510 511 assert(ensureMatch("/foo bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/" 512 //assert(ensureMatch("/foo%20bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/" 513 assert(ensureMatch("/foo/bar/", "/foo/bar/") is null); // normalized pattern: "/foo/bar/" 514 //assert(ensureMatch("/foo/bar/", "/foo%2fbar/") !is null); 515 //assert(ensureMatch("/foo%2fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/" 516 //assert(ensureMatch("/foo%2Fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/" 517 //assert(ensureMatch("/foo%2fbar/", "/foo%2Fbar/") is null); 518 //assert(ensureMatch("/foo%2fbar/", "/foo/bar/") !is null); 519 //assert(ensureMatch("/:foo/", "/foo%2Fbar/", ["foo": "foo/bar"]) is null); 520 assert(ensureMatch("/:foo/", "/foo/bar/") !is null); 521 } 522 523 unittest { // issue #2561 524 import vibe.http.server : createTestHTTPServerRequest, createTestHTTPServerResponse; 525 import vibe.inet.url : URL; 526 import vibe.stream.memory : createMemoryOutputStream; 527 528 Route[] routes = [ 529 Route(HTTPMethod.PUT, "/public/devices/commandList"), 530 Route(HTTPMethod.PUT, "/public/devices/logicStateList"), 531 Route(HTTPMethod.OPTIONS, "/public/devices/commandList"), 532 Route(HTTPMethod.OPTIONS, "/public/devices/logicStateList"), 533 Route(HTTPMethod.PUT, "/public/mnemoschema"), 534 Route(HTTPMethod.PUT, "/public/static"), 535 Route(HTTPMethod.PUT, "/public/dynamic"), 536 Route(HTTPMethod.PUT, "/public/info"), 537 Route(HTTPMethod.PUT, "/public/info-network"), 538 Route(HTTPMethod.PUT, "/public/events"), 539 Route(HTTPMethod.PUT, "/public/eventList"), 540 Route(HTTPMethod.PUT, "/public/availBatteryModels"), 541 Route(HTTPMethod.OPTIONS, "/public/availBatteryModels"), 542 Route(HTTPMethod.OPTIONS, "/public/dynamic"), 543 Route(HTTPMethod.OPTIONS, "/public/eventList"), 544 Route(HTTPMethod.OPTIONS, "/public/events"), 545 Route(HTTPMethod.OPTIONS, "/public/info"), 546 Route(HTTPMethod.OPTIONS, "/public/info-network"), 547 Route(HTTPMethod.OPTIONS, "/public/mnemoschema"), 548 Route(HTTPMethod.OPTIONS, "/public/static"), 549 Route(HTTPMethod.PUT, "/settings/admin/getinfo"), 550 Route(HTTPMethod.PUT, "/settings/admin/setconf"), 551 Route(HTTPMethod.PUT, "/settings/admin/checksetaccess"), 552 Route(HTTPMethod.OPTIONS, "/settings/admin/checksetaccess"), 553 Route(HTTPMethod.OPTIONS, "/settings/admin/getinfo"), 554 Route(HTTPMethod.OPTIONS, "/settings/admin/setconf"), 555 ]; 556 557 auto router = new URLRouter; 558 559 foreach (r; routes) 560 router.match(r.method, r.pattern, (req, res) { 561 res.writeBody("OK"); 562 }); 563 564 { // make sure unmatched routes are not handled by the router 565 auto req = createTestHTTPServerRequest(URL("http://localhost/foobar"), HTTPMethod.PUT); 566 auto res = createTestHTTPServerResponse(); 567 router.handleRequest(req, res); 568 assert(!res.headerWritten); 569 } 570 571 // ensure all routes are matched 572 foreach (r; routes) { 573 auto url = URL("http://localhost"~r.pattern); 574 auto output = createMemoryOutputStream(); 575 auto req = createTestHTTPServerRequest(url, r.method); 576 auto res = createTestHTTPServerResponse(output, null, TestHTTPResponseMode.bodyOnly); 577 router.handleRequest(req, res); 578 //assert(res.headerWritten); 579 assert(output.data == "OK"); 580 } 581 } 582 583 584 /** 585 Convenience abstraction for a single `URLRouter` route. 586 587 See `URLRouter.route` for a usage example. 588 */ 589 struct URLRoute { 590 @safe: 591 592 URLRouter router; 593 string path; 594 595 ref URLRoute get(Handler)(Handler h) { router.get(path, h); return this; } 596 ref URLRoute post(Handler)(Handler h) { router.post(path, h); return this; } 597 ref URLRoute put(Handler)(Handler h) { router.put(path, h); return this; } 598 ref URLRoute delete_(Handler)(Handler h) { router.delete_(path, h); return this; } 599 ref URLRoute patch(Handler)(Handler h) { router.patch(path, h); return this; } 600 ref URLRoute any(Handler)(Handler h) { router.any(path, h); return this; } 601 ref URLRoute match(Handler)(HTTPMethod method, Handler h) { router.match(method, path, h); return this; } 602 } 603 604 605 private enum maxRouteParameters = 64; 606 607 private struct Route { 608 HTTPMethod method; 609 string pattern; 610 HTTPServerRequestDelegate cb; 611 } 612 613 private string skipPathNode(string str, ref size_t idx) 614 @safe { 615 size_t start = idx; 616 while( idx < str.length && str[idx] != '/' ) idx++; 617 return str[start .. idx]; 618 } 619 620 private string skipPathNode(ref string str) 621 @safe { 622 size_t idx = 0; 623 auto ret = skipPathNode(str, idx); 624 str = str[idx .. $]; 625 return ret; 626 } 627 628 private struct MatchTree(T) { 629 @safe: 630 631 import std.algorithm : countUntil; 632 import std.array : array; 633 634 private { 635 alias NodeIndex = uint; 636 alias TerminalTagIndex = uint; 637 alias TerminalIndex = ushort; 638 alias VarIndex = ushort; 639 640 struct Node { 641 TerminalTagIndex terminalsStart; // slice into m_terminalTags 642 TerminalTagIndex terminalsEnd; 643 NodeIndex[ubyte.max+1] edges = NodeIndex.max; // character -> index into m_nodes 644 } 645 struct TerminalTag { 646 TerminalIndex index; // index into m_terminals array 647 VarIndex var = VarIndex.max; // index into Terminal.varNames/varValues or VarIndex.max 648 } 649 struct Terminal { 650 string pattern; 651 T data; 652 string[] varNames; 653 VarIndex[NodeIndex] varMap; 654 } 655 Node[] m_nodes; // all nodes as a single array 656 TerminalTag[] m_terminalTags; 657 Terminal[] m_terminals; 658 659 enum TerminalChar = 0; 660 bool m_upToDate = false; 661 } 662 663 @property size_t terminalCount() const { return m_terminals.length; } 664 665 void addTerminal(string pattern, T data) 666 { 667 assert(m_terminals.length < TerminalIndex.max, "Attempt to register too many routes."); 668 m_terminals ~= Terminal(pattern, data, null, null); 669 m_upToDate = false; 670 } 671 672 bool match(string text, scope bool delegate(size_t terminal, scope string[] vars) @safe del) 673 { 674 // lazily update the match graph 675 if (!m_upToDate) rebuildGraph(); 676 677 return doMatch(text, del); 678 } 679 680 const(string)[] getTerminalVarNames(size_t terminal) const { return m_terminals[terminal].varNames; } 681 ref inout(T) getTerminalData(size_t terminal) inout { return m_terminals[terminal].data; } 682 683 void print() 684 const { 685 import std.algorithm : map; 686 import std.array : join; 687 import std.conv : to; 688 import std.range : iota; 689 import std.string : format; 690 691 logInfo("Nodes:"); 692 foreach (i, n; m_nodes) { 693 logInfo(" %s %s", i, m_terminalTags[n.terminalsStart .. n.terminalsEnd] 694 .map!(t => format("T%s%s", t.index, t.var != VarIndex.max ? "("~m_terminals[t.index].varNames[t.var]~")" : "")).join(" ")); 695 //logInfo(" %s %s-%s", i, n.terminalsStart, n.terminalsEnd); 696 697 698 static string mapChar(ubyte ch) { 699 if (ch == TerminalChar) return "$"; 700 if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch); 701 if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch); 702 if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch); 703 if (ch == '/') return "/"; 704 if (ch == '^') return "^"; 705 return ch.to!string; 706 } 707 708 void printRange(uint node, ubyte from, ubyte to) 709 { 710 if (to - from <= 10) logInfo(" %s -> %s", iota(from, cast(uint)to+1).map!(ch => mapChar(cast(ubyte)ch)).join("|"), node); 711 else logInfo(" %s-%s -> %s", mapChar(from), mapChar(to), node); 712 } 713 714 auto last_to = NodeIndex.max; 715 ubyte last_ch = 0; 716 foreach (ch, e; n.edges) 717 if (e != last_to) { 718 if (last_to != NodeIndex.max) 719 printRange(last_to, last_ch, cast(ubyte)(ch-1)); 720 last_ch = cast(ubyte)ch; 721 last_to = e; 722 } 723 if (last_to != NodeIndex.max) 724 printRange(last_to, last_ch, ubyte.max); 725 } 726 } 727 728 private bool doMatch(string text, scope bool delegate(size_t terminal, scope string[] vars) @safe del) 729 const { 730 string[maxRouteParameters] vars_buf;// = void; 731 732 import std.algorithm : canFind; 733 734 // first, determine the end node, if any 735 auto n = matchTerminals(text); 736 if (!n) return false; 737 738 // then, go through the terminals and match their variables 739 foreach (ref t; m_terminalTags[n.terminalsStart .. n.terminalsEnd]) { 740 auto term = &m_terminals[t.index]; 741 auto vars = vars_buf[0 .. term.varNames.length]; 742 matchVars(vars, term, text); 743 if (vars.canFind!(v => v.length == 0)) continue; // all variables must be non-empty to match 744 if (del(t.index, vars)) return true; 745 } 746 return false; 747 } 748 749 private inout(Node)* matchTerminals(string text) 750 inout { 751 if (!m_nodes.length) return null; 752 753 auto n = &m_nodes[0]; 754 755 // follow the path through the match graph 756 foreach (i, char ch; text) { 757 auto nidx = n.edges[cast(size_t)ch]; 758 if (nidx == NodeIndex.max) return null; 759 n = &m_nodes[nidx]; 760 } 761 762 // finally, find a matching terminal node 763 auto nidx = n.edges[TerminalChar]; 764 if (nidx == NodeIndex.max) return null; 765 n = &m_nodes[nidx]; 766 return n; 767 } 768 769 private void matchVars(string[] dst, in Terminal* term, string text) 770 const { 771 NodeIndex nidx = 0; 772 VarIndex activevar = VarIndex.max; 773 size_t activevarstart; 774 775 dst[] = null; 776 777 // folow the path throgh the match graph 778 foreach (i, char ch; text) { 779 auto var = term.varMap.get(nidx, VarIndex.max); 780 781 // detect end of variable 782 if (var != activevar && activevar != VarIndex.max) { 783 dst[activevar] = text[activevarstart .. i-1]; 784 activevar = VarIndex.max; 785 } 786 787 // detect beginning of variable 788 if (var != VarIndex.max && activevar == VarIndex.max) { 789 activevar = var; 790 activevarstart = i; 791 } 792 793 nidx = m_nodes[nidx].edges[cast(ubyte)ch]; 794 assert(nidx != NodeIndex.max); 795 } 796 797 // terminate any active varible with the end of the input string or with the last character 798 auto var = term.varMap.get(nidx, VarIndex.max); 799 if (activevar != VarIndex.max) dst[activevar] = text[activevarstart .. (var == activevar ? $ : $-1)]; 800 } 801 802 private void rebuildGraph() 803 @trusted { 804 import std.array : appender; 805 import std.conv : to; 806 807 if (m_upToDate) return; 808 m_upToDate = true; 809 810 m_nodes = null; 811 m_terminalTags = null; 812 813 if (!m_terminals.length) return; 814 815 MatchGraphBuilder builder; 816 foreach (i, ref t; m_terminals) { 817 t.varNames = builder.insert(t.pattern, i.to!TerminalIndex); 818 assert(t.varNames.length <= VarIndex.max, "Too many variables in route."); 819 } 820 //builder.print(); 821 builder.disambiguate(); 822 823 auto nodemap = new NodeIndex[builder.m_nodes.length]; 824 nodemap[] = NodeIndex.max; 825 826 auto nodes = appender!(Node[]); 827 nodes.reserve(1024); 828 auto termtags = appender!(TerminalTag[]); 829 termtags.reserve(1024); 830 831 NodeIndex process(NodeIndex n) 832 { 833 import std.algorithm : canFind; 834 835 if (nodemap[n] != NodeIndex.max) return nodemap[n]; 836 auto nmidx = cast(NodeIndex)nodes.data.length; 837 nodemap[n] = nmidx; 838 nodes.put(Node.init); 839 840 Node nn; 841 nn.terminalsStart = termtags.data.length.to!TerminalTagIndex; 842 foreach (t; builder.m_nodes[n].terminals) { 843 auto var = cast(VarIndex)t.var; 844 assert(!termtags.data[nn.terminalsStart .. $].canFind!(u => u.index == t.index && u.var == var)); 845 termtags ~= TerminalTag(cast(TerminalIndex)t.index, var); 846 if (var != VarIndex.max) 847 m_terminals[t.index].varMap[nmidx] = var; 848 } 849 nn.terminalsEnd = termtags.data.length.to!TerminalTagIndex; 850 foreach (ch, targets; builder.m_nodes[n].edges) 851 foreach (to; builder.m_edgeEntries.getItems(targets)) 852 nn.edges[ch] = process(to); 853 854 nodes.data[nmidx] = nn; 855 856 return nmidx; 857 } 858 assert(builder.m_edgeEntries.hasLength(builder.m_nodes[0].edges['^'], 1), 859 "Graph must be disambiguated before purging."); 860 process(builder.m_edgeEntries.getItems(builder.m_nodes[0].edges['^']).front); 861 862 m_nodes = nodes.data; 863 m_terminalTags = termtags.data; 864 865 logDebug("Match tree has %s (of %s in the builder) nodes, %s terminals", m_nodes.length, builder.m_nodes.length, m_terminals.length); 866 } 867 } 868 869 unittest { 870 import std.string : format; 871 MatchTree!int m; 872 873 void testMatch(string str, size_t[] terms, string[] vars) 874 { 875 size_t[] mterms; 876 string[] mvars; 877 m.match(str, (t, scope vals) { 878 mterms ~= t; 879 mvars ~= vals; 880 return false; 881 }); 882 assert(mterms == terms, format("Mismatched terminals: %s (expected %s)", mterms, terms)); 883 assert(mvars == vars, format("Mismatched variables; %s (expected %s)", mvars, vars)); 884 } 885 886 m.addTerminal("a", 0); 887 m.addTerminal("b", 0); 888 m.addTerminal("ab", 0); 889 m.rebuildGraph(); 890 assert(m.getTerminalVarNames(0) == []); 891 assert(m.getTerminalVarNames(1) == []); 892 assert(m.getTerminalVarNames(2) == []); 893 testMatch("a", [0], []); 894 testMatch("ab", [2], []); 895 testMatch("abc", [], []); 896 testMatch("b", [1], []); 897 898 m = MatchTree!int.init; 899 m.addTerminal("ab", 0); 900 m.addTerminal("a*", 0); 901 m.rebuildGraph(); 902 assert(m.getTerminalVarNames(0) == []); 903 assert(m.getTerminalVarNames(1) == []); 904 testMatch("a", [1], []); 905 testMatch("ab", [0, 1], []); 906 testMatch("abc", [1], []); 907 908 m = MatchTree!int.init; 909 m.addTerminal("ab", 0); 910 m.addTerminal("a:var", 0); 911 m.rebuildGraph(); 912 assert(m.getTerminalVarNames(0) == []); 913 assert(m.getTerminalVarNames(1) == ["var"], format("%s", m.getTerminalVarNames(1))); 914 testMatch("a", [], []); // vars may not be empty 915 testMatch("ab", [0, 1], ["b"]); 916 testMatch("abc", [1], ["bc"]); 917 918 m = MatchTree!int.init; 919 m.addTerminal(":var1/:var2", 0); 920 m.addTerminal("a/:var3", 0); 921 m.addTerminal(":var4/b", 0); 922 m.rebuildGraph(); 923 assert(m.getTerminalVarNames(0) == ["var1", "var2"]); 924 assert(m.getTerminalVarNames(1) == ["var3"]); 925 assert(m.getTerminalVarNames(2) == ["var4"]); 926 testMatch("a", [], []); 927 testMatch("a/a", [0, 1], ["a", "a", "a"]); 928 testMatch("a/b", [0, 1, 2], ["a", "b", "b", "a"]); 929 testMatch("a/bc", [0, 1], ["a", "bc", "bc"]); 930 testMatch("ab/b", [0, 2], ["ab", "b", "ab"]); 931 testMatch("ab/bc", [0], ["ab", "bc"]); 932 933 m = MatchTree!int.init; 934 m.addTerminal(":var1/", 0); 935 m.rebuildGraph(); 936 assert(m.getTerminalVarNames(0) == ["var1"]); 937 testMatch("ab/", [0], ["ab"]); 938 testMatch("ab", [], []); 939 testMatch("/ab", [], []); 940 testMatch("a/b", [], []); 941 testMatch("ab//", [], []); 942 } 943 944 945 private struct MatchGraphBuilder { 946 @safe: 947 import std.container.array : Array; 948 import std.array : array; 949 import std.algorithm : filter; 950 import std.string : format; 951 952 alias NodeIndex = uint; 953 alias TerminalIndex = ushort; 954 alias VarIndex = ushort; 955 alias NodeSet = LinkedSetBacking!NodeIndex.Handle; 956 957 private { 958 enum TerminalChar = 0; 959 struct TerminalTag { 960 TerminalIndex index; 961 VarIndex var = VarIndex.max; 962 } 963 struct Node { 964 Array!TerminalTag terminals; 965 NodeSet[ubyte.max+1] edges; 966 } 967 Array!Node m_nodes; 968 LinkedSetBacking!NodeIndex m_edgeEntries; 969 } 970 971 @disable this(this); 972 973 string[] insert(string pattern, TerminalIndex terminal) 974 { 975 import std.algorithm : canFind; 976 977 auto full_pattern = pattern; 978 string[] vars; 979 if (!m_nodes.length) addNode(); 980 981 // create start node and connect to zero node 982 auto nidx = addNode(); 983 addEdge(0, nidx, '^', terminal); 984 985 while (pattern.length) { 986 auto ch = pattern[0]; 987 if (ch == '*') { 988 assert(pattern.length == 1, "Asterisk is only allowed at the end of a pattern: "~full_pattern); 989 pattern = null; 990 991 foreach (v; ubyte.min .. ubyte.max+1) { 992 if (v == TerminalChar) continue; 993 addEdge(nidx, nidx, cast(ubyte)v, terminal); 994 } 995 } else if (ch == ':') { 996 pattern = pattern[1 .. $]; 997 auto name = skipPathNode(pattern); 998 assert(name.length > 0, "Missing placeholder name: "~full_pattern); 999 assert(!vars.canFind(name), "Duplicate placeholder name ':"~name~"': '"~full_pattern~"'"); 1000 auto varidx = cast(VarIndex)vars.length; 1001 vars ~= name; 1002 assert(!pattern.length || (pattern[0] != '*' && pattern[0] != ':'), 1003 "Cannot have two placeholders directly follow each other."); 1004 1005 foreach (v; ubyte.min .. ubyte.max+1) { 1006 if (v == TerminalChar || v == '/') continue; 1007 addEdge(nidx, nidx, cast(ubyte)v, terminal, varidx); 1008 } 1009 } else { 1010 nidx = addEdge(nidx, ch, terminal); 1011 pattern = pattern[1 .. $]; 1012 } 1013 } 1014 1015 addEdge(nidx, TerminalChar, terminal); 1016 return vars; 1017 } 1018 1019 void disambiguate() 1020 @trusted { 1021 import std.algorithm : map, sum; 1022 import std.array : appender, join; 1023 1024 //logInfo("Disambiguate with %s initial nodes", m_nodes.length); 1025 if (!m_nodes.length) return; 1026 1027 import vibe.utils.hashmap; 1028 HashMap!(LinkedSetHash, NodeIndex) combined_nodes; 1029 Array!bool visited; 1030 visited.length = m_nodes.length * 2; 1031 Stack!NodeIndex node_stack; 1032 node_stack.reserve(m_nodes.length); 1033 node_stack.push(0); 1034 while (!node_stack.empty) { 1035 auto n = node_stack.pop(); 1036 1037 while (n >= visited.length) visited.length = visited.length * 2; 1038 if (visited[n]) continue; 1039 //logInfo("Disambiguate %s (stack=%s)", n, node_stack.fill); 1040 visited[n] = true; 1041 1042 foreach (ch; ubyte.min .. ubyte.max+1) { 1043 auto chnodes = m_nodes[n].edges[ch]; 1044 LinkedSetHash chhash = m_edgeEntries.getHash(chnodes); 1045 1046 // handle trivial cases 1047 if (m_edgeEntries.hasMaxLength(chnodes, 1)) 1048 continue; 1049 1050 // generate combined state for ambiguous edges 1051 if (auto pn = () @trusted { return chhash in combined_nodes; } ()) { 1052 m_nodes[n].edges[ch] = m_edgeEntries.create(*pn); 1053 assert(m_edgeEntries.hasLength(m_nodes[n].edges[ch], 1)); 1054 continue; 1055 } 1056 1057 // for new combinations, create a new node 1058 NodeIndex ncomb = addNode(); 1059 combined_nodes[chhash] = ncomb; 1060 1061 // write all edges 1062 size_t idx = 0; 1063 foreach (to_ch; ubyte.min .. ubyte.max+1) { 1064 auto e = &m_nodes[ncomb].edges[to_ch]; 1065 foreach (chn; m_edgeEntries.getItems(chnodes)) 1066 m_edgeEntries.insert(e, m_edgeEntries.getItems(m_nodes[chn].edges[to_ch])); 1067 } 1068 1069 // add terminal indices 1070 foreach (chn; m_edgeEntries.getItems(chnodes)) 1071 foreach (t; m_nodes[chn].terminals) 1072 addTerminal(ncomb, t.index, t.var); 1073 foreach (i; 1 .. m_nodes[ncomb].terminals.length) 1074 assert(m_nodes[ncomb].terminals[0] != m_nodes[ncomb].terminals[i]); 1075 1076 m_nodes[n].edges[ch] = m_edgeEntries.create(ncomb); 1077 assert(m_edgeEntries.hasLength(m_nodes[n].edges[ch], 1)); 1078 } 1079 1080 // process nodes recursively 1081 foreach (ch; ubyte.min .. ubyte.max+1) { 1082 // should only have single out-edges now 1083 assert(m_edgeEntries.hasMaxLength(m_nodes[n].edges[ch], 1)); 1084 foreach (e; m_edgeEntries.getItems(m_nodes[n].edges[ch])) 1085 node_stack.push(e); 1086 } 1087 } 1088 1089 import std.algorithm.sorting : sort; 1090 foreach (ref n; m_nodes) 1091 n.terminals[].sort!((a, b) => a.index < b.index)(); 1092 1093 debug logDebug("Disambiguate done: %s nodes, %s max stack size", m_nodes.length, node_stack.maxSize); 1094 } 1095 1096 void print() 1097 const @trusted { 1098 import std.algorithm : map; 1099 import std.array : join; 1100 import std.conv : to; 1101 import std.string : format; 1102 1103 logInfo("Nodes:"); 1104 size_t i = 0; 1105 foreach (n; m_nodes) { 1106 string mapChar(ubyte ch) { 1107 if (ch == TerminalChar) return "$"; 1108 if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch); 1109 if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch); 1110 if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch); 1111 if (ch == '^') return "^"; 1112 if (ch == '/') return "/"; 1113 return format("$%s", ch); 1114 } 1115 logInfo(" %s: %s", i, n.terminals[].map!(t => t.var != VarIndex.max ? format("T%s(%s)", t.index, t.var) : format("T%s", t.index)).join(" ")); 1116 ubyte first_char; 1117 LinkedSetHash list_hash; 1118 NodeSet list; 1119 1120 void printEdges(ubyte last_char) { 1121 if (!list.empty) { 1122 string targets; 1123 foreach (tn; m_edgeEntries.getItems(list)) 1124 targets ~= format(" %s", tn); 1125 if (targets.length > 0) 1126 logInfo(" [%s ... %s] -> %s", mapChar(first_char), mapChar(last_char), targets); 1127 } 1128 } 1129 foreach (ch, tnodes; n.edges) { 1130 auto h = m_edgeEntries.getHash(tnodes); 1131 if (h != list_hash) { 1132 printEdges(cast(ubyte)(ch-1)); 1133 list_hash = h; 1134 list = tnodes; 1135 first_char = cast(ubyte)ch; 1136 } 1137 } 1138 printEdges(ubyte.max); 1139 i++; 1140 } 1141 } 1142 1143 private void addEdge(NodeIndex from, NodeIndex to, ubyte ch, TerminalIndex terminal, VarIndex var = VarIndex.max) 1144 @trusted { 1145 m_edgeEntries.insert(&m_nodes[from].edges[ch], to); 1146 addTerminal(to, terminal, var); 1147 } 1148 1149 private NodeIndex addEdge(NodeIndex from, ubyte ch, TerminalIndex terminal, VarIndex var = VarIndex.max) 1150 @trusted { 1151 import std.algorithm : canFind; 1152 import std.string : format; 1153 if (!m_nodes[from].edges[ch].empty) 1154 assert(false, format("%s is in %s", ch, m_nodes[from].edges[])); 1155 auto nidx = addNode(); 1156 addEdge(from, nidx, ch, terminal, var); 1157 return nidx; 1158 } 1159 1160 private void addTerminal(NodeIndex node, TerminalIndex terminal, VarIndex var) 1161 @trusted { 1162 foreach (ref t; m_nodes[node].terminals) { 1163 if (t.index == terminal) { 1164 if (t.var != VarIndex.max && t.var != var) 1165 assert(false, format("Ambiguous route var match!? %s vs %s", t.var, var)); 1166 t.var = var; 1167 return; 1168 } 1169 } 1170 m_nodes[node].terminals ~= TerminalTag(terminal, var); 1171 } 1172 1173 private NodeIndex addNode() 1174 @trusted { 1175 assert(m_nodes.length <= 1_000_000_000, "More than 1B nodes in tree!?"); 1176 auto idx = cast(NodeIndex)m_nodes.length; 1177 m_nodes ~= Node.init; 1178 return idx; 1179 } 1180 } 1181 1182 1183 /** Used to store and manipulate multiple linked lists within a single array 1184 based storage. 1185 */ 1186 struct LinkedSetBacking(T) { 1187 import std.container.array : Array; 1188 import std.range : isInputRange; 1189 1190 static struct Handle { 1191 uint index = uint.max; 1192 @property bool empty() const { return index == uint.max; } 1193 } 1194 1195 private { 1196 static struct Entry { 1197 uint next; 1198 T value; 1199 } 1200 1201 Array!Entry m_storage; 1202 1203 static struct Range { 1204 private { 1205 Array!Entry* backing; 1206 uint index; 1207 } 1208 1209 @property bool empty() const { return index == uint.max; } 1210 @property ref const(T) front() const { return (*backing)[index].value; } 1211 1212 void popFront() 1213 { 1214 index = (*backing)[index].next; 1215 } 1216 } 1217 } 1218 1219 @property Handle emptySet() { return Handle.init; } 1220 1221 Handle create(scope T[] items...) 1222 { 1223 Handle ret; 1224 foreach (i; items) 1225 ret.index = createNode(i, ret.index); 1226 return ret; 1227 } 1228 1229 void insert(Handle* h, T value) 1230 { 1231 /*foreach (c; getItems(*h)) 1232 if (value == c) 1233 return;*/ 1234 h.index = createNode(value, h.index); 1235 } 1236 1237 void insert(R)(Handle* h, R items) 1238 if (isInputRange!R) 1239 { 1240 foreach (itm; items) 1241 insert(h, itm); 1242 } 1243 1244 LinkedSetHash getHash(Handle sh) 1245 const { 1246 import std.digest.md : md5Of; 1247 1248 // NOTE: the returned hash is order independent, to avoid bogus 1249 // mismatches when comparing lists of different order 1250 LinkedSetHash ret = cast(LinkedSetHash)md5Of([]); 1251 while (sh != Handle.init) { 1252 auto h = cast(LinkedSetHash)md5Of(cast(const(ubyte)[])(&m_storage[sh.index].value)[0 .. 1]); 1253 foreach (i; 0 .. ret.length) ret[i] ^= h[i]; 1254 sh.index = m_storage[sh.index].next; 1255 } 1256 return ret; 1257 } 1258 1259 auto getItems(Handle sh) { return Range(&m_storage, sh.index); } 1260 auto getItems(Handle sh) const { return Range(&(cast()this).m_storage, sh.index); } 1261 1262 bool hasMaxLength(Handle sh, size_t l) 1263 const { 1264 uint idx = sh.index; 1265 do { 1266 if (idx == uint.max) return true; 1267 idx = m_storage[idx].next; 1268 } while (l-- > 0); 1269 return false; 1270 } 1271 1272 bool hasLength(Handle sh, size_t l) 1273 const { 1274 uint idx = sh.index; 1275 while (l-- > 0) { 1276 if (idx == uint.max) return false; 1277 idx = m_storage[idx].next; 1278 } 1279 return idx == uint.max; 1280 } 1281 1282 private uint createNode(ref T val, uint next) 1283 { 1284 auto id = cast(uint)m_storage.length; 1285 m_storage ~= Entry(next, val); 1286 return id; 1287 } 1288 } 1289 1290 unittest { 1291 import std.algorithm.comparison : equal; 1292 1293 LinkedSetBacking!int b; 1294 auto s = b.emptySet; 1295 assert(s.empty); 1296 assert(b.getItems(s).empty); 1297 s = b.create(3, 5, 7); 1298 assert(b.getItems(s).equal([7, 5, 3])); 1299 assert(!b.hasLength(s, 2)); 1300 assert(b.hasLength(s, 3)); 1301 assert(!b.hasLength(s, 4)); 1302 assert(!b.hasMaxLength(s, 2)); 1303 assert(b.hasMaxLength(s, 3)); 1304 assert(b.hasMaxLength(s, 4)); 1305 1306 auto h = b.getHash(s); 1307 assert(h != b.getHash(b.emptySet)); 1308 s = b.create(5, 3, 7); 1309 assert(b.getHash(s) == h); 1310 assert(b.getItems(s).equal([7, 3, 5])); 1311 1312 b.insert(&s, 11); 1313 assert(b.hasLength(s, 4)); 1314 assert(b.getHash(s) != h); 1315 assert(b.getItems(s).equal([11, 7, 3, 5])); 1316 } 1317 1318 alias LinkedSetHash = ulong[16/ulong.sizeof]; 1319 1320 private struct Stack(E) 1321 { 1322 import std.range : isInputRange; 1323 1324 private { 1325 E[] m_storage; 1326 size_t m_fill; 1327 debug size_t m_maxFill; 1328 } 1329 1330 @property bool empty() const { return m_fill == 0; } 1331 1332 @property size_t fill() const { return m_fill; } 1333 1334 debug @property size_t maxSize() const { return m_maxFill; } 1335 1336 void reserve(size_t amt) 1337 { 1338 auto minsz = m_fill + amt; 1339 if (m_storage.length < minsz) { 1340 auto newlength = 64; 1341 while (newlength < minsz) newlength *= 2; 1342 m_storage.length = newlength; 1343 } 1344 } 1345 1346 void push(E el) 1347 { 1348 reserve(1); 1349 m_storage[m_fill++] = el; 1350 debug if (m_fill > m_maxFill) m_maxFill = m_fill; 1351 } 1352 1353 void push(R)(R els) 1354 if (isInputRange!R) 1355 { 1356 reserve(els.length); 1357 foreach (el; els) 1358 m_storage[m_fill++] = el; 1359 debug if (m_fill > m_maxFill) m_maxFill = m_fill; 1360 } 1361 1362 E pop() 1363 { 1364 assert(!empty, "Stack underflow."); 1365 return m_storage[--m_fill]; 1366 } 1367 }