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