1 module vibrant.router; 2 3 import std.algorithm; 4 import std.conv; 5 import std.functional; 6 import std.traits; 7 import std.typecons; 8 import std.typetuple; 9 10 import vibe.d; 11 12 import vibrant.helper; 13 14 // TODO : This is probably a hack. 15 extern (C) int _d_isbaseof(ClassInfo oc, ClassInfo c); 16 17 /++ 18 + The vibrant router class. 19 ++/ 20 class VibrantRouter(bool GenerateAll = false) : HTTPServerRequestHandler { 21 /++ 22 + The URL router that manages Vibrant's routes. 23 ++/ 24 URLRouter router; 25 26 private { 27 28 /++ 29 + An internal throwable type used to halt execution. 30 ++/ 31 class HaltThrowable : Throwable { 32 33 /++ 34 + The status code sent in the response. 35 ++/ 36 private int status; 37 38 /++ 39 + Constructs a HaltThrowable. 40 + 41 + Params: 42 + status = The status code to send to the client. 43 + msg = A message body to include in the response. 44 ++/ 45 this(int status, string msg) { 46 super(msg); 47 this.status = status; 48 } 49 50 } 51 52 /++ 53 + A saved listener, used to stop and restart the server. 54 ++/ 55 Nullable!HTTPListener savedListener; 56 57 /++ 58 + Filter callbacks invoked before a route handler. 59 ++/ 60 VoidCallback[][string] beforeCallbacks; 61 62 /++ 63 + Filter callbacks invoked after a route handler. 64 ++/ 65 VoidCallback[][string] afterCallbacks; 66 67 /++ 68 + A table storing exception callbacks. 69 ++/ 70 ExceptionCallback[ClassInfo] exceptionCallbacks; 71 72 /++ 73 + Tests if a type is a valid result from a callback. 74 ++/ 75 template isValidResultType(Result) { 76 enum isValidResultType = 77 is(Result == const(ubyte[])) || 78 is(Result == ubyte[]) || 79 is(Result == string) || 80 is(Result == void); 81 } 82 83 /++ 84 + Tests if a type is a valid result from a transform function. 85 ++/ 86 template isValidTransformedType(Temp) { 87 enum isValidTransformedType = 88 is(Temp == const(ubyte[])) || 89 is(Temp == ubyte[]) || 90 is(Temp == string); 91 } 92 93 } 94 95 /++ 96 + Constructs a new vibrant router. 97 + 98 + Params: 99 + router = A URLRouter to forward requests to. 100 ++/ 101 package this(URLRouter router) { 102 this.router = router; 103 104 // Preload the HaltThrowable handler. 105 Catch(HaltThrowable.classinfo, (t, req, res) { 106 // Get the HaltThrowable object. 107 HaltThrowable ht = cast(HaltThrowable) t; 108 109 // Check for a status code. 110 if (ht.status != 0) { 111 res.statusCode = ht.status; 112 } 113 114 // Write the response body. 115 res.writeBody(ht.msg); 116 }); 117 } 118 119 /++ 120 + Constructs a new vibrant router and starts listening for connections. 121 + 122 + Params: 123 + settings = The settings for the HTTP server. 124 + prefix = The prefix to place all routes at. 125 ++/ 126 package this(HTTPServerSettings settings, string prefix) { 127 this(new URLRouter(prefix)); 128 savedListener = listenHTTP(settings, router); 129 } 130 131 /++ 132 + Forwards a requrest to the internal URLRouter. 133 + 134 + Params: 135 + req = The HTTPServerRequest object. 136 + res = The HTTPServerResponse object. 137 ++/ 138 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) { 139 router.handleRequest(req, res); 140 } 141 142 /++ 143 + Produces a child of the current scope with the given prefix. 144 + 145 + Params: 146 + prefix = The prefix the scope operates from. 147 + 148 + Returns: 149 + A new VibrantRouter object. 150 ++/ 151 auto Scope(string prefix) { 152 // Create a subrouter and forward requests that match its prefix. 153 auto subrouter = new URLRouter(router.prefix ~ prefix); 154 router.any("*", subrouter); 155 156 // Create a new child vibrant router, and pass along the listener. 157 auto newRouter = new VibrantRouter!GenerateAll(subrouter); 158 newRouter.savedListener = savedListener; 159 return newRouter; 160 } 161 162 /++ 163 + Instantly updates the installed routes (instead of lazily). 164 ++/ 165 void Flush() { 166 router.rebuild; 167 } 168 169 /++ 170 + Instantly stops the server. 171 ++/ 172 void Stop() { 173 savedListener.get.stopListening; 174 savedListener.nullify; 175 } 176 177 /++ 178 + Attaches a handler to an exception type. 179 + 180 + Params: 181 + type = The type of exception to catch. 182 + callback = The handler for the exception. 183 ++/ 184 void Catch(ClassInfo type, ExceptionCallback callback) { 185 // Add the callback to the type list. 186 exceptionCallbacks[type] = callback; 187 } 188 189 /++ 190 + Adds a filter to all paths which is called before the handler. 191 + 192 + Params: 193 + callback = The filter that handles the event. 194 ++/ 195 void Before(VoidCallback callback) { 196 addFilterCallback(beforeCallbacks, null, callback); 197 } 198 199 /++ 200 + Adds a filter to the given path which is called before the handler. 201 + 202 + Params: 203 + path = The path that this filter is specific to. 204 + callback = The filter that handles the event. 205 ++/ 206 void Before(string path, VoidCallback callback) { 207 addFilterCallback(beforeCallbacks, path, callback); 208 } 209 210 /++ 211 + Adds a filter to all paths which is called after the handler. 212 + 213 + Params: 214 + callback = The filter that handles the event. 215 ++/ 216 void After(VoidCallback callback) { 217 addFilterCallback(afterCallbacks, null, callback); 218 } 219 220 /++ 221 + Adds a filter to the given path which is called after the handler. 222 + 223 + Params: 224 + path = The path that this filter is specific to. 225 + callback = The filter that handles the event. 226 ++/ 227 void After(string path, VoidCallback callback) { 228 addFilterCallback(afterCallbacks, path, callback); 229 } 230 231 /++ 232 + Halt execution of a route or filter handler. 233 + Halt uses a HaltThrowable. If caught, it should be re-thrown 234 + to properly stop exection of a callback. 235 + 236 + Params: 237 + message = A message body to optionally include. Defaults to empty. 238 ++/ 239 void halt(string message = "") { 240 throw new HaltThrowable(0, message); 241 } 242 243 /++ 244 + Halt execution of a route or filter handler. 245 + Halt uses a HaltThrowable. If caught, it should be re-thrown 246 + to properly stop exection of a callback. 247 + 248 + Params: 249 + status = The status code sent with the message. 250 + message = A message body to optionally include. Defaults to empty. 251 ++/ 252 void halt(int status, string message = "") { 253 throw new HaltThrowable(status, message); 254 } 255 256 void Resource(Type)() { 257 Type.install(this); 258 } 259 260 void Resources(TList...)() { 261 foreach (Type; TList) { 262 Resource!Type; 263 } 264 } 265 266 /++ 267 + Adds a handler for all method types on the given path. 268 + 269 + Params: 270 + path = The path that gets handled. 271 + callback = The handler that gets called for requests. 272 ++/ 273 void Any(Result)(string path, 274 Result function(HTTPServerRequest, HTTPServerResponse) callback) 275 if (isValidResultType!Result) { 276 return Any!(Result)(path, null, callback); 277 } 278 279 /++ 280 + Adds a handler for all method types on the given path. 281 + 282 + Params: 283 + path = The path that gets handled. 284 + contentType = The content type header to include in the response. 285 + callback = The handler that gets called for requests. 286 ++/ 287 void Any(Result)(string path, string contentType, 288 Result function(HTTPServerRequest, HTTPServerResponse) callback) 289 if (isValidResultType!Result) { 290 foreach (method; EnumMembers!HTTPMethod) { 291 // Match each HTTP method type. 292 Match(method, path, contentType, callback); 293 } 294 } 295 296 /++ 297 + Adds a handler for all method types on the given path. 298 + 299 + Params: 300 + path = The path that gets handled. 301 + callback = The handler that gets called for requests. 302 ++/ 303 void Any(Result)(string path, string contentType, 304 Result delegate(HTTPServerRequest, HTTPServerResponse) callback) 305 if (isValidResultType!Result) { 306 return Any!(Result)(path, null, callback); 307 } 308 309 /++ 310 + Adds a handler for all method types on the given path. 311 + 312 + Params: 313 + path = The path that gets handled. 314 + contentType = The content type header to include in the response. 315 + callback = The handler that gets called for requests. 316 ++/ 317 void Any(Result)(string path, string contentType, 318 Result delegate(HTTPServerRequest, HTTPServerResponse) callback) 319 if (isValidResultType!Result) { 320 foreach (method; EnumMembers!HTTPMethod) { 321 // Match each HTTP method type. 322 Match(method, path, contentType, callback); 323 } 324 } 325 326 template Any(Temp) if (isValidTransformedType!Result) { 327 static if (!is(Temp == void)) { 328 /++ 329 + Adds a handler for all method types on the given path. 330 + 331 + Params: 332 + path = The path that gets handled. 333 + callback = The handler that gets called for requests. 334 + transformer = The transformer function that converts output. 335 ++/ 336 void Any(Result = string)(string path, 337 Temp function(HTTPServerRequest, HTTPServerResponse) callback, 338 Result function(Temp) transformer) 339 if (isValidTransformedType!Result) { 340 return Any!(Result)(path, null, callback, transformer); 341 } 342 343 /++ 344 + Adds a handler for all method types on the given path. 345 + 346 + Params: 347 + path = The path that gets handled. 348 + contentType = The content type header to include in the response. 349 + callback = The handler that gets called for requests. 350 + transformer = The transformer function that converts output. 351 ++/ 352 void Any(Result = string)(string path, string contentType, 353 Temp function(HTTPServerRequest, HTTPServerResponse) callback, 354 Result function(Temp) transformer) 355 if (isValidTransformedType!Result) { 356 foreach (method; EnumMembers!HTTPMethod) { 357 // Match each HTTP method type. 358 Match!(Temp)(method, path, contentType, callback, transformer); 359 } 360 } 361 362 /++ 363 + Adds a handler for all method types on the given path. 364 + 365 + Params: 366 + path = The path that gets handled. 367 + callback = The handler that gets called for requests. 368 + transformer = The transformer delegate that converts output. 369 ++/ 370 void Any(Result = string)(string path, 371 Temp delegate(HTTPServerRequest, HTTPServerResponse) callback, 372 Result delegate(Temp) transformer) 373 if (isValidTransformedType!Result) { 374 return Any!(Result)(path, null, callback, transformer); 375 } 376 377 /++ 378 + Adds a handler for all method types on the given path. 379 + 380 + Params: 381 + path = The path that gets handled. 382 + contentType = The content type header to include in the response. 383 + callback = The handler that gets called for requests. 384 + transformer = The transformer delegate that converts output. 385 ++/ 386 void Any(Result = string)(string path, string contentType, 387 Temp delegate(HTTPServerRequest, HTTPServerResponse) callback, 388 Result delegate(Temp) transformer) 389 if (isValidTransformedType!Result) { 390 foreach (method; EnumMembers!HTTPMethod) { 391 // Match each HTTP method type. 392 Match!(Temp)(method, path, contentType, callback, transformer); 393 } 394 } 395 } 396 } 397 398 /++ 399 + A template that generates the source for HTTP methods. 400 + 401 + Params: 402 + method = The name of the method to produce. 403 ++/ 404 private template GetHTTPMethodCode(string method) { 405 import std.string; 406 407 enum GetHTTPMethodCode = format(` 408 void %1$s(Result)(string path, 409 Result function(HTTPServerRequest, HTTPServerResponse) callback) 410 if(isValidResultType!Result) 411 { 412 %1$s!(Result)(path, null, callback); 413 } 414 415 void %1$s(Result)(string path, string contentType, 416 Result function(HTTPServerRequest, HTTPServerResponse) callback) 417 if(isValidResultType!Result) 418 { 419 Match(HTTPMethod.%2$s, path, contentType, callback); 420 } 421 422 void %1$s(Result)(string path, 423 Result delegate(HTTPServerRequest, HTTPServerResponse) callback) 424 if(isValidResultType!Result) 425 { 426 %1$s!(Result)(path, null, callback); 427 } 428 429 void %1$s(Result)(string path, string contentType, 430 Result delegate(HTTPServerRequest, HTTPServerResponse) callback) 431 if(isValidResultType!Result) 432 { 433 Match(HTTPMethod.%2$s, path, contentType, callback); 434 } 435 436 template %1$s(Temp) 437 if(!is(Temp == void)) 438 { 439 static if(!is(Temp == void)) 440 { 441 void %1$s(Result = string)(string path, 442 Temp function(HTTPServerRequest, HTTPServerResponse) callback, 443 Result function(Temp) transformer) 444 if(isValidTransformedType!Result) 445 { 446 %1$s!(Result)(path, null, callback, transformer); 447 } 448 449 void %1$s(Result = string)(string path, string contentType, 450 Temp function(HTTPServerRequest, HTTPServerResponse) callback, 451 Result function(Temp) transformer) 452 if(isValidTransformedType!Result) 453 { 454 Match!(Temp)( 455 HTTPMethod.%2$s, path, contentType, callback, transformer 456 ); 457 } 458 459 void %1$s(Result = string)(string path, 460 Temp delegate(HTTPServerRequest, HTTPServerResponse) callback, 461 Result delegate(Temp) transformer) 462 if(isValidTransformedType!Result) 463 { 464 %1$s!(Result)(path, null, callback, transformer); 465 } 466 467 void %1$s(Result = string)(string path, string contentType, 468 Temp delegate(HTTPServerRequest, HTTPServerResponse) callback, 469 Result delegate(Temp) transformer) 470 if(isValidTransformedType!Result) 471 { 472 Match!(Temp)( 473 HTTPMethod.%2$s, path, contentType, callback, transformer 474 ); 475 } 476 } 477 } 478 `,// The Titlecase function name. 479 method[0 .. 1].toUpper ~ method[1 .. $].toLower,// The UPPERCASE HTTP method name. 480 method.toUpper 481 ); 482 } 483 484 static if (GenerateAll) { 485 // Include all supported methods. 486 private enum HTTPEnabledMethodList = __traits(allMembers, HTTPMethod); 487 } else { 488 // Include only common methods. 489 private enum HTTPEnabledMethodList = TypeTuple!( 490 "GET", "POST", "PUT", "PATCH", "DELETE", 491 "HEAD", "OPTIONS", "CONNECT", "TRACE" 492 ); 493 } 494 495 // Generate methods. 496 mixin( 497 joiner([ 498 staticMap!( 499 GetHTTPMethodCode, 500 HTTPEnabledMethodList 501 ) 502 ]).text 503 ); 504 505 /++ 506 + Matches a path and method type using a function callback. 507 + 508 + Params: 509 + method = The HTTP method matched. 510 + path = The path assigned to this route. 511 + callback = A function callback handler for the route. 512 ++/ 513 void Match(Result)(HTTPMethod method, string path, 514 Result function(HTTPServerRequest, HTTPServerResponse) callback) 515 if (isValidResultType!Result) { 516 // Wrap the function in a delegate. 517 Match!(Result)(method, path, null, callback); 518 } 519 520 /++ 521 + Matches a path and method type using a function callback. 522 + 523 + Params: 524 + method = The HTTP method matched. 525 + path = The path assigned to this route. 526 + contentType = The content type header to include in the response. 527 + callback = A function callback handler for the route. 528 ++/ 529 void Match(Result)(HTTPMethod method, string path, string contentType, 530 Result function(HTTPServerRequest, HTTPServerResponse) callback) 531 if (isValidResultType!Result) { 532 // Wrap the function in a delegate. 533 Match!(Result)(method, path, contentType, toDelegate(callback)); 534 } 535 536 /++ 537 + Matches a path and method type using a delegate callback. 538 + 539 + Params: 540 + method = The HTTP method matched. 541 + path = The path assigned to this route. 542 + callback = A delegate callback handler for the route. 543 ++/ 544 void Match(Result)(HTTPMethod method, string path, 545 Result delegate(HTTPServerRequest, HTTPServerResponse) callback) 546 if (isValidResultType!Result) { 547 return Match!(Result)(method, path, null, callback); 548 } 549 550 /++ 551 + Matches a path and method type using a delegate callback. 552 + 553 + Params: 554 + method = The HTTP method matched. 555 + path = The path assigned to this route. 556 + contentType = The content type header to include in the response. 557 + callback = A delegate callback handler for the route. 558 ++/ 559 void Match(Result)(HTTPMethod method, string path, string contentType, 560 Result delegate(HTTPServerRequest, HTTPServerResponse) callback) 561 if (isValidResultType!Result) { 562 router.match(method, path, (HTTPServerRequest req, HTTPServerResponse res) { 563 try { 564 // Invoke before-filters. 565 applyFilterCallback(beforeCallbacks, path, req, res); 566 567 static if (!is(Result == void)) { 568 // Call the callback and save the result. 569 auto result = callback(req, res); 570 } else { 571 // Call the callback; no result. 572 callback(req, res); 573 auto result = ""; 574 } 575 576 // Invoke after-filters. 577 applyFilterCallback(afterCallbacks, path, req, res); 578 579 // Just send an empty response. 580 res.writeBody(result, contentType); 581 } catch (Throwable t) { 582 handleException(t, req, res); 583 } 584 }); 585 } 586 587 template Match(Temp) if (!is(Temp == void)) { 588 static if (!is(Temp == void)) { 589 /++ 590 + Matches a path and method type using a function callback. 591 + 592 + Params: 593 + method = The HTTP method matched. 594 + path = The path assigned to this route. 595 + callback = A function callback handler for the route. 596 + transformer = A transformer that converts the handler's output. 597 ++/ 598 void Match(Result = string)(HTTPMethod method, string path, 599 Temp function(HTTPServerRequest, HTTPServerResponse) callback, 600 Result function(Temp) transformer) 601 if (isValidTransformedType!Result) { 602 Match!(Result)(method, path, null, callback, transformer); 603 } 604 605 /++ 606 + Matches a path and method type using a function callback. 607 + 608 + Params: 609 + method = The HTTP method matched. 610 + path = The path assigned to this route. 611 + contentType = The content type header to include in the response. 612 + callback = A function callback handler for the route. 613 + transformer = A transformer that converts the handler's output. 614 ++/ 615 void Match(Result = string)(HTTPMethod method, string path, string contentType, 616 Temp function(HTTPServerRequest, HTTPServerResponse) callback, 617 Result function(Temp) transformer) 618 if (isValidTransformedType!Result) { 619 // Wrap the function in a delegate. 620 Match!(Result)( 621 method, path, contentType, toDelegate(callback), toDelegate(transformer) 622 ); 623 } 624 625 /++ 626 + Matches a path and method type using a delegate callback. 627 + 628 + Params: 629 + method = The HTTP method matched. 630 + path = The path assigned to this route. 631 + callback = A delegate callback handler for the route. 632 + transformer = A transformer that converts the handler's output. 633 ++/ 634 void Match(Result = string)(HTTPMethod method, string path, 635 Temp delegate(HTTPServerRequest, HTTPServerResponse) callback, 636 Result delegate(Temp) transformer) 637 if (isValidTransformedType!Result) { 638 Match!(Result)(method, path, null, callback, transformer); 639 } 640 641 /++ 642 + Matches a path and method type using a delegate callback. 643 + 644 + Params: 645 + method = The HTTP method matched. 646 + path = The path assigned to this route. 647 + contentType = The content type header to include in the response. 648 + callback = A delegate callback handler for the route. 649 + transformer = A transformer that converts the handler's output. 650 ++/ 651 void Match(Result = string)(HTTPMethod method, string path, string contentType, 652 Temp delegate(HTTPServerRequest, HTTPServerResponse) callback, 653 Result delegate(Temp) transformer) 654 if (isValidTransformedType!Result) { 655 router.match(method, path, (HTTPServerRequest req, HTTPServerResponse res) { 656 try { 657 // Invoke before-filters. 658 applyFilterCallback(beforeCallbacks, path, req, res); 659 660 // Transform the result into a string. 661 string result = transformer(callback(req, res)); 662 663 // Invoke after-filters. 664 applyFilterCallback(afterCallbacks, path, req, res); 665 666 // Just send the response. 667 res.writeBody(result, contentType); 668 } catch (Throwable t) { 669 handleException(t, req, res); 670 } 671 }); 672 } 673 } 674 } 675 676 private { 677 678 /++ 679 + Adds a filter to a filter callback table. 680 + 681 + Params: 682 + filterTable = The table to add the callback to. 683 + path = The path the callback runs on. 684 + callback = The callback to add. 685 ++/ 686 void addFilterCallback(ref VoidCallback[][string] filterTable,/+ @Nullable +/ 687 string path, VoidCallback callback) { 688 // Check if the path has callbacks. 689 auto ptr = path in filterTable; 690 691 if (ptr is null) { 692 filterTable[path] = [callback]; 693 } else { 694 *ptr ~= callback; 695 } 696 } 697 698 /++ 699 + Matches a filter to a path and invokes matched callbacks. 700 + 701 + Params: 702 + table = The table of callbacks to scan. 703 + path = The path to be matched. 704 + req = The server request object. 705 + res = The server response object. 706 ++/ 707 void applyFilterCallback(ref VoidCallback[][string] table, string path, 708 HTTPServerRequest req, HTTPServerResponse res) { 709 // Search for matching callbacks. 710 foreach (callbackPath, callbacks; table) { 711 bool matches = true; 712 713 if (callbackPath !is null) { 714 // Substitue wildwards. 715 import std.array : replace; 716 717 string pattern = callbackPath.replace("*", ".*?"); 718 719 // Check the pattern for a match. 720 import std.regex : matchFirst; 721 722 matches = !path.matchFirst(pattern).empty; 723 } 724 725 if (matches) { 726 // Invoke matched callbacks. 727 foreach (callback; callbacks) { 728 callback(req, res); 729 } 730 } 731 } 732 } 733 734 /++ 735 + Matches a throwable type and invokes its handler. 736 + 737 + Params: 738 + t = The throwable being matched. 739 + req = The server request object. 740 + res = The server response object. 741 ++/ 742 void handleException(Throwable t, HTTPServerRequest req, HTTPServerResponse res) { 743 foreach (typeinfo, handler; exceptionCallbacks) { 744 if (_d_isbaseof(t.classinfo, typeinfo)) { 745 // Forward error to handler. 746 handler(t, req, res); 747 return; 748 } 749 } 750 751 // Rethrow. 752 throw t; 753 } 754 755 } 756 757 }