1 /*
2  * Geario - A cross-platform abstraction library with asynchronous I/O.
3  *
4  * Copyright (C) 2021-2022 Kerisy.com
5  *
6  * Website: https://www.kerisy.com
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module geario.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 geario.logging;
27 import geario.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         log.info("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(GEAR_CONFIG_DEBUG) log.trace("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             log.warn("The items for '%s' is undefined! Use the defaults now", 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             log.warn("The items for '%s' is undefined! Use the defaults now", path);
143         }
144         return r;
145     }
146 
147     bool Exists(string name) {
148         auto v = _map.get(name, null);
149         bool r = v !is null;
150         if(!r) {
151             // try to check array items
152             foreach(string key; _map.byKey) {
153                 Captures!string p = matchFirst(key, ArrayItemParttern);
154                 if(!p.empty && p[1] == name) {
155                     return true;
156                 }
157             }
158         }
159         return r;
160     }
161 
162     @property string Name() {
163         return _name;
164     }
165 
166     @property string FullPath() {
167         return _fullPath;
168     }
169 
170     @property string Value() {
171         return _value;
172     }
173 
174     ConfigurationItem OpDispatch(string s)() {
175         return SubItem(s);
176     }
177 
178     ConfigurationItem OpIndex(string s) {
179         return SubItem(s);
180     }
181 
182     T as(T = string)(T v = T.init) {
183         return _value.as!(T)(v);
184     }
185 
186     void ApppendChildNode(string key, ConfigurationItem subItem) {
187         subItem.parent = this;
188         _map[key] = subItem;
189     }
190 
191     override string toString() {
192         return _fullPath;
193     }
194 
195     // string buildFullPath()
196     // {
197     //     string r = name;
198     //     ConfigurationItem cur = parent;
199     //     while (cur !is null && !cur.name.empty)
200     //     {
201     //         r = cur.name ~ "." ~ r;
202     //         cur = cur.parent;
203     //     }
204     //     return r;
205     // }
206 
207 private:
208     string _value;
209     string _name;
210     string _fullPath;
211     ConfigurationItem[string] _map;
212 }
213 
214 // dfmt off
215 __gshared const string[] reservedWords = [
216     "abstract", "alias", "align", "asm", "assert", "auto", "body", "bool",
217     "break", "byte", "case", "cast", "catch", "cdouble", "cent", "cfloat", 
218     "char", "class","const", "continue", "creal", "dchar", "debug", "default", 
219     "delegate", "delete", "deprecated", "do", "double", "else", "enum", "export", 
220     "extern", "false", "final", "finally", "float", "for", "foreach", "foreach_reverse",
221     "function", "goto", "idouble", "if", "ifloat", "immutable", "import", "in", "inout", 
222     "int", "interface", "invariant", "ireal", "is", "lazy", "long",
223     "macro", "mixin", "module", "new", "nothrow", "null", "out", "override", "package",
224     "pragma", "private", "protected", "public", "pure", "real", "ref", "return", "scope", 
225     "shared", "short", "static", "struct", "super", "switch", "synchronized", "template", 
226     "this", "throw", "true", "try", "typedef", "typeid", "typeof", "ubyte", "ucent", 
227     "uint", "ulong", "union", "unittest", "ushort", "version", "void", "volatile", "wchar",
228     "while", "with", "__FILE__", "__FILE_FULL_PATH__", "__MODULE__", "__LINE__", 
229     "__FUNCTION__", "__PRETTY_FUNCTION__", "__gshared", "__traits", "__vector", "__parameters",
230     "subItem", "RootItem"
231 ];
232 // dfmt on
233 
234 /**
235 */
236 class ConfigBuilder {
237 
238     this() {
239         _value = new ConfigurationItem("");
240     }
241 
242 
243     this(string filename, string section = "") {
244         _section = section;
245         _value = new ConfigurationItem("");
246         
247         string rootPath = dirName(thisExePath());
248         filename = buildPath(rootPath, filename);
249         LoadConfig(filename);
250     }
251 
252 
253     ConfigurationItem SubItem(string name) {
254         return _value.SubItem(name);
255     }
256 
257     @property ConfigurationItem RootItem() {
258         return _value;
259     }
260 
261     ConfigurationItem OpDispatch(string s)() {
262         return _value.OpDispatch!(s)();
263     }
264 
265     ConfigurationItem OpIndex(string s) {
266         return _value.SubItem(s);
267     }
268 
269     /**
270      * Searches for the property with the specified key in this property list.
271      * If the key is not found in this property list, the default property list,
272      * and its defaults, recursively, are then checked. The method returns
273      * {@code null} if the property is not found.
274      *
275      * @param   key   the property key.
276      * @return  the value in this property list with the specified key value.
277      */
278     string GetProperty(string key) {
279         return _itemMap.get(key, "");
280     }
281 
282     /**
283      * Searches for the property with the specified key in this property list.
284      * If the key is not found in this property list, the default property list,
285      * and its defaults, recursively, are then checked. The method returns
286      * {@code null} if the property is not found.
287      *
288      * @param   key   the property key.
289      * @return  the value in this property list with the specified key value.
290      * @see     #setProperty
291      * @see     #defaults
292      */
293     string GetProperty(string key, string defaultValue) {
294         return _itemMap.get(key, defaultValue);
295     }
296 
297     bool HasProperty(string key) {
298         auto p = key in _itemMap;
299         return p !is null;
300     }
301 
302     bool IsEmpty() {
303         return _itemMap.length == 0;
304     }
305 
306     alias setProperty = SetValue;
307 
308     void SetValue(string key, string value) {
309 
310         version (GEAR_CONFIG_DEBUG)
311             log.trace("setting item: key=%s, value=%s", key, value);
312         _itemMap[key] = value;
313 
314         string currentPath;
315         string[] list = split(key, '.');
316         ConfigurationItem cvalue = _value;
317         foreach (str; list) {
318             if (str.length == 0)
319                 continue;
320 
321             if (canFind(reservedWords, str)) {
322                 version (GEAR_CONFIG_DEBUG) log.warn("Found a reserved word: %s. It may cause some errors.", str);
323             }
324 
325             if (currentPath.empty)
326                 currentPath = str;
327             else
328                 currentPath = currentPath ~ "." ~ str;
329 
330             // version (GEAR_CONFIG_DEBUG)
331             //     log.trace("checking node: path=%s", currentPath);
332             ConfigurationItem tvalue = cvalue._map.get(str, null);
333             if (tvalue is null) {
334                 tvalue = new ConfigurationItem(str);
335                 tvalue._fullPath = currentPath;
336                 cvalue.ApppendChildNode(str, tvalue);
337                 version (GEAR_CONFIG_DEBUG)
338                     log.trace("new node: key=%s, parent=%s, node=%s", key, cvalue.FullPath, str);
339             }
340             cvalue = tvalue;
341         }
342 
343         if (cvalue !is _value)
344             cvalue._value = value;
345     }
346 
347     T Build(T, string nodeName = "")() {
348         static if (!nodeName.empty) {
349             // version(GEAR_CONFIG_DEBUG) pragma(msg, "node name: " ~ nodeName);
350             return BuildItem!(T)(this.SubItem(nodeName));
351         } else static if (hasUDA!(T, Configuration)) {
352             enum string name = getUDAs!(T, Configuration)[0].name;
353             // pragma(msg,  "node name: " ~ name);
354             // log.warn("node name: ", name);
355             static if (!name.empty) {
356                 return BuildItem!(T)(this.SubItem(name));
357             } else {
358                 return BuildItem!(T)(this.RootItem);
359             }
360         } else {
361             return BuildItem!(T)(this.RootItem);
362         }
363     }
364 
365     private static T CreatT(T)() {
366         static if (is(T == struct)) {
367             return T();
368         } else static if (is(T == class)) {
369             return new T();
370         } else {
371             static assert(false, T.stringof ~ " is not supported!");
372         }
373     }
374 
375     private static T BuildItem(T)(ConfigurationItem item) {
376         auto r = CreatT!T();
377         enum generatedCode = BuildSetFunction!(T, r.stringof, item.stringof)();
378         // pragma(msg, generatedCode);
379         mixin(generatedCode);
380         return r;
381     }
382 
383     private static string BuildSetFunction(T, string returnParameter, string incomingParameter)() {
384         import std.format;
385 
386         string str = "import geario.logging;";
387         foreach (memberName; __traits(allMembers, T)) // TODO: // foreach (memberName; __traits(derivedMembers, T))
388         {
389             enum memberProtection = __traits(getProtection, __traits(getMember, T, memberName));
390             static if (memberProtection == "private"
391                     || memberProtection == "protected" || memberProtection == "export") {
392                 // version (GEAR_CONFIG_DEBUG) pragma(msg, "skip private member: " ~ memberName);
393             } else static if (isType!(__traits(getMember, T, memberName))) {
394                 // version (GEAR_CONFIG_DEBUG) pragma(msg, "skip inner type member: " ~ memberName);
395             } else static if (__traits(isStaticFunction, __traits(getMember, T, memberName))) {
396                 // version (GEAR_CONFIG_DEBUG) pragma(msg, "skip static member: " ~ memberName);
397             } else {
398                 alias memberType = typeof(__traits(getMember, T, memberName));
399                 enum memberTypeString = memberType.stringof;
400 
401                 static if (hasUDA!(__traits(getMember, T, memberName), Value)) {
402                     enum itemName = getUDAs!((__traits(getMember, T, memberName)), Value)[0].name;
403                     enum settingItemName = itemName.empty ? memberName : itemName;
404                 } else {
405                     enum settingItemName = memberName;
406                 }
407 
408                 static if (!is(memberType == string) && is(memberType T : T[])) {
409                     static if(is(T == struct) || is(T == struct)) {
410                         enum isArrayMember = true;
411                     } else {
412                         enum isArrayMember = false;
413                     }
414                 } else {
415                     enum isArrayMember = false;
416                 }
417 
418                 // 
419                 static if (is(memberType == interface)) {
420                     pragma(msg, "interface (unsupported): " ~ memberName);
421                 } else static if (is(memberType == struct) || is(memberType == class)) {
422                     str ~= SetClassMemeber!(memberType, settingItemName,
423                             memberName, returnParameter, incomingParameter)();
424                 } else static if (isFunction!(memberType)) {
425                     enum r = SetFunctionMemeber!(memberType, settingItemName,
426                                 memberName, returnParameter, incomingParameter)();
427                     if (!r.empty)
428                         str ~= r;
429                 } else static if(isArrayMember) { // struct or class
430                     enum memberModuleName = moduleName!(T);
431                     str ~= "import " ~ memberModuleName ~ ";";
432                     str ~= q{
433                         if(%5$s.Exists("%1$s")) {
434                             ConfigurationItem[] items = %5$s.SubItems("%1$s");
435                             %3$s tempValues;
436                             foreach(ConfigurationItem it; items) {
437                                 // version (GEAR_CONFIG_DEBUG) log.trace("name:%%s, value:%%s", it.name, item.value);
438                                 tempValues ~= BuildItem!(%6$s)(it); // it.as!(%6$s)();
439                             }
440                             %4$s.%2$s = tempValues;
441                         } else {
442                             version (GEAR_CONFIG_DEBUG) log.warn("Undefined item: %%s.%1$s" , %5$s.FullPath);
443                         }                        
444                         version (GEAR_CONFIG_DEBUG) log.trace("%4$s.%2$s=%%s", %4$s.%2$s);
445 
446                     }.format(settingItemName, memberName,
447                             memberTypeString, returnParameter, incomingParameter, T.stringof);
448                 } else {
449                     // version (GEAR_CONFIG_DEBUG) pragma(msg,
450                     //         "setting " ~ memberName ~ " with item " ~ settingItemName);
451 
452                     str ~= q{
453                         if(%5$s.Exists("%1$s")) {
454                             %4$s.%2$s = %5$s.SubItem("%1$s").as!(%3$s)();
455                         } else {
456                             version (GEAR_CONFIG_DEBUG) log.warn("Undefined item: %%s.%1$s" , %5$s.FullPath);
457                         }                        
458                         version (GEAR_CONFIG_DEBUG) log.trace("%4$s.%2$s=%%s", %4$s.%2$s);
459 
460                     }.format(settingItemName, memberName,
461                             memberTypeString, returnParameter, incomingParameter);
462                 }
463             }
464         }
465         return str;
466     }
467 
468     private static string SetFunctionMemeber(memberType, string settingItemName,
469             string memberName, string returnParameter, string incomingParameter)() {
470         string r = "";
471         alias memeberParameters = Parameters!(memberType);
472         static if (memeberParameters.length == 1) {
473             alias parameterType = memeberParameters[0];
474 
475             static if (is(parameterType == struct) || is(parameterType == class)
476                     || is(parameterType == interface)) {
477                 // version (GEAR_CONFIG_DEBUG) pragma(msg, "skip method with class: " ~ memberName);
478             } else {
479                 // version (GEAR_CONFIG_DEBUG) pragma(msg, "method: " ~ memberName);
480 
481                 r = q{
482                     if(%5$s.Exists("%1$s")) {
483                         %4$s.%2$s(%5$s.SubItem("%1$s").as!(%3$s)());
484                     } else {
485                         version (GEAR_CONFIG_DEBUG) log.warn("Undefined item: %%s.%1$s" , %5$s.FullPath);
486                     }
487                     
488                     version (GEAR_CONFIG_DEBUG) log.trace("%4$s.%2$s=%%s", %4$s.%2$s);
489                     }.format(settingItemName, memberName,
490                         parameterType.stringof, returnParameter, incomingParameter);
491             }
492         } else {
493             // version (GEAR_CONFIG_DEBUG) pragma(msg, "skip method: " ~ memberName);
494         }
495 
496         return r;
497     }
498 
499     private static SetClassMemeber(memberType, string settingItemName,
500             string memberName, string returnParameter, string incomingParameter)() {
501         enum fullTypeName = fullyQualifiedName!(memberType);
502         enum memberModuleName = moduleName!(memberType);
503 
504         static if (settingItemName == memberName && hasUDA!(memberType, Configuration)) {
505             // try to get the ItemName from the UDA Configuration in a class or struct
506             enum newSettingItemName = getUDAs!(memberType, Configuration)[0].name;
507         } else {
508             enum newSettingItemName = settingItemName;
509         }
510 
511         // version (GEAR_CONFIG_DEBUG)
512         // {
513         //     pragma(msg, "module name: " ~ memberModuleName);
514         //     pragma(msg, "full type name: " ~ fullTypeName);
515         //     pragma(msg, "setting " ~ memberName ~ " with item " ~ newSettingItemName);
516         // }
517 
518         string r = q{
519             import %1$s;
520             
521             // log.trace("%5$s.%3$s is a class/struct.");
522             if(%6$s.Exists("%2$s")) {
523                 %5$s.%3$s = BuildItem!(%4$s)(%6$s.SubItem("%2$s"));
524             }
525             else {
526                 version (GEAR_CONFIG_DEBUG) log.warn("Undefined item: %%s.%2$s" , %6$s.FullPath);
527             }
528         }.format(memberModuleName, newSettingItemName,
529                 memberName, fullTypeName, returnParameter, incomingParameter);
530         return r;
531     }
532 
533     private void LoadConfig(string filename) {
534         if (!exists(filename) || isDir(filename)) {
535             throw new ConfigurationException("The config file doesn't exist: " ~ filename);
536         }
537 
538         auto f = File(filename, "r");
539         if (!f.isOpen())
540             return;
541         scope (exit)
542             f.close();
543         string section = "";
544         int line = 1;
545         while (!f.eof()) {
546             scope (exit)
547                 line += 1;
548             string str = f.readln();
549             str = strip(str);
550             if (str.length == 0)
551                 continue;
552             if (str[0] == '#' || str[0] == ';')
553                 continue;
554             auto len = str.length - 1;
555             if (str[0] == '[' && str[len] == ']') {
556                 section = str[1 .. len].strip;
557                 continue;
558             }
559             if (section != _section && section != "")
560                 continue;
561 
562             str = StripInlineComment(str);
563             auto site = str.indexOf("=");
564             enforce!BadFormatException((site > 0),
565                     format("Bad format in file %s, at line %d", filename, line));
566             string key = str[0 .. site].strip;
567             SetValue(key, str[site + 1 .. $].strip);
568         }
569     }
570 
571     private string StripInlineComment(string line) {
572         ptrdiff_t index = indexOf(line, "# ");
573 
574         if (index == -1)
575             return line;
576         else
577             return line[0 .. index];
578     }
579 
580     
581     private string _section;
582     private ConfigurationItem _value;
583     private string[string] _itemMap;
584 }
585 
586 // version (unittest) {
587 //     import geario.util.Configuration;
588 
589 //     @Configuration("app")
590 //     class TestConfig {
591 //         string test;
592 //         double time;
593 
594 //         TestHttpConfig http;
595 
596 //         @Value("optial", true)
597 //         int optial = 500;
598 
599 //         @Value(true)
600 //         int optial2 = 500;
601 
602 //         // mixin ReadConfig!TestConfig;
603 //     }
604 
605 //     @Configuration("http")
606 //     struct TestHttpConfig {
607 //         @Value("listen")
608 //         int value;
609 //         string addr;
610 
611 //         // mixin ReadConfig!TestHttpConfig;
612 //     }
613 // }
614 
615 // unittest {
616 //     import std.stdio;
617 //     import FE = std.file;
618 
619 //     FE.Write("test.config", `app.http.listen = 100
620 //     http.listen = 100
621 //     app.test = 
622 //     app.time = 0.25 
623 //     # this is  
624 //      ; start dev
625 //     [dev]
626 //     app.test = dev`);
627 
628 //     auto conf = new ConfigBuilder("test.config");
629 //     assert(conf.http.listen.value.as!long() == 100);
630 //     assert(conf.app.test.value() == "");
631 
632 //     auto confdev = new ConfigBuilder("test.config", "dev");
633 //     long tv = confdev.http.listen.value.as!long;
634 //     assert(tv == 100);
635 //     assert(confdev.http.listen.value.as!long() == 100);
636 //     writeln("----------", confdev.app.test.value());
637 //     string tvstr = cast(string) confdev.app.test.value;
638 
639 //     assert(tvstr == "dev");
640 //     assert(confdev.app.test.value() == "dev");
641 //     bool tvBool = confdev.app.test.value.as!bool;
642 //     assert(tvBool);
643 
644 //     assertThrown!(EmptyValueException)(confdev.app.host.value());
645 
646 //     TestConfig test = confdev.build!(TestConfig)();
647 //     assert(test.test == "dev");
648 //     assert(test.time == 0.25);
649 //     assert(test.http.value == 100);
650 //     assert(test.optial == 500);
651 //     assert(test.optial2 == 500);
652 // }