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