1 /*
2  * Kiss - A refined core library for D programming language.
3  *
4  * Copyright (C) 2015-2018  Shanghai Putao Technology Co., Ltd
5  *
6  * Developer: HuntLabs.cn
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module kiss.util.configuration;
13 
14 import std.algorithm;
15 import std.array;
16 import std.conv;
17 import std.exception;
18 import std.file;
19 import std.format;
20 import std.path;
21 import std.stdio;
22 import std.string;
23 import std.traits;
24 
25 import kiss.logger;
26 
27 /**
28 */
29 struct Configuration
30 {
31     string name;
32 
33     this(string str)
34     {
35         name = str;
36     }
37 }
38 
39 /**
40 */
41 struct Value
42 {
43     this(bool opt)
44     {
45         optional = opt;
46     }
47 
48     this(string str, bool opt = false)
49     {
50         name = str;
51         optional = opt;
52     }
53 
54     string name;
55     bool optional = false;
56 }
57 
58 class BadFormatException : Exception
59 {
60     mixin basicExceptionCtors;
61 }
62 
63 class EmptyValueException : Exception
64 {
65     mixin basicExceptionCtors;
66 }
67 
68 /**
69 */
70 T as(T)(string value, T iv = T.init)
71 {
72     static if (is(T == bool))
73     {
74         if (value.length == 0 || value == "false" || value == "0")
75             return false;
76         else
77             return true;
78     }
79     else static if (std.traits.isNumeric!(T))
80     {
81         if (value.length == 0)
82             return iv;
83         else
84             return to!T(value);
85     }
86     else
87     {
88         if (value.length == 0)
89             return iv;
90         else
91             return cast(T) value;
92     }
93 }
94 
95 /**
96 */
97 class ConfigurationItem
98 {
99     ConfigurationItem parent;
100 
101     this(string name, string parentPath = "")
102     {
103         _name = name;
104     }
105 
106     @property ConfigurationItem subItem(string name)
107     {
108         auto v = _map.get(name, null);
109         if (v is null)
110         {
111             string path = this.fullPath();
112             if (path.empty)
113                 path = name;
114             else
115                 path = path ~ "." ~ name;
116             throw new EmptyValueException(format("The item of '%s' is not defined! ", path));
117         }
118         return v;
119     }
120 
121     @property bool exists(string name)
122     {
123         auto v = _map.get(name, null);
124         return (v !is null);
125     }
126 
127     string currentName()
128     {
129         return _name;
130     }
131 
132     string fullPath()
133     {
134         return _fullPath;
135     }
136 
137     @property string value()
138     {
139         return _value;
140     }
141 
142     ConfigurationItem opDispatch(string s)()
143     {
144         return subItem(s);
145     }
146 
147     ConfigurationItem opIndex(string s)
148     {
149         return subItem(s);
150     }
151 
152     T as(T = string)(T iv = T.init)
153     {
154         static if (is(T == bool))
155         {
156             if (_value.length == 0 || _value == "false" || _value == "0")
157                 return false;
158             else
159                 return true;
160         }
161         else static if (std.traits.isNumeric!(T))
162         {
163             if (_value.length == 0)
164                 return iv;
165             else
166                 return to!T(_value);
167         }
168         else
169         {
170             if (_value.length == 0)
171                 return iv;
172             else
173                 return cast(T) _value;
174         }
175     }
176 
177     void apppendChildNode(string key, ConfigurationItem subItem)
178     {
179         subItem.parent = this;
180         _map[key] = subItem;
181     }
182 
183     override string toString()
184     {
185         return _fullPath;
186     }
187 
188     // string buildFullPath()
189     // {
190     //     string r = name;
191     //     ConfigurationItem cur = parent;
192     //     while (cur !is null && !cur.name.empty)
193     //     {
194     //         r = cur.name ~ "." ~ r;
195     //         cur = cur.parent;
196     //     }
197     //     return r;
198     // }
199 
200 private:
201     string _value;
202     string _name;
203     string _fullPath;
204     ConfigurationItem[string] _map;
205 }
206 
207 // dfmt off
208 __gshared const string[] reservedWords = [
209     "abstract", "alias", "align", "asm", "assert", "auto", "body", "bool",
210     "break", "byte", "case", "cast", "catch", "cdouble", "cent", "cfloat", 
211     "char", "class","const", "continue", "creal", "dchar", "debug", "default", 
212     "delegate", "delete", "deprecated", "do", "double", "else", "enum", "export", 
213     "extern", "false", "final", "finally", "float", "for", "foreach", "foreach_reverse",
214     "function", "goto", "idouble", "if", "ifloat", "immutable", "import", "in", "inout", 
215     "int", "interface", "invariant", "ireal", "is", "lazy", "long",
216     "macro", "mixin", "module", "new", "nothrow", "null", "out", "override", "package",
217     "pragma", "private", "protected", "public", "pure", "real", "ref", "return", "scope", 
218     "shared", "short", "static", "struct", "super", "switch", "synchronized", "template", 
219     "this", "throw", "true", "try", "typedef", "typeid", "typeof", "ubyte", "ucent", 
220     "uint", "ulong", "union", "unittest", "ushort", "version", "void", "volatile", "wchar",
221     "while", "with", "__FILE__", "__FILE_FULL_PATH__", "__MODULE__", "__LINE__", 
222     "__FUNCTION__", "__PRETTY_FUNCTION__", "__gshared", "__traits", "__vector", "__parameters",
223     "subItem", "rootItem"
224 ];
225 // dfmt on
226 
227 /**
228 */
229 class ConfigBuilder
230 {
231     this(string filename, string section = "")
232     {
233         if (!exists(filename) || isDir(filename))
234             throw new Exception("The config file does not exist: " ~ filename);
235         _section = section;
236         loadConfig(filename);
237     }
238 
239     ConfigurationItem subItem(string name)
240     {
241         return _value.subItem(name);
242     }
243 
244     @property ConfigurationItem rootItem()
245     {
246         return _value;
247     }
248 
249     ConfigurationItem opDispatch(string s)()
250     {
251         return _value.opDispatch!(s)();
252     }
253 
254     ConfigurationItem opIndex(string s)
255     {
256         return _value.subItem(s);
257     }
258 
259     T build(T, string nodeName = "")()
260     {
261         static if (!nodeName.empty)
262         {
263             pragma(msg, "node name: " ~ nodeName);
264             return buildItem!(T)(this.subItem(nodeName));
265         }
266         else static if (hasUDA!(T, Configuration))
267         {
268             enum name = getUDAs!(T, Configuration)[0].name;
269             // pragma(msg,  "node name: " ~ name);
270             static if (name.length > 0)
271             {
272                 return buildItem!(T)(this.subItem(name));
273             }
274             else
275             {
276                 return buildItem!(T)(this.rootItem);
277             }
278         }
279         else
280         {
281             return buildItem!(T)(this.rootItem);
282         }
283     }
284 
285     static private T buildItem(T)(ConfigurationItem item)
286     {
287         T creatT(T)()
288         {
289             static if (is(T == struct))
290             {
291                 return T();
292             }
293             else static if (is(T == class))
294             {
295                 return new T();
296             }
297             else
298             {
299                 static assert(false, T.stringof ~ " is not supported!");
300             }
301         }
302 
303         auto r = creatT!T();
304         enum generatedCode = buildSetFunction!(T, r.stringof, item.stringof)();
305         // pragma(msg, generatedCode);
306         mixin(generatedCode);
307         return r;
308     }
309 
310     static private string buildSetFunction(T, string returnParameter, string incomingParameter)()
311     {
312         import std.format;
313 
314         string str = "import kiss.logger;";
315         foreach (memberName; __traits(allMembers, T)) // TODO: // foreach (memberName; __traits(derivedMembers, T))
316         {
317             enum memberProtection = __traits(getProtection, __traits(getMember, T, memberName));
318             static if (memberProtection == "private"
319                     || memberProtection == "protected" || memberProtection == "export")
320             {
321                 version (KissDebugMode) pragma(msg, "skip private member: " ~ memberName);
322             }
323             else static if (isType!(__traits(getMember, T, memberName)))
324             {
325                 version (KissDebugMode) pragma(msg, "skip inner type member: " ~ memberName);
326             }
327             else static if (__traits(isStaticFunction, __traits(getMember, T, memberName)))
328             {
329                 version (KissDebugMode) pragma(msg, "skip static member: " ~ memberName);
330             }
331             else
332             {
333                 alias memberType = typeof(__traits(getMember, T, memberName));
334                 enum memberTypeString = memberType.stringof;
335 
336                 static if (hasUDA!(__traits(getMember, T, memberName), Value))
337                 {
338                     enum item = getUDAs!((__traits(getMember, T, memberName)), Value)[0];
339                     enum settingItemName = item.name.empty ? memberName : item.name;
340                 }
341                 else
342                 {
343                     enum settingItemName = memberName;
344                 }
345 
346                 // 
347                 static if (is(memberType == interface))
348                 {
349                     pragma(msg, "interface (unsupported): " ~ memberName);
350                 }
351                 else static if (is(memberType == struct) || is(memberType == class))
352                 {
353                     str ~= setClassMemeber!(memberType, settingItemName,
354                             memberName, returnParameter, incomingParameter)();
355                 }
356                 else static if (isFunction!(memberType))
357                 {
358                     enum r = setFunctionMemeber!(memberType, settingItemName,
359                                 memberName, returnParameter, incomingParameter)();
360                     if (!r.empty)
361                         str ~= r;
362                 }
363                 else
364                 {
365                     version (KissDebugMode) pragma(msg,
366                             "setting " ~ memberName ~ " with item " ~ settingItemName);
367                     str ~= q{
368                         if(%5$s.exists("%1$s")) {
369                             %4$s.%2$s = %5$s.subItem("%1$s").as!(%3$s)();
370                         }
371                         else {
372                             version (KissDebugMode) warningf("Undefined item: %%s.%1$s" , %5$s.fullPath);
373                         }
374                         
375                         version (KissDebugMode) tracef("%4$s.%2$s=%%s", %4$s.%2$s);
376                     }.format(settingItemName, memberName,
377                             memberTypeString, returnParameter, incomingParameter);
378                 }
379             }
380         }
381         return str;
382     }
383 
384     private static string setFunctionMemeber(memberType, string settingItemName,
385             string memberName, string returnParameter, string incomingParameter)()
386     {
387         string r = "";
388         alias memeberParameters = Parameters!(memberType);
389         static if (memeberParameters.length == 1)
390         {
391             alias parameterType = memeberParameters[0];
392 
393             static if (is(parameterType == struct) || is(parameterType == class)
394                     || is(parameterType == interface))
395             {
396                 version (KissDebugMode) pragma(msg, "skip method with class: " ~ memberName);
397             }
398             else
399             {
400                 version (KissDebugMode) pragma(msg, "method: " ~ memberName);
401 
402                 r = q{
403                             if(%5$s.exists("%1$s")) {
404                                 %4$s.%2$s(%5$s.subItem("%1$s").as!(%3$s)());
405                             }
406                             else {
407                                 version (KissDebugMode) warningf("Undefined item: %%s.%1$s" , %5$s.fullPath);
408                             }
409                             
410                             version (KissDebugMode) tracef("%4$s.%2$s=%%s", %4$s.%2$s);
411                             }.format(settingItemName, memberName,
412                         parameterType.stringof, returnParameter, incomingParameter);
413             }
414         }
415         else
416         {
417             version (KissDebugMode) pragma(msg, "skip method: " ~ memberName);
418         }
419 
420         return r;
421     }
422 
423     private static setClassMemeber(memberType, string settingItemName, string memberName, string returnParameter, string incomingParameter)()
424     {
425         enum fullTypeName = fullyQualifiedName!(memberType);
426         enum memberModuleName = moduleName!(memberType);
427 
428         static if (settingItemName == memberName && hasUDA!(memberType, Configuration))
429         {
430             // try to get the ItemName from the UDA Configuration in a class or struct
431             enum newSettingItemName = getUDAs!(memberType, Configuration)[0].name;
432         }
433         else
434         {
435             enum newSettingItemName = settingItemName;
436         }
437 
438         version (KissDebugMode)
439         {
440             pragma(msg, "module name: " ~ memberModuleName);
441             pragma(msg, "full type name: " ~ fullTypeName);
442             pragma(msg, "setting " ~ memberName ~ " with item " ~ newSettingItemName);
443         }
444 
445         string r = q{
446             import %1$s;
447             
448             tracef("%5$s.%3$s is a class/struct.");
449             if(%6$s.exists("%2$s")) {
450                 %5$s.%3$s = buildItem!(%4$s)(%6$s.subItem("%2$s"));
451             }
452             else {
453                 version (KissDebugMode) warningf("Undefined item: %%s.%2$s" , %6$s.fullPath);
454             }
455         }.format(memberModuleName, newSettingItemName,
456                 memberName, fullTypeName, returnParameter, incomingParameter);
457         return r;
458     }
459 
460 private:
461     void loadConfig(string filename)
462     {
463         _value = new ConfigurationItem("");
464 
465         if (!exists(filename))
466             return;
467 
468         auto f = File(filename, "r");
469         if (!f.isOpen())
470             return;
471         scope (exit)
472             f.close();
473         string section = "";
474         int line = 1;
475         while (!f.eof())
476         {
477             scope (exit)
478                 line += 1;
479             string str = f.readln();
480             str = strip(str);
481             if (str.length == 0)
482                 continue;
483             if (str[0] == '#' || str[0] == ';')
484                 continue;
485             auto len = str.length - 1;
486             if (str[0] == '[' && str[len] == ']')
487             {
488                 section = str[1 .. len].strip;
489                 continue;
490             }
491             if (section != _section && section != "")
492                 continue;
493 
494             str = stripInlineComment(str);
495             auto site = str.indexOf("=");
496             enforce!BadFormatException((site > 0),
497                     format("Bad format in file %s, at line %d", filename, line));
498             string key = str[0 .. site].strip;
499             setValue(key, str[site + 1 .. $].strip);
500         }
501     }
502 
503     string stripInlineComment(string line)
504     {
505         ptrdiff_t index = indexOf(line, "# ");
506 
507         if (index == -1)
508             return line;
509         else
510             return line[0 .. index];
511     }
512 
513     void setValue(string key, string value)
514     {
515         string currentPath;
516         string[] list = split(key, '.');
517         auto cvalue = _value;
518         foreach (str; list)
519         {
520             if (str.length == 0)
521                 continue;
522 
523             if (canFind(reservedWords, str))
524             {
525                 warningf("Found a reserved word: %s. It may cause some errors to use it.", str);
526             }
527 
528             if (currentPath.empty)
529                 currentPath = str;
530             else
531                 currentPath = currentPath ~ "." ~ str;
532 
533             // version (KissDebugMode)
534             //     tracef("checking node: path=%s", currentPath);
535             auto tvalue = cvalue._map.get(str, null);
536             if (tvalue is null)
537             {
538                 tvalue = new ConfigurationItem(str);
539                 tvalue._fullPath = currentPath;
540                 cvalue.apppendChildNode(str, tvalue);
541                 // version (KissDebugMode)
542                 //     tracef("new node: parent=%s, node=%s, value=%s", cvalue.fullPath, str, value);
543             }
544             cvalue = tvalue;
545         }
546 
547         if (cvalue !is _value)
548             cvalue._value = value;
549     }
550 
551     string _section;
552     ConfigurationItem _value;
553 }
554 
555 version (unittest)
556 {
557     import kiss.util.configuration;
558 
559     @Configuration("app")
560     class TestConfig
561     {
562         string test;
563         double time;
564 
565         TestHttpConfig http;
566 
567         @Value("optial", true)
568         int optial = 500;
569 
570         @Value(true)
571         int optial2 = 500;
572 
573         // mixin ReadConfig!TestConfig;
574     }
575 
576     @Configuration("http")
577     struct TestHttpConfig
578     {
579         @Value("listen")
580         int value;
581         string addr;
582 
583         // mixin ReadConfig!TestHttpConfig;
584     }
585 }
586 
587 unittest
588 {
589     import std.stdio;
590     import FE = std.file;
591 
592     FE.write("test.config", `app.http.listen = 100
593     http.listen = 100
594     app.test = 
595     app.time = 0.25 
596     # this is  
597      ; start dev
598     [dev]
599     app.test = dev`);
600 
601     auto conf = new ConfigBuilder("test.config");
602     assert(conf.http.listen.value.as!long() == 100);
603     assert(conf.app.test.value() == "");
604 
605     auto confdev = new ConfigBuilder("test.config", "dev");
606     long tv = confdev.http.listen.value.as!long;
607     assert(tv == 100);
608     assert(confdev.http.listen.value.as!long() == 100);
609     writeln("----------", confdev.app.test.value());
610     string tvstr = cast(string) confdev.app.test.value;
611 
612     assert(tvstr == "dev");
613     assert(confdev.app.test.value() == "dev");
614     bool tvBool = confdev.app.test.value.as!bool;
615     assert(tvBool);
616 
617     assertThrown!(EmptyValueException)(confdev.app.host.value());
618 
619     TestConfig test = confdev.build!(TestConfig)();
620     assert(test.test == "dev");
621     assert(test.time == 0.25);
622     assert(test.http.value == 100);
623     assert(test.optial == 500);
624     assert(test.optial2 == 500);
625 }