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