1 module scorpion.bootstrap;
2 
3 import std.conv : to;
4 import std.exception : enforce;
5 import std.experimental.logger : sharedLog, LogLevel, info;
6 import std.regex : Regex;
7 import std..string : split, join;
8 import std.traits : hasUDA, getUDAs, isFunction, Parameters, ParameterIdentifierTuple, ReturnType;
9 import std.typecons : Tuple;
10 
11 import asdf : serializeToJson;
12 
13 import lighttp.resource : Resource;
14 import lighttp.router : Router, routeInfo;
15 import lighttp.server : ServerOptions, Server;
16 import lighttp.util : StatusCodes, ServerRequest, ServerResponse;
17 
18 import scorpion.component : Component, Init, Value;
19 import scorpion.config : Config, Configuration, LanguageConfiguration, ProfilesConfiguration;
20 import scorpion.context : Context;
21 import scorpion.controller : Controller, Route, Callable, Path, Param, Body;
22 import scorpion.entity : Entity, ExtendEntity;
23 import scorpion.lang : LanguageManager;
24 import scorpion.profile : Profile;
25 import scorpion.repository : Repository, DatabaseRepository;
26 import scorpion.session : Session;
27 import scorpion.validation : Validation, validateParam, validateBody;
28 import scorpion.view : View;
29 
30 import shark : Database, MysqlDatabase, PostgresqlDatabase;
31 
32 /**
33  * Stores instructions on how to build controllers, components
34  * and entities.
35  * This object is only used internally by scorpion for
36  * initialization and bootstrapping of the server.
37  */
38 final class ScorpionServer {
39 
40 	/**
41 	 * Instance of the language manager. During the bootstrapping
42 	 * of the server language files are given to the language manager
43 	 * which converts them in key-value pairs.
44 	 * The language manager is the only object in the register class
45 	 * that is used after the server initialization.
46 	 */
47 	private LanguageManager languageManager;
48 
49 	/**
50 	 * Contains instances of the `ProfilesConfiguration` configuration.
51 	 */
52 	private ProfilesConfiguration[] profilesConfigurations;
53 
54 	/**
55 	 * Contains instructions on how to build entities.
56 	 */
57 	private EntityInfo[] entities;
58 
59 	/**
60 	 * Contains informations about components and instructions on how
61 	 * to build a new one.
62 	 */
63 	private ComponentInfo[] components;
64 
65 	/**
66 	 * Contains informations about controllers and a function to initialize
67 	 * routes.
68 	 */
69 	private ControllerInfo[] controllers;
70 	
71 	/**
72 	 * Scans a module for controllers, components and entities and
73 	 * adds the instructions for initialization in the register.
74 	 */
75 	public void registerModule(alias module_)() {
76 		foreach(immutable member ; __traits(allMembers, module_)) {
77 			static if(__traits(getProtection, __traits(getMember, module_, member)) == "public") {
78 				alias T = __traits(getMember, module_, member);
79 				static if(hasUDA!(T, Configuration)) {
80 					T configuration = new T();
81 					static if(is(T : LanguageConfiguration)) {
82 						foreach(lang, data; configuration.loadLanguages()) {
83 							languageManager.add(lang, data);
84 						}
85 					}
86 					static if(is(T : ProfilesConfiguration)) {
87 						profilesConfigurations ~= configuration;
88 					}
89 				}
90 				static if(hasUDA!(T, Entity)) {
91 					entities ~= new EntityInfoImpl!(ExtendEntity!(T, getUDAs!(T, Entity)[0].name))(Profile.get(getUDAs!(T, Profile)));
92 				}
93 				static if(hasUDA!(T, Component)) {
94 					//static if(is(T : Repository!R, R)) services ~= new ServiceInfoImpl!(DatabaseRepository!T)();
95 					static if(is(T : Repository!R, R)) components ~= new ComponentInfoImpl!(DatabaseRepository!T, true)(Profile.get(getUDAs!(T, Profile)));
96 					else components ~= new ComponentInfoImpl!(T, false)(Profile.get(getUDAs!(T, Profile)));
97 				}
98 				static if(hasUDA!(T, Controller)) {
99 					controllers ~= new ControllerInfoImpl!(T)(Profile.get(getUDAs!(T, Profile)));
100 				}
101 			}
102 		}
103 	}
104 
105 	/**
106 	 * Starts the server by reading the configuration files and
107 	 * runs it. This function starts the event loop snd rever returns.
108 	 */
109 	public void run(string[] args) {
110 
111 		Config config = Config.load();
112 
113 		//TODO override configurations using args
114 		
115 		// sets the log level reading it from `scorpion.log` in the configuration files
116 		sharedLog.logLevel = {
117 			switch(config.get("scorpion.log", "info")) {
118 				case "all": return LogLevel.all;
119 				case "trace": return LogLevel.trace;
120 				case "info": return LogLevel.info;
121 				case "warning": return LogLevel.warning;
122 				case "error": return LogLevel.error;
123 				case "critical": return LogLevel.critical;
124 				case "fatal": return LogLevel.fatal;
125 				case "off": return LogLevel.off;
126 				default: throw new Exception("Invalid value for scorpion.log");
127 			}
128 		}();
129 		
130 		immutable ip = config.get!string("scorpion.ip", "0.0.0.0");
131 		immutable port = config.get!ushort("scorpion.port", 80);
132 		
133 		info("Starting server on ", ip, ":", port);
134 		
135 		// initialize the database using values from `scorpion.database.*`
136 		Database database;
137 		immutable type = config.get("scorpion.database.driver", string.init);
138 		if(type !is null) {
139 			Database getDatabase() {
140 				switch(type) {
141 					case "mysql": return new MysqlDatabase(config.get("scorpion.database.host", "localhost"), config.get("scorpion.database.port", ushort(3306)));
142 					case "postgresql": return new PostgresqlDatabase(config.get("scorpion.database.host", "localhost"), config.get("scorpion.database.port", ushort(5432)));
143 					default: throw new Exception("Cannot create a database of type '" ~ type ~ "'");
144 				}
145 			}
146 			database = getDatabase();
147 			database.connect(config.get("scorpion.database.name", string.init), config.get("scorpion.database.user", "root"), config.get("scorpion.database.password", ""));
148 		}
149 		
150 		// creates the default options for the server
151 		ServerOptions options;
152 		options.name = "Scorpion/0.1";
153 		options.max = config.get("scorpion.upload.max", 2 ^^ 24); // 16 MB
154 		
155 		// creates the server, initializes it and starts the event loop
156 		Server server = new Server(options);
157 		init(server.router, config, database);
158 		server.host(ip, port);
159 		server.run();
160 
161 	}
162 
163 	/**
164 	 * Initializes entities and controllers.
165 	 */
166 	private void init(Router router, Config config, Database database) {
167 		Context context = new Context(config);
168 		foreach(profilesConfiguration ; profilesConfigurations) {
169 			config.addProfiles(profilesConfiguration.defaultProfiles());
170 		}
171 		info("Active profiles: ", config.profiles.join(", "));
172 		void filter(T)(ref T[] array) {
173 			T[] ret;
174 			foreach(element ; array) {
175 				auto info = cast(Info)element;
176 				if(info.profiles.length == 0 || config.hasProfile(info.profiles)) ret ~= element;
177 			}
178 			array = ret;
179 		}
180 		filter(entities);
181 		filter(components);
182 		filter(controllers);
183 		foreach(entityInfo ; entities) {
184 			entityInfo.init(context, database);
185 		}
186 		foreach(controllerInfo ; controllers) {
187 			controllerInfo.init(router, context, database);
188 		}
189 	}
190 
191 	/**
192 	 * Tries to initialize a component and throws and exception
193 	 * on failure.
194 	 */
195 	private void initComponent(T)(ref T value, Database database) {
196 		foreach(component ; components) {
197 			T instance = cast(T)component.instance(database);
198 			if(instance !is null) {
199 				value = component.useCached ? instance : cast(T)component.newInstance(database);
200 				return;
201 			}
202 		}
203 		throw new Exception("Failed to initialize component of type " ~ T.stringof);
204 	}
205 
206 	private class Info {
207 		
208 		string[] profiles;
209 
210 		this(string[] profiles) {
211 			this.profiles = profiles;
212 		}
213 
214 	}
215 
216 	private interface EntityInfo {
217 
218 		void init(Context context, Database database);
219 
220 	}
221 
222 	private class EntityInfoImpl(T) : Info, EntityInfo {
223 
224 		this(string[] profiles) {
225 			super(profiles);
226 		}
227 
228 		override void init(Context context, Database database) {
229 			enforce!Exception(database !is null, "A database connection is required");
230 			database.init!T();
231 		}
232 
233 	}
234 
235 	private interface ComponentInfo {
236 
237 		@property bool useCached();
238 
239 		Object instance(Database);
240 
241 		Object newInstance(Database);
242 
243 	}
244 
245 	private class ComponentInfoImpl(T, bool repository) : Info, ComponentInfo {
246 
247 		private T cached;
248 		
249 		this(string[] profiles) {
250 			super(profiles);
251 			static if(!repository) cached = new T();
252 		}
253 
254 		override bool useCached() {
255 			return repository;
256 		}
257 
258 		override Object instance(Database database) {
259 			static if(repository) if(cached is null) cached = new T(database);
260 			return cached;
261 		}
262 
263 		override Object newInstance(Database database) {
264 			static if(repository) T ret = new T(database);
265 			else T ret = new T();
266 			initComponent(ret, database);
267 			return ret;
268 		}
269 
270 	}
271 
272 	private interface ControllerInfo {
273 
274 		void init(Router router, Context context, Database);
275 
276 	}
277 
278 	private class ControllerInfoImpl(T) : Info, ControllerInfo {
279 
280 		this(string[] profiles) {
281 			super(profiles);
282 		}
283 
284 		override void init(Router router, Context context, Database database) {
285 			T controller = new T();
286 			static if(!__traits(compiles, getUDAs!(T, Controller)[0]())) enum controllerPath = getUDAs!(T, Controller)[0].path;
287 			foreach(i, immutable member; __traits(allMembers, T)) {
288 				static if(__traits(getProtection, __traits(getMember, T, member)) == "public") {
289 					immutable full = "controller." ~ member;
290 					alias F = __traits(getMember, T, member);
291 					enum tests = {
292 						string[] ret;
293 						foreach(j, immutable uda; __traits(getAttributes, F)) {
294 							static if(is(typeof(__traits(getMember, uda, "test")) == function)) {
295 								static if(__traits(compiles, uda())) ret ~= "__traits(getAttributes, F)[" ~ j.to!string ~ "].init";
296 								else ret ~= "__traits(getAttributes, F)[" ~ j.to!string ~ "]";
297 							}
298 						}
299 						return ret;
300 					}();
301 					// weird bug on DMD 2.084: without the static foreach the compiler
302 					// says that variable `uda` cannot be read at compile time.
303 					static foreach(immutable uda ; __traits(getAttributes, F)) {
304 						static if(is(typeof(uda) == Route) || is(typeof(uda()) == Route)) {
305 							static if(is(typeof(controllerPath))) enum path = controllerPath ~ uda.path;
306 							else enum path = uda.path;
307 							enum regexPath = path.join(`\/`);
308 							static if(isFunction!F) {
309 								auto fun = mixin(generateFunction!F(T.stringof, member, regexPath, tests));
310 							} else {
311 								static assert(is(typeof(F) : Resource), "Members annotated with @Route must be callable or an instance of Resource");
312 								auto fun = delegate(ServerRequest request, ServerResponse response){
313 									context.refresh(request, response);
314 									static foreach(test ; tests) {
315 										if(!mixin(test).test(context)) return;
316 									}
317 									response.headers["X-Scorpion-Controller"] = T.stringof ~ "." ~ member;
318 									response.headers["X-Scorpion-Path"] = regexPath;
319 									mixin(full).apply(request, response);
320 								};
321 							}
322 							router.add(routeInfo(uda.method, uda.hasBody, regexPath), fun);
323 							info("Routing ", uda.method, " /", path.join("/"), " to ", T.stringof, ".", member, (isFunction!F ? "()" : ""));
324 						} else static if(is(typeof(uda) == Callable) || is(typeof(uda()) == Callable)) {
325 							static if(is(typeof(uda) == Callable)) enum path = ["internal", "function", uda.functionName];
326 							else enum path = ["internal", "function", member];
327 							mixin(generateStruct!(F, i));
328 							auto fun = delegate(ServerRequest request, ServerResponse response){
329 								context.refresh(request, response);
330 								static foreach(test ; tests) {
331 									if(!mixin(test).test(context)) return;
332 								}
333 								static if(Parameters!F.length) {
334 									Validation validation = new Validation();
335 									X members = validateBody!X(request, response, validation);
336 									if(validation.valid) {
337 										Parameters!F args;
338 										foreach(j, immutable member; __traits(allMembers, X)) {
339 											mixin("args[" ~ j.to!string ~ "]") = mixin("members." ~ member);
340 										}
341 										response.contentType = "application/json";
342 										static if(is(ReturnType!F == void)) mixin(full)(args);
343 										else response.body_ = serializeToJson(mixin(full)(args));
344 									}
345 								} else {
346 									response.contentType = "application/json";
347 									static if(is(ReturnType!F == void)) mixin(full)();
348 									else response.body_ = serializeToJson(mixin(full)());
349 								}
350 							};
351 							router.add(routeInfo("CALL", true, path.join(`\/`)), fun);
352 						}
353 					}
354 					static if(hasUDA!(F, Init)) {
355 						initComponent(mixin(full), database);
356 					}
357 					static if(hasUDA!(F, Value)) {
358 						mixin(full) = context.config.get(getUDAs!(F, Value)[0].key, mixin(full));
359 					}
360 				}
361 			}
362 		}
363 
364 	}
365 
366 }
367 
368 private string generateStruct(alias F, size_t index)() {
369 	string ret;
370 	static foreach(i ; 0..Parameters!F.length) {
371 		ret ~= "Parameters!F[" ~ i.to!string ~ "] ";
372 		ret ~= ParameterIdentifierTuple!F[i] ~ ';';
373 	}
374 	return "struct X" ~ index.to!string ~ "{" ~ ret ~ "}alias X=X" ~ index.to!string ~ ";";
375 
376 }
377 
378 private string generateFunction(alias M)(string controller, string member, string path, string[] tests) {
379 	string[] ret = ["ServerRequest request", "ServerResponse response"];
380 	string body1 = "context.refresh(request,response);response.headers[`X-Scorpion-Controller`]=`" ~ controller ~ "." ~ member ~ "`;response.headers[`X-Scorpion-Path`]=`" ~ path ~ "`;";
381 	string body2, body3;
382 	string[Parameters!M.length] call;
383 	bool validation = false;
384 	foreach(test ; tests) {
385 		body1 ~= "if(!" ~ test ~ ".test(context)){return;}";
386 	}
387 	body1 ~= "response.status=StatusCodes.ok;Validation validation=new Validation();";
388 	foreach(i, param; Parameters!M) {
389 		static if(is(param == ServerRequest)) call[i] = "request";
390 		else static if(is(param == ServerResponse)) call[i] = "response";
391 		else static if(is(param == View)) {
392 			body2 ~= "View view=View(request,response,languageManager);";
393 			call[i] = "view";
394 		} else static if(is(param == Session)) {
395 			call[i] = "context.session";
396 		} else static if(is(param == Validation)) {
397 			call[i] = "validation";
398 			validation = true;
399 		} else static if(is(typeof(M) Params == __parameters)) {
400 			immutable p = "Parameters!F[" ~ i.to!string ~ "] " ~ member ~ i.to!string;
401 			call[i] = member ~ i.to!string;
402 			foreach(attr ; __traits(getAttributes, Params[i..i+1])) {
403 				static if(is(attr == Path)) {
404 					ret ~= p;
405 				} else static if(is(attr == Param) || is(typeof(attr) == Param)) {
406 					static if(is(attr == Param)) enum name = ParameterIdentifierTuple!M[i];
407 					else enum name = attr.param;
408 					body1 ~= p ~ "=validateParam!(Parameters!F[" ~ i.to!string ~ "])(\"" ~ name ~ "\",request,response);";
409 					body1 ~= "if(response.status.code==400){return;}";
410 				} else static if(is(attr == Body)) {
411 					body1 ~= p ~ "=validateBody!(Parameters!F[" ~ i.to!string ~ "])(request,response,validation);";
412 				}
413 			}
414 		}
415 	}
416 	if(validation) body2 ~= "validation.apply(response);if(response.status.code==400){return;}";
417 	else body3 = "validation.apply(response);";
418 	return "delegate(" ~ ret.join(",") ~ "){" ~ body1 ~ body2 ~ "controller." ~ member ~ "(" ~ join(cast(string[])call, ",") ~ ");" ~ body3 ~ "}";
419 }
420 
421 unittest {
422 
423 	static import scorpion.welcome;
424 
425 	ScorpionServer server = new ScorpionServer();
426 	server.registerModule!(scorpion.welcome);
427 
428 }