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