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