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