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 }