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 // }