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