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