1 // Written in D programming language 2 /** 3 * Config reading system. 4 * 5 * Ctor $(B AppConfig(string)) create a config from file. 6 * 7 * Copyright: © 2014 DSoftOut 8 * License: Subject to the terms of the MIT license, as written in the included LICENSE file. 9 * Authors: Zaramzan <shamyan.roman@gmail.com> 10 */ 11 module server.config; 12 13 import std.exception; 14 import std.stdio; 15 import std.path; 16 import std.file; 17 import std.range; 18 import std.typecons; 19 import std.conv; 20 21 import vibe.data.json; 22 import vibe.core.log; 23 24 import util; 25 26 /** 27 * Represent configuration file. 28 * 29 * Authors: Zaramzan <shamyan.roman@gmail.com> 30 */ 31 struct AppConfig 32 { 33 @required 34 ushort port; 35 36 @required 37 uint maxConn; 38 39 @required 40 SqlConfig[] sqlServers; 41 42 @possible 43 string[] sqlAuth = null; 44 45 @required 46 uint sqlTimeout; //ms 47 48 @possible 49 uint aliveCheckTime = 3000; //ms 50 51 @required 52 string sqlJsonTable; 53 54 @possible 55 string[] bindAddresses = null; 56 57 @possible 58 string hostname = null; 59 60 @possible 61 int sqlReconnectTime = -1; //ms 62 63 @possible 64 string groupid; // to low root privileges 65 66 @possible 67 string userid; // to low root privileges 68 69 @required 70 string vibelog = "/var/log/"~APPNAME~"/"~"http.txt"; 71 72 @required 73 string logname = "/var/log/"~APPNAME~"/"~APPNAME~".txt"; 74 75 @possible 76 bool logSqlTransactions = false; 77 78 @possible 79 bool logJsonQueries = false; 80 81 /** 82 * Deserializing config from provided $(B json) object. 83 * 84 * Throws: InvalidConfig if json is incorrect (without info 85 * about config file name) 86 * 87 * Authors: Zaramzan <shamyan.roman@gmail.com> 88 * NCrashed <ncrashed@gmail.com> 89 */ 90 this(Json json) 91 { 92 try 93 { 94 this = deserializeFromJson!AppConfig(json); 95 } 96 catch(Exception e) 97 { 98 throw new InvalidConfig("", e.msg); 99 } 100 } 101 102 /** 103 * Parsing config from file $(B path). 104 * 105 * Throws: InvalidConfig 106 * 107 * Authors: Zaramzan <shamyan.roman@gmail.com> 108 * NCrashed <ncrashed@gmail.com> 109 */ 110 this(string path) immutable 111 { 112 auto str = File(path, "r").byLine.join.idup; 113 auto json = parseJson(str); 114 115 AppConfig conf; 116 try 117 { 118 conf = deserializeFromJson!AppConfig(json); 119 } 120 catch(Exception e) 121 { 122 throw new InvalidConfig(path, e.msg); 123 } 124 125 port = conf.port; 126 maxConn = conf.maxConn; 127 sqlServers = conf.sqlServers.idup; 128 sqlAuth = conf.sqlAuth.idup; 129 aliveCheckTime = conf.aliveCheckTime; 130 sqlTimeout = conf.sqlTimeout; 131 sqlJsonTable = conf.sqlJsonTable; 132 bindAddresses = conf.bindAddresses.idup; 133 hostname = conf.hostname; 134 sqlReconnectTime = conf.sqlReconnectTime; 135 vibelog = conf.vibelog; 136 logname = conf.logname; 137 groupid = conf.groupid; 138 userid = conf.userid; 139 logSqlTransactions = conf.logSqlTransactions; 140 logJsonQueries = conf.logJsonQueries; 141 } 142 } 143 144 /// Describes basic sql info in AppConfig 145 struct SqlConfig 146 { 147 @possible 148 string name = null; 149 150 @required 151 size_t maxConn; 152 153 @required 154 string connString; 155 } 156 157 /** 158 * The exception is thrown when configuration parsing error occurs 159 * (AppConfig constructor). Also encapsulates config file name. 160 * 161 * Throws: InvalidConfig 162 * 163 * Authors: Zaramzan <shamyan.roman@gmail.com> 164 * NCrashed <ncrashed@gmail.com> 165 */ 166 class InvalidConfig : Exception 167 { 168 private string mConfPath; 169 170 @safe pure nothrow this(string confPath, string msg, string file = __FILE__, size_t line = __LINE__) 171 { 172 mConfPath = confPath; 173 super(msg, file, line); 174 } 175 176 string confPath() @property 177 { 178 return mConfPath; 179 } 180 } 181 182 /** 183 * The exception is thrown by $(B tryConfigPaths) is called and 184 * all config file alternatives are failed to be loaded. 185 * 186 * Also encapsulates a set of tried paths. 187 * 188 * Authors: NCrashed <ncrashed@gmail.com> 189 */ 190 class NoConfigLoaded : Exception 191 { 192 private string[] mConfPaths; 193 194 @safe pure nothrow this(string[] confPaths, string file = __FILE__, size_t line = __LINE__) 195 { 196 mConfPaths = confPaths; 197 198 string msg; 199 try 200 { 201 msg = text("Failed to load configuration file from one of following paths: ", confPaths); 202 } 203 catch(Exception th) 204 { 205 msg = "<Internal error while collecting error message, report this bug!>"; 206 } 207 208 super(msg, file, line); 209 } 210 211 string[] confPaths() @property 212 { 213 return mConfPaths; 214 } 215 } 216 217 /** 218 * Return value from $(B tryConfigPaths). Handles loaded config 219 * and exact file $(B path). 220 * 221 * Authors: NCrashed <ncrashed@gmail.com> 222 */ 223 alias Tuple!(immutable AppConfig, "config", string, "path") LoadedConfig; 224 225 /** 226 * Takes range of paths and one at a time tries to load config from each one. 227 * If the path doesn't exist, then the next candidate is checked. If the file 228 * exists, but parsing or deserializing are failed, $(B InvalidConfig) exception 229 * is thrown. 230 * 231 * If functions go out of paths and none of them can be opened, then $(B NoConfigLoaded) 232 * exception is thrown. 233 * 234 * Throws: NoConfigLoaded if can't load configuration file. 235 * InvalidConfig if configuration file is invalid (first successfully opened) 236 * 237 * Authors: NCrashed <ncrashed@gmail.com> 238 * Zaramzan <shamyan.roman@gmail.com> 239 */ 240 LoadedConfig tryConfigPaths(R)(R paths) 241 if(isInputRange!R && is(ElementType!R == string)) 242 { 243 foreach(path; paths) 244 { 245 if(!path.exists) continue; 246 247 try 248 { 249 return LoadedConfig(immutable AppConfig(path), path); 250 } 251 catch(Exception e) 252 { 253 throw new InvalidConfig(path, e.msg); 254 } 255 } 256 257 throw new NoConfigLoaded(paths.array); 258 } 259 260 /** 261 * Returns config example to be edited by end user. 262 * 263 * This configuration is generated only if explicit 264 * key is passed to the application. 265 * 266 * Authors: NCrashed <ncrashed@gmail.com> 267 */ 268 AppConfig defaultConfig() 269 { 270 AppConfig ret; 271 ret.port = 8080; 272 ret.maxConn = 50; 273 ret.sqlServers = [SqlConfig("sql-server-1", 1, "dbname=rpc-proxy user=rpc-proxy password=123456")]; 274 ret.sqlAuth = ["login", "password"]; 275 ret.sqlTimeout = 1000; 276 ret.sqlReconnectTime = 5000; 277 ret.sqlJsonTable = "public.json_rpc"; 278 ret.bindAddresses = ["127.0.0.1"]; 279 ret.logname = "/var/log/"~APPNAME~"/"~APPNAME~".txt"; 280 return ret; 281 } 282 283 284 /** 285 * Writes configuration $(B appConfig) to file path $(B name). 286 * It is a wrapper function around $(B writeJson). 287 * 288 * Authors: NCrashed <ncrashed@gmail.com> 289 * Zaramzan <shamyan.roman@gmail.com> 290 */ 291 bool writeConfig(AppConfig appConfig, string name) 292 { 293 return writeJson(vibe.data.json.serializeToJson(appConfig), name); 294 } 295 296 /** 297 * Writes down $(B json) to provided file $(B name). 298 * 299 * Authors: NCrashed <ncrashed@gmail.com> 300 * Zaramzan <shamyan.roman@gmail.com> 301 */ 302 bool writeJson(Json json, string name) 303 { 304 scope(failure) return false; 305 306 auto dir = name.dirName; 307 if (!dir.exists) 308 { 309 dir.mkdirRecurse; 310 } 311 312 auto file = new File(name, "w"); 313 scope(exit) file.close(); 314 315 auto builder = appender!string; 316 writePrettyJsonString(builder, json, 0); 317 file.writeln(builder.data); 318 319 return true; 320 } 321 322 /** 323 * Generates and write down minimal configuration to $(B path). 324 * 325 * Authors: NCrashed <ncrashed@gmail.com> 326 * Zaramzan <shamyan.roman@gmail.com> 327 */ 328 void genConfig(string path) 329 { 330 if (!writeJson(defaultConfig.serializeRequiredToJson, path)) 331 { 332 std.stdio.writeln("Can't generate config at ", path); 333 } 334 } 335 336 version(unittest) 337 { 338 string configExample = " 339 { 340 \"bindAddresses\" : [ 341 \"::\", 342 \"0.0.0.0\", 343 ], 344 345 \"hostname\" : \"\", 346 347 \"port\" : 8888, 348 349 \"maxConn\" : 50, 350 351 \"sqlServers\" : [ 352 { 353 \"name\" : \"sql1\", 354 \"connString\" : \"\", 355 \"maxConn\" : 1 356 }, 357 { 358 \"name\" : \"sql2\", 359 \"connString\" : \"\", 360 \"maxConn\" : 2 361 } 362 ], 363 364 \"sqlTimeout\": 100, 365 366 \"sqlReconnectTime\": 150, 367 368 \"sqlAuth\" : [\"login\",\"password\"], 369 370 \"sqlJsonTable\" : \"json_rpc\", 371 372 \"logname\" : \"log.txt\", 373 374 \"vibelog\" : \"http.txt\", 375 376 \"logSqlTransactions\" : true 377 }"; 378 } 379 380 unittest 381 { 382 auto config1 = AppConfig(parseJsonString(configExample)); 383 384 AppConfig config2; 385 386 config2.port = cast(ushort) 8888; 387 config2.bindAddresses = ["::", "0.0.0.0"]; 388 config2.hostname = ""; 389 config2.maxConn = cast(uint) 50; 390 config2.sqlAuth = ["login", "password"]; 391 config2.sqlJsonTable = "json_rpc"; 392 config2.logname = "log.txt"; 393 config2.vibelog = "http.txt"; 394 config2.sqlReconnectTime = 150; 395 config2.sqlTimeout = 100; 396 config2.sqlServers = [SqlConfig("sql1", cast(size_t)1,""), SqlConfig("sql2", cast(size_t)2, "",)]; 397 config2.logSqlTransactions = true; 398 399 assert(config1 == config2, text("Config unittest failed ", config1, " != ", config2)); 400 }