1 /**
2 * Copyright © DiamondMVC 2018
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.controllers.basecontroller;
7 
8 import diamond.core.apptype;
9 
10 static if (isWeb)
11 {
12   import std.conv : to;
13   import std.variant : Variant;
14   import std.traits : EnumMembers;
15   import std.algorithm : filter;
16   import std.array : array;
17   import std..string : strip;
18 
19   import diamond.http;
20   import diamond.controllers.action;
21   import diamond.controllers.status;
22   import diamond.errors;
23   import diamond.core.collections;
24   import diamond.controllers.rest;
25 
26   /// Wrapper for a base controller.
27   abstract class BaseController
28   {
29     package(diamond.controllers)
30     {
31       /// Data passed by the request in a RESTful manner.
32       Variant[string] _data;
33 
34       /**
35       * Valiates a route and the passed source data to it.
36       * Params:
37       *   routeData =  The route part data.
38       *   sourceData = The data passed.
39       */
40       void validateRoute(RoutePart[] routeData, string[] sourceData)
41       {
42         if (!sourceData)
43         {
44           throw new RouteException("Passed no data to the route.");
45         }
46 
47         sourceData = sourceData.filter!(d => d && d.strip().length).array;
48 
49         if (sourceData.length != (routeData.length - 1))
50         {
51           throw new RouteException("Passed invalid amount of arguments to the route.");
52         }
53 
54         foreach (i; 0 .. sourceData.length)
55         {
56           auto rData = routeData[i + 1];
57           auto data = sourceData[i].strip();
58 
59           switch (rData.routeType)
60           {
61             case RouteType.identifier:
62             {
63               if (rData.identifier != data)
64               {
65                 throw new RouteException("Expected '" ~ rData.identifier ~ "' as identifier in the route.");
66               }
67 
68               break;
69             }
70 
71             case RouteType.type:
72             {
73               final switch (rData.type)
74               {
75                 foreach (memberIndex, member; EnumMembers!RouteDataType)
76                 {
77                   mixin(
78                     "case RouteDataType." ~
79                     to!string(cast(RouteDataType)member) ~
80                     ": mapValue!" ~
81                     member ~
82                     "(data); break;"
83                   );
84                 }
85               }
86 
87               break;
88             }
89 
90             case RouteType.typeIdentifier:
91             {
92               final switch (rData.type)
93               {
94                 foreach (memberIndex, member; EnumMembers!RouteDataType)
95                 {
96                   mixin(
97                     "case RouteDataType." ~
98                     to!string(cast(RouteDataType)member) ~
99                     ": mapValue!" ~
100                     member ~
101                     "(data, rData.identifier); break;"
102                   );
103                 }
104               }
105 
106               break;
107             }
108 
109             default: break;
110           }
111         }
112       }
113 
114       /**
115       * Maps a value that was passed to the route in a RESTful manner.
116       * Params:
117       *   data = The to be mapped.
118       *   mapName = The name to map it as. If no name is passed then conversion is just validated.
119       */
120       void mapValue(T)(string data, string mapName = null)
121       {
122         static if (is(typeof(T) == typeof(string)))
123         {
124           alias value = data;
125         }
126         else
127         {
128           T value = to!T(data);
129         }
130 
131         if (mapName)
132         {
133           _data[mapName] = value;
134         }
135       }
136     }
137 
138     protected:
139     /// Alias for the action entry.
140     alias ActionEntry = Action[string];
141     /// Alias for the method entry.
142     alias MethodEntry = ActionEntry[HttpMethod];
143 
144     /// Collection of actions.
145     public MethodEntry _actions;
146     /// The default action for the controller.
147     Action _defaultAction;
148     /// The mandatory action for the controller.
149     Action _mandatoryAction;
150 
151     /// Creates a new base controller.
152     this() { }
153 
154     /// Will handle the controller.
155     public abstract Status handle();
156 
157     final:
158     /**
159     * Maps an action to a http method by a name.
160     * Params:
161     *     method =    The http method.
162     *     action =    The action name.
163     *     fun =       The controller action associated with the mapping.
164     */
165     void mapAction(HttpMethod method, string action, Action fun)
166     {
167       _actions[method][action] = fun;
168     }
169 
170     /**
171     * Maps an action to a http method by a name.
172     * Params:
173     *     method =    The http method.
174     *     action =    The action name.
175     *     d =       The controller action associated with the mapping.
176     */
177     void mapAction(HttpMethod method, string action, Status delegate() d)
178     {
179       _actions[method][action] = new Action(d);
180     }
181 
182     /**
183     * Maps an action to a http method by a name.
184     * Params:
185     *     method =    The http method.
186     *     action =    The action name.
187     *     f =       The controller action associated with the mapping.
188     */
189     void mapAction(HttpMethod method, string action, Status function() f)
190     {
191       _actions[method][action] = new Action(f);
192     }
193 
194     /**
195     * Maps a default action for the controller.
196     * Params:
197     *     fun =       The controller action associated with the mapping.
198     */
199     void mapDefault(Action fun)
200     {
201       _defaultAction = fun;
202     }
203 
204     /**
205     * Maps a default action for the controller.
206     * Params:
207     *     d =       The controller action associated with the mapping.
208     */
209     void mapDefault(Status delegate() d)
210     {
211       _defaultAction = new Action(d);
212     }
213 
214     /**
215     * Maps a default action for the controller.
216     * Params:
217     *     f =       The controller action associated with the mapping.
218     */
219     void mapDefault(Status function() f)
220     {
221       _defaultAction = new Action(f);
222     }
223 
224     /**
225     * Maps a mandatory action for the controller.
226     * Params:
227     *     fun =       The controller action associated with the mapping.
228     */
229     void mapMandatory(Action fun)
230     {
231       _mandatoryAction = fun;
232     }
233 
234     /**
235     * Maps a mandatory action for the controller.
236     * Params:
237     *     d =       The controller action associated with the mapping.
238     */
239     void mapMandatory(Status delegate() d)
240     {
241       _mandatoryAction = new Action(d);
242     }
243 
244     /**
245     * Maps a mandatory action for the controller.
246     * Params:
247     *     f =       The controller action associated with the mapping.
248     */
249     void mapMandatory(Status function() f)
250     {
251       _mandatoryAction = new Action(f);
252     }
253 
254     /**
255     * Gets a value from the passed data.
256     * Params:
257     *   name =          The name of the value to get.
258     *   defaultValue =  The default value.
259     * Returns:
260     *   Returns the value if found, else the default value.
261     */
262     T get(T)(string name, T defaultValue = T.init)
263     {
264       Variant emptyVariant;
265       auto value = _data.get(name, emptyVariant);
266 
267       return value.hasValue ? value.get!T : defaultValue;
268     }
269 
270     static if (isTesting)
271     {
272       @property
273       {
274         import diamond.unittesting;
275 
276         /// Gets a boolean determnining whether the request is a test or not.
277         bool testing() { return !testsPassed; }
278       }
279     }
280   }
281 }