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 }