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 }