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