1 /** 2 * Copyright © DiamondMVC 2019 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.controllers.controller; 7 8 import diamond.core.apptype; 9 10 static if (isWeb) 11 { 12 import std..string : strip, format; 13 import std.traits : fullyQualifiedName, hasUDA, getUDAs, Parameters, ParameterIdentifierTuple; 14 import std.array : split, array, join; 15 import std.conv : to; 16 17 public import diamond.http; 18 public import diamond.controllers.authentication; 19 public import diamond.authentication; 20 21 import diamond.controllers.action; 22 import diamond.controllers.status; 23 import diamond.controllers.basecontroller; 24 import diamond.controllers.attributes; 25 import diamond.core.collections; 26 import diamond.core..string : firstToLower; 27 import diamond.errors; 28 import diamond.controllers.rest; 29 import diamond.security; 30 31 package(diamond.controllers) 32 { 33 /// Specially mapped routes. 34 RoutePart[][string] _mappedRoutes; 35 /// Mapped controllers. 36 HashSet!string _mappedControllers; 37 } 38 39 import models; 40 41 /// The format used for default mappings. 42 enum defaultMappingFormat = q{ 43 static if (hasUDA!(%1$s.%2$s, HttpDefault)) 44 { 45 mapDefault(&controller.%2$s); 46 } 47 }; 48 49 /// The format used for mandatory formats. 50 enum mandatoryMappingFormat = q{ 51 static if (hasUDA!(%1$s.%2$s, HttpMandatory)) 52 { 53 mapMandatory(&controller.%2$s); 54 } 55 }; 56 57 /// The format used for no-action formats. 58 enum noActionMappingFormat = q{ 59 static if (hasUDA!(%1$s.%2$s, HttpNoAction)) 60 { 61 mapNoAction(&controller.%2$s); 62 } 63 }; 64 65 /// The format for creating the http acton member. 66 enum actionNameFormat = "static HttpAction action_%2$s;\r\n"; 67 68 /// The format used for actions. 69 enum actionMappingFormat = q{ 70 static if (hasUDA!(%1$s.%2$s, HttpAction)) 71 { 72 static action_%2$s = getUDAs!(%1$s.%2$s, HttpAction)[0]; 73 74 if (action_%2$s.action && action_%2$s.action.strip().length) 75 { 76 auto routingData = _mappedRoutes.get("%2$s", null); 77 78 if (!routingData && !_mappedControllers[fullyQualifiedName!TController]) 79 { 80 routingData = parseRoute(action_%2$s.action); 81 82 if (routingData && routingData.length > 1) 83 { 84 _mappedRoutes["%2$s"] = routingData; 85 86 action_%2$s.action = routingData[0].identifier; 87 } 88 } 89 90 if (routingData && routingData.length) 91 { 92 if (routingData[0].identifier == "<>") 93 { 94 action_%2$s.action = null; 95 } 96 } 97 } 98 99 enum parameterTypes_%2$s = Parameters!(controller.%2$s).stringof[1..$-1].split(", "); 100 101 static if (parameterTypes_%2$s.length) 102 { 103 template isJsonObject(T) 104 { 105 static if (is(T == struct) || is(T == class)) 106 { 107 enum isJsonObject = true; 108 } 109 else 110 { 111 enum isJsonObject = false; 112 } 113 } 114 115 mixin("enum isJson_%2$s = isJsonObject!" ~ parameterTypes_%2$s[0] ~ ";"); 116 117 static if (isJson_%2$s) 118 { 119 static if(parameterTypes_%2$s.length == 1) 120 { 121 static if (hasUDA!(%1$s.%2$s, HttpSanitize)) 122 { 123 mapAction( 124 action_%2$s.method, 125 ( 126 action_%2$s.action && action_%2$s.action.strip().length ? 127 action_%2$s.action : "%2$s" 128 ).firstToLower(), 129 () { 130 mixin("return controller.%2$s(client.getSanitizedModelFromJson!" ~ (parameterTypes_%2$s[0]) ~ ");"); 131 } 132 ); 133 } 134 else 135 { 136 mapAction( 137 action_%2$s.method, 138 ( 139 action_%2$s.action && action_%2$s.action.strip().length ? 140 action_%2$s.action : "%2$s" 141 ).firstToLower(), 142 () { 143 mixin("return controller.%2$s(client.getModelFromJson!" ~ (parameterTypes_%2$s[0]) ~ ");"); 144 } 145 ); 146 } 147 } 148 else 149 { 150 static assert(0, "Can only map a single json object."); 151 } 152 } 153 else 154 { 155 enum parameterNames_%2$s = [ParameterIdentifierTuple!(controller.%2$s)]; 156 157 mapAction( 158 action_%2$s.method, 159 ( 160 action_%2$s.action && action_%2$s.action.strip().length ? 161 action_%2$s.action : "%2$s" 162 ).firstToLower(), 163 () { 164 static foreach (i; 0 .. parameterNames_%2$s.length) 165 { 166 static if (hasUDA!(%1$s.%2$s, HttpQuery)) 167 { 168 static if (hasUDA!(%1$s.%2$s, HttpSanitize)) 169 { 170 mixin("auto " ~ parameterNames_%2$s[i] ~ " = to!(" ~ (parameterTypes_%2$s[i]) ~ ")(escapeHtml(client.query.get(\"" ~ parameterNames_%2$s[i] ~ "\")));"); 171 } 172 else 173 { 174 mixin("auto " ~ parameterNames_%2$s[i] ~ " = to!(" ~ (parameterTypes_%2$s[i]) ~ ")(client.query.get(\"" ~ parameterNames_%2$s[i] ~ "\"));"); 175 } 176 } 177 else static if (hasUDA!(%1$s.%2$s, HttpForm)) 178 { 179 static if (hasUDA!(%1$s.%2$s, HttpSanitize)) 180 { 181 mixin("auto " ~ parameterNames_%2$s[i] ~ " = to!(" ~ (parameterTypes_%2$s[i]) ~ ")(escapeHtml(client.form.get(\"" ~ parameterNames_%2$s[i] ~ "\")));"); 182 } 183 else 184 { 185 mixin("auto " ~ parameterNames_%2$s[i] ~ " = to!(" ~ (parameterTypes_%2$s[i]) ~ ")(client.form.get(\"" ~ parameterNames_%2$s[i] ~ "\"));"); 186 } 187 } 188 else 189 { 190 mixin(parameterTypes_%2$s[i] ~ " " ~ parameterNames_%2$s[i] ~ ";"); 191 192 static if (hasUDA!(%1$s.%2$s, HttpSanitize) && parameterTypes_%2$s[i] == "string") 193 { 194 if (action_%2$s.action && action_%2$s.action.length) 195 { 196 mixin(parameterNames_%2$s[i] ~ " = escapeHtml(get!(" ~ (parameterTypes_%2$s[i]) ~ ")(\"" ~ parameterNames_%2$s[i] ~ "\"));"); 197 } 198 else 199 { 200 mixin(parameterNames_%2$s[i] ~ " = escapeHtml(getByIndex!(" ~ (parameterTypes_%2$s[i]) ~ ")(\"" ~ to!string(i) ~ "\"));"); 201 } 202 } 203 else 204 { 205 if (action_%2$s.action && action_%2$s.action.length) 206 { 207 mixin(parameterNames_%2$s[i] ~ " = get!(" ~ (parameterTypes_%2$s[i]) ~ ")(\"" ~ parameterNames_%2$s[i] ~ "\");"); 208 } 209 else 210 { 211 mixin(parameterNames_%2$s[i] ~ " = getByIndex!(" ~ (parameterTypes_%2$s[i]) ~ ")(\"" ~ to!string(i) ~ "\");"); 212 } 213 } 214 } 215 } 216 217 mixin("return controller.%2$s(" ~ (parameterNames_%2$s.join(",")) ~ ");"); 218 } 219 ); 220 } 221 } 222 else 223 { 224 mapAction( 225 action_%2$s.method, 226 ( 227 action_%2$s.action && action_%2$s.action.strip().length ? 228 action_%2$s.action : "%2$s" 229 ).firstToLower(), 230 &controller.%2$s 231 ); 232 } 233 } 234 }; 235 236 /// The format used for disabled authentication. 237 enum disableAuthFormat = q{ 238 static if (hasUDA!(%1$s.%2$s, HttpDisableAuth)) 239 { 240 static if (hasUDA!(%1$s.%2$s, HttpDefault)) 241 { 242 _disabledAuth.add("/"); 243 } 244 else static if (hasUDA!(%1$s.%2$s, HttpAction)) 245 { 246 _disabledAuth.add( 247 ( 248 action_%2$s.action && action_%2$s.action.strip().length ? 249 action_%2$s.action : "%2$s" 250 ).firstToLower() 251 ); 252 } 253 } 254 }; 255 256 /// The format used for restricted connections. 257 enum restrictedFormat = q{ 258 static if (hasUDA!(%1$s.%2$s, HttpRestricted)) 259 { 260 static if (hasUDA!(%1$s.%2$s, HttpDefault)) 261 { 262 _restrictedActions.add("/"); 263 } 264 else static if (hasUDA!(%1$s.%2$s, HttpAction)) 265 { 266 _restrictedActions.add( 267 ( 268 action_%2$s.action && action_%2$s.action.strip().length ? 269 action_%2$s.action : "%2$s" 270 ).firstToLower() 271 ); 272 } 273 } 274 }; 275 } 276 277 // WebServer's will have a view associated with the controller, the view then contains information about the request etc. 278 static if (isWebServer) 279 { 280 public import diamond.views.view; 281 282 /// Wrapper around a controller. 283 class WebController(TView) : BaseController 284 { 285 private: 286 /// The view associatedi with the controller. 287 TView _view; 288 289 /// The client associated with the controller. 290 HttpClient _client; 291 292 /// The authentication used for the controller. 293 IControllerAuth _auth; 294 295 /// Hash set of actions with disabled authentication. 296 HashSet!string _disabledAuth; 297 298 /// Hash set of actions with restrictions. 299 HashSet!string _restrictedActions; 300 301 /// The successor to this controller. 302 BaseController _successorController; 303 304 /// The version in which the successor controller can be used. 305 string _successorVersion; 306 307 protected: 308 /** 309 * Creates a new controller. 310 * Params: 311 * view = The view associated with the controller. 312 */ 313 this(this TController)(TView view) 314 { 315 super(); 316 317 _view = view; 318 _client = _view.client; 319 320 mixin("import diamondapp : " ~ TController.stringof.split("!")[1][1 .. $-1] ~ ";"); 321 322 import controllers; 323 auto controller = cast(TController)this; 324 325 static if (hasUDA!(TController, HttpAuthentication)) 326 { 327 enum authenticationUDA = getUDAs!(TController, HttpAuthentication)[0]; 328 329 mixin("_auth = new " ~ authenticationUDA.authenticationClass ~ ";"); 330 _disabledAuth = new HashSet!string; 331 } 332 333 static if (hasUDA!(TController, HttpVersion)) 334 { 335 import std..string : indexOf; 336 337 enum versionUDA = getUDAs!(TController, HttpVersion)[0]; 338 339 mixin 340 ( 341 "_successorController = new " ~ 342 versionUDA.versionControllerClass[0 .. versionUDA.versionControllerClass.indexOf('(')] ~ 343 "!" ~ TView.stringof ~ "(_view);" 344 ); 345 _successorVersion = versionUDA.versionName; 346 } 347 348 _restrictedActions = new HashSet!string; 349 350 auto fullName = fullyQualifiedName!TController; 351 352 if (!_mappedControllers) 353 { 354 _mappedControllers = new HashSet!string; 355 } 356 357 foreach (member; __traits(derivedMembers, TController)) 358 { 359 static if (member != "__ctor") 360 { 361 mixin(defaultMappingFormat.format(TController.stringof, member)); 362 mixin(mandatoryMappingFormat.format(TController.stringof, member)); 363 mixin(noActionMappingFormat.format(TController.stringof, member)); 364 mixin(actionMappingFormat.format(TController.stringof, member)); 365 mixin(disableAuthFormat.format(TController.stringof, member)); 366 mixin(restrictedFormat.format(TController.stringof, member)); 367 } 368 } 369 370 if (!_mappedControllers[fullName]) 371 { 372 _mappedControllers.add(fullName); 373 } 374 } 375 376 public: 377 final: 378 @property 379 { 380 /// Gets the view. 381 TView view() { return _view; } 382 383 /// Gets the client. 384 HttpClient client() { return _client; } 385 } 386 387 /** 388 * Generates a json response. 389 * Params: 390 * jsonObject = The object to serialize as json. 391 * Returns: 392 * A status of Status.end 393 */ 394 Status json(T)(T jsonObject) 395 { 396 import vibe.d : serializeToJsonString; 397 return jsonString(jsonObject.serializeToJsonString()); 398 } 399 400 /** 401 * Generates a json response from a json string. 402 * Params: 403 * s = The json string. 404 * Returns: 405 * A status of Status.end 406 */ 407 Status jsonString(string s) 408 { 409 import diamond.core.webconfig; 410 foreach (headerKey,headerValue; webConfig.defaultHeaders.general) 411 { 412 _view.client.rawResponse.headers[headerKey] = headerValue; 413 } 414 415 _view.client.rawResponse.headers["Content-Type"] = "application/json; charset=UTF-8"; 416 _view.client.write(s); 417 418 return Status.end; 419 } 420 421 /** 422 * Redirects the response to a specific url. 423 * Params: 424 * url = The url to redirect to. 425 * status = The status of the redirection. (Default is HTTPStatus.Found) 426 * Returns: 427 * The status required for the redirection to work properly. (Status.end) 428 */ 429 Status redirectTo(string url, HttpStatus status = HttpStatus.found) 430 { 431 _view.client.redirect(url, status); 432 433 return Status.end; 434 } 435 436 /** 437 * Gets a value from the route's parameters by index. 438 * Params: 439 * index = The index. 440 * defaultValue = The default value. 441 * Returns: 442 * The value from the route's parameters if found, else the default value. 443 */ 444 T getByIndex(T)(size_t index, T defaultValue = T.init) 445 { 446 if (index < 0 || index >= _view.route.params.length) 447 { 448 return defaultValue; 449 } 450 451 return _view.route.getData!T(index); 452 } 453 454 /** 455 * Handles the view's current controller action. 456 * Returns: 457 * The status of the controller action. 458 */ 459 final override Status handle() 460 { 461 if (_successorController && _view.client.route.action == _successorVersion) 462 { 463 _view.client.route.passDataToAction(); 464 465 auto status = _successorController.handle(); 466 467 if (status != Status.notFound) 468 { 469 return status; 470 } 471 } 472 473 if (_view.isDefaultRoute) 474 { 475 if (_restrictedActions["/"]) 476 { 477 validateRestrictedIPs(view.client); 478 } 479 480 if (_auth && !_disabledAuth["/"]) 481 { 482 auto authStatus = _auth.isAuthenticated(view.client); 483 484 if (!authStatus || !authStatus.authenticated) 485 { 486 _auth.authenticationFailed(authStatus); 487 return Status.end; 488 } 489 } 490 491 if (_mandatoryAction) 492 { 493 auto mandatoryResult = _mandatoryAction(); 494 495 if (mandatoryResult != Status.success) 496 { 497 return mandatoryResult; 498 } 499 } 500 501 if (_defaultAction) 502 { 503 return _defaultAction(); 504 } 505 506 if (_noAction) 507 { 508 auto noActionResult = _noAction(); 509 510 return noActionResult; 511 } 512 513 return Status.success; 514 } 515 516 ActionEntry methodEntries = _actions.get(_view.httpMethod, null); 517 518 if (!methodEntries) 519 { 520 if (_noAction) 521 { 522 auto noActionResult = _noAction(); 523 524 return noActionResult; 525 } 526 527 return Status.notFound; 528 } 529 530 auto action = methodEntries.get(_view.route.action, null); 531 532 if (!action) 533 { 534 if (_noAction) 535 { 536 auto noActionResult = _noAction(); 537 538 return noActionResult; 539 } 540 541 return Status.notFound; 542 } 543 544 if (_restrictedActions[_view.route.action]) 545 { 546 validateRestrictedIPs(view.client); 547 } 548 549 if (_auth && !_disabledAuth[_view.route.action]) 550 { 551 auto authStatus = _auth.isAuthenticated(view.client); 552 553 if (!authStatus || !authStatus.authenticated) 554 { 555 _auth.authenticationFailed(authStatus); 556 return Status.end; 557 } 558 } 559 560 if (_mandatoryAction) 561 { 562 auto mandatoryResult = _mandatoryAction(); 563 564 if (mandatoryResult != Status.success) 565 { 566 return mandatoryResult; 567 } 568 } 569 570 auto routeData = _mappedRoutes.get(_view.route.action, null); 571 572 if (routeData) 573 { 574 validateRoute(routeData, view.route.params); 575 } 576 577 return action(); 578 } 579 580 import diamond.extensions; 581 mixin ExtensionEmit!(ExtensionType.controllerExtension, q{ 582 mixin {{extensionEntry}}.extensions; 583 }); 584 } 585 } 586 587 // A webapi will not have a view associated with it, thus all information such as the request etc. is available within the controller 588 static if (isWebApi) 589 { 590 /// Wrapper around a controller. 591 class ApiController : BaseController 592 { 593 private: 594 /// The client. 595 HttpClient _client; 596 597 /// The authentication used for the controller. 598 IControllerAuth _auth; 599 600 /// Hash set of actions with disabled authentication. 601 HashSet!string _disabledAuth; 602 603 /// Hash set of actions with restrictions. 604 HashSet!string _restrictedActions; 605 606 /// The successor to this controller. 607 BaseController _successorController; 608 609 /// The version in which the successor controller can be used. 610 string _successorVersion; 611 612 protected: 613 /** 614 * Creates a new controller. 615 * Params: 616 * client = The client of the controller. 617 * controller = The controller itself 618 */ 619 this(this TController)(HttpClient client) 620 { 621 super(); 622 623 _client = client; 624 625 import controllers; 626 auto controller = cast(TController)this; 627 628 static if (hasUDA!(TController, HttpAuthentication)) 629 { 630 enum authenticationUDA = getUDAs!(TController, HttpAuthentication)[0]; 631 632 mixin("_auth = new " ~ authenticationUDA.authenticationClass ~ ";"); 633 _disabledAuth = new HashSet!string; 634 } 635 636 static if (hasUDA!(TController, HttpVersion)) 637 { 638 enum versionUDA = getUDAs!(TController, HttpVersion)[0]; 639 640 mixin("_successorController = new " ~ versionUDA.versionControllerClass ~ "(_client);"); 641 _successorVersion = versionUDA.versionName; 642 } 643 644 _restrictedActions = new HashSet!string; 645 646 auto fullName = fullyQualifiedName!TController; 647 648 if (!_mappedControllers) 649 { 650 _mappedControllers = new HashSet!string; 651 } 652 653 foreach (member; __traits(derivedMembers, TController)) 654 { 655 static if (member != "__ctor") 656 { 657 mixin(defaultMappingFormat.format(TController.stringof, member)); 658 mixin(mandatoryMappingFormat.format(TController.stringof, member)); 659 mixin(noActionMappingFormat.format(TController.stringof, member)); 660 mixin(actionMappingFormat.format(TController.stringof, member)); 661 mixin(disableAuthFormat.format(TController.stringof, member)); 662 mixin(restrictedFormat.format(TController.stringof, member)); 663 } 664 } 665 666 if (!_mappedControllers[fullName]) 667 { 668 _mappedControllers.add(fullName); 669 } 670 } 671 672 public: 673 final: 674 @property 675 { 676 /// Gets the client. 677 auto client() { return _client; } 678 /// Gets the http method. 679 auto httpMethod() { return _client.method; } 680 /// Gets the route. 681 auto route() { return client.route; } 682 } 683 684 /** 685 * Generates a json response. 686 * Params: 687 * jsonObject = The object to serialize as json. 688 * Returns: 689 * A status of Status.end 690 */ 691 Status json(T)(T jsonObject) 692 { 693 import vibe.d : serializeToJsonString; 694 695 return jsonString(jsonObject.serializeToJsonString()); 696 } 697 698 /** 699 * Generates a json response from a json string. 700 * Params: 701 * s = The json string. 702 * Returns: 703 * A status of Status.end 704 */ 705 Status jsonString(string s) 706 { 707 import diamond.core.webconfig; 708 foreach (headerKey,headerValue; webConfig.defaultHeaders.general) 709 { 710 _client.rawResponse.headers[headerKey] = headerValue; 711 } 712 713 _client.rawResponse.headers["Content-Type"] = "application/json; charset=UTF-8"; 714 _client.write(s); 715 716 return Status.end; 717 } 718 719 /** 720 * Redirects the response to a specific url. 721 * Params: 722 * url = The url to redirect to. 723 * status = The status of the redirection. (Default is HTTPStatus.Found) 724 * Returns: 725 * The status required for the redirection to work properly. (Status.end) 726 */ 727 Status redirectTo(string url, HttpStatus status = HttpStatus.found) 728 { 729 _client.redirect(url, status); 730 731 return Status.end; 732 } 733 734 /** 735 * Gets a value from the route's parameters by index. 736 * Params: 737 * index = The index. 738 * defaultValue = The default value. 739 * Returns: 740 * The value from the route's parameters if found, else the default value. 741 */ 742 T getByIndex(T)(size_t index, T defaultValue = T.init) 743 { 744 if (index < 0 || index >= route.params.length) 745 { 746 return defaultValue; 747 } 748 749 return route.getData!T(index); 750 } 751 752 /** 753 * Handles the current controller action. 754 * Returns: 755 * The status of the controller action. 756 */ 757 final override Status handle() 758 { 759 if (_successorController && route.action == _successorVersion) 760 { 761 route.passDataToAction(); 762 763 auto status = _successorController.handle(); 764 765 if (status != Status.notFound) 766 { 767 return status; 768 } 769 } 770 771 if (!route.action) 772 { 773 if (_restrictedActions["/"]) 774 { 775 validateRestrictedIPs(_client); 776 } 777 778 if (_auth && !_disabledAuth["/"]) 779 { 780 auto authStatus = _auth.isAuthenticated(_client); 781 782 if (!authStatus || !authStatus.authenticated) 783 { 784 _auth.authenticationFailed(authStatus); 785 return Status.end; 786 } 787 } 788 789 if (_mandatoryAction) 790 { 791 auto mandatoryResult = _mandatoryAction(); 792 793 if (mandatoryResult != Status.success) 794 { 795 return mandatoryResult; 796 } 797 } 798 799 if (_defaultAction) 800 { 801 return _defaultAction(); 802 } 803 804 if (_noAction) 805 { 806 auto noActionResult = _noAction(); 807 808 return noActionResult; 809 } 810 811 return Status.notFound; 812 } 813 814 ActionEntry methodEntries = _actions.get(httpMethod, null); 815 816 if (!methodEntries) 817 { 818 if (_noAction) 819 { 820 auto noActionResult = _noAction(); 821 822 return noActionResult; 823 } 824 825 return Status.notFound; 826 } 827 828 auto action = methodEntries.get(route.action, null); 829 830 if (!action) 831 { 832 if (_noAction) 833 { 834 auto noActionResult = _noAction(); 835 836 return noActionResult; 837 } 838 839 return Status.notFound; 840 } 841 842 if (_restrictedActions[route.action]) 843 { 844 validateRestrictedIPs(client); 845 } 846 847 if (_auth && !_disabledAuth[route.action]) 848 { 849 auto authStatus = _auth.isAuthenticated(client); 850 851 if (!authStatus || !authStatus.authenticated) 852 { 853 _auth.authenticationFailed(authStatus); 854 return Status.end; 855 } 856 } 857 858 if (_mandatoryAction) 859 { 860 auto mandatoryResult = _mandatoryAction(); 861 862 if (mandatoryResult != Status.success) 863 { 864 return mandatoryResult; 865 } 866 } 867 868 auto routeData = _mappedRoutes.get(route.action, null); 869 870 if (routeData) 871 { 872 validateRoute(routeData, route.params); 873 } 874 875 return action(); 876 } 877 878 import diamond.extensions; 879 mixin ExtensionEmit!(ExtensionType.controllerExtension, q{ 880 mixin {{extensionEntry}}.extensions; 881 }); 882 } 883 884 /// Mixin template for generating the controllers. 885 mixin template GenerateControllers(string[] controllerInitializers) 886 { 887 import std.traits : hasUDA, getUDAs; 888 import controllers; 889 890 /// Format for generating the routes for controllers. 891 private enum generateFormat = q{ 892 static if (hasUDA!(%sController, HttpRoutes)) 893 { 894 static const controller_%s = getUDAs!(%sController, HttpRoutes)[0]; 895 896 foreach (controllerRoute; controller_%s.routes) 897 { 898 import diamond.core..string : firstToLower; 899 900 controllerCollection[controllerRoute.firstToLower()] = new GenerateControllerAction((client) 901 { 902 return new %sController(client); 903 }); 904 } 905 } 906 else 907 { 908 controllerCollection["%s"] = new GenerateControllerAction((client) 909 { 910 return new %sController(client); 911 }); 912 } 913 }; 914 915 /// Generates the controller collection. 916 string generateControllerCollection() 917 { 918 auto controllerCollectionResult = ""; 919 920 foreach (controller; controllerInitializers) 921 { 922 import std..string : format; 923 controllerCollectionResult ~= 924 format 925 ( 926 generateFormat, 927 controller, controller, controller, 928 controller, controller, 929 controller.firstToLower(), 930 controller 931 ); 932 } 933 934 return controllerCollectionResult; 935 } 936 937 /// The controller collection. 938 GenerateControllerAction[string] controllerCollection; 939 940 /// Gets a controller by its name 941 GenerateControllerAction getControllerAction(string name) 942 { 943 if (!controllerCollection || !controllerCollection.length) 944 { 945 mixin(generateControllerCollection); 946 } 947 948 return controllerCollection.get(name, null); 949 } 950 } 951 }