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