1 /** 2 This module manages a config file format in the form of key=value. Much like an ini file but simpler. 3 4 Author: Paul Crane 5 */ 6 7 module variantconfig; 8 9 import std.conv : to; 10 import std.string; 11 import std.stdio : File, writeln; 12 import std.file : exists, readText; 13 import std.algorithm; 14 import std.range : take; 15 import std.array : empty, array; 16 import std.typecons : tuple; 17 import std.variant; 18 19 import typeutils; 20 21 private enum DEFAULT_GROUP_NAME = null; 22 private enum DEFAULT_CONFIG_FILE_NAME = "app.config"; 23 24 private struct KeyValueData 25 { 26 string key; 27 Variant value; 28 string group; 29 string comment; 30 } 31 32 /** 33 Handles the processing of config files. 34 */ 35 struct VariantConfig 36 { 37 private: 38 39 /** 40 Processes the text found in config file into an array of KeyValueData structures. 41 42 Params: 43 text = The text to be processed. 44 */ 45 bool processText(const string text) @trusted 46 { 47 auto lines = text.lineSplitter(); 48 string currentGroupName = DEFAULT_GROUP_NAME; 49 string currentComment; 50 51 foreach(line; lines) 52 { 53 line = strip(line); 54 55 if(line.empty) 56 { 57 continue; 58 } 59 else if(line.startsWith("#")) 60 { 61 currentComment = line[1..$]; 62 } 63 else if(line.startsWith("[")) 64 { 65 if(line.endsWith("]")) 66 { 67 immutable string groupName = line[1..$-1]; 68 currentGroupName = groupName; 69 } 70 else 71 { 72 return false; // Error incomplete group. 73 } 74 } 75 else 76 { 77 auto groupAndKey = line.findSplit("="); 78 immutable auto key = groupAndKey[0].stripRight(); 79 immutable auto value = groupAndKey[2].stripLeft(); 80 81 if(groupAndKey[1].length) 82 { 83 KeyValueData data; 84 85 data.key = key; 86 data.group = currentGroupName; 87 88 if(value.isInteger) 89 { 90 data.value = to!long(value); 91 } 92 else if(value.isDecimal) 93 { 94 data.value = to!double(value); 95 } 96 else if(isBoolean(value, AllowNumericBooleanValues.no)) 97 { 98 data.value = to!bool(value); 99 } 100 else 101 { 102 data.value = value; 103 } 104 105 if(currentComment != "") 106 { 107 data.comment = currentComment; 108 currentComment = string.init; 109 } 110 111 values_ ~= data; 112 } 113 else 114 { 115 return false; // Error line doesn't contain an = sign. 116 } 117 } 118 } 119 120 return true; 121 } 122 123 /** 124 Determines if the group string is in the form of group.key. 125 126 Params: 127 value = The string to test. 128 129 Returns: 130 true if the string is in the group.key form false otherwise. 131 */ 132 bool isGroupString(const string value) pure @safe 133 { 134 if(value.indexOf(".") == -1) 135 { 136 return false; 137 } 138 139 return true; 140 } 141 142 /** 143 Retrieves the group and key from a string in the form of group.key. 144 145 Params: 146 value = The string to process. 147 148 Returns: 149 A tuple containing the group and key. 150 */ 151 auto getGroupAndKeyFromString(const string value) pure @safe 152 { 153 auto groupAndKey = value.findSplit("."); 154 immutable auto group = groupAndKey[0].strip(); 155 immutable auto key = groupAndKey[2].strip(); 156 157 return tuple!("group", "key")(group, key); 158 } 159 160 public: 161 /** 162 Saves config values to the config file used when loading(loadFile). 163 */ 164 void save() @trusted 165 { 166 save(saveToFileName_); 167 } 168 169 /** 170 Saves config values to the config file. 171 172 Params: 173 fileName = Name of the file which values will be stored. 174 */ 175 void save(string fileName) @trusted 176 { 177 if(fileName.length && valuesModified_) 178 { 179 auto configfile = File(fileName, "w+"); 180 string curGroup; 181 182 foreach(data; values_) 183 { 184 if(curGroup != data.group) 185 { 186 curGroup = data.group; 187 188 if(curGroup != DEFAULT_GROUP_NAME) 189 { 190 configfile.writeln("[", curGroup, "]"); 191 } 192 } 193 194 if(data.comment.length) 195 { 196 configfile.writeln("#", data.comment); 197 } 198 199 configfile.writeln(data.key, " = ", data.value); 200 } 201 } 202 } 203 204 /** 205 Loads a config fileName(app.config by default) to be processed. 206 207 Params: 208 fileName = The name of the file to be processed/loaded. 209 Returns: 210 Returns true on a successful load false otherwise. 211 */ 212 bool loadFile(string fileName) @safe 213 { 214 saveToFileName_ = fileName; 215 216 if(fileName.exists) 217 { 218 return processText(fileName.readText); 219 } 220 221 return false; 222 } 223 224 /** 225 Similar to loadFile but loads and processes the passed string instead. 226 227 Params: 228 text = The string to process. 229 Returns: 230 Returns true on a successful load false otherwise. 231 */ 232 233 bool loadString(const string text) @safe 234 { 235 if(text.length) 236 { 237 return processText(text); 238 } 239 240 return false; 241 } 242 243 /** 244 Retrieves the value T associated with key where T is the designated type to be converted to. 245 246 Params: 247 key = Name of the key to get. 248 defaultValue = Allow the assignment of a default value if key does not exist. 249 250 Returns: 251 The value associated with key. 252 253 */ 254 Variant get(T)(const string key, T defaultValue) @trusted 255 { 256 if(isGroupString(key)) 257 { 258 immutable auto groupAndKey = getGroupAndKeyFromString(key); 259 return get(groupAndKey.group, groupAndKey.key, defaultValue); 260 } 261 262 return get(DEFAULT_GROUP_NAME, key, defaultValue); 263 } 264 265 /** 266 Retrieves the value T associated with key where T is the designated type to be converted to. 267 268 Params: 269 group = Name of the group to retrieve ie portion [groupName] of config file/string. 270 key = Name of the key to get. 271 defaultValue = Allow the assignment of a default value if key does not exist. 272 273 Returns: 274 The value of value of the key/value pair. 275 276 */ 277 Variant get(T)(const string group, const string key, T defaultValue) @trusted 278 { 279 if(containsGroup(group)) 280 { 281 return getGroupValue(group, key, defaultValue); 282 } 283 284 return Variant(defaultValue); 285 } 286 287 /** 288 Gets the value associated with the group and key. 289 290 Params: 291 group = Name of the group the value is stored in. 292 key = Name of the key the value is stored in. 293 defaultValue = The value to use if group and or key is not found. 294 295 Returns: 296 The value associated with the group and key. 297 */ 298 Variant getGroupValue(T)(const string group, const string key, const T defaultValue = T.init) @trusted 299 { 300 Variant value = defaultValue; 301 auto found = values_.filter!(a => (a.group == group) && (a.key == key)); 302 303 if(!found.empty) 304 { 305 value = found.front.value; 306 } 307 308 return value; 309 } 310 311 /** 312 Retrieves key/values associated with the group portion of a config file/string. 313 314 Params: 315 group = Name of the the group to retrieve. 316 317 Returns: 318 Returns an array containing all the key/values associated with group. 319 320 */ 321 auto getGroup(const string group) @trusted 322 { 323 return values_.filter!(a => a.group == group); 324 } 325 326 /** 327 Retrieves an array containing key/values of all groups in the configfile omitting groupless key/values. 328 329 Returns: 330 An array containing every group. 331 */ 332 auto getGroups() @trusted 333 { 334 return values_.filter!(a => a.group != ""); 335 } 336 337 /** 338 Sets a config value. 339 340 Params: 341 key = Name of the key to set. Can be in the group.key form. 342 value = The value to be set to. 343 */ 344 void set(T)(const string key, const T value) @trusted 345 { 346 if(isGroupString(key)) 347 { 348 immutable auto groupAndKey = getGroupAndKeyFromString(key); 349 set(groupAndKey.group, groupAndKey.key, value); 350 } 351 else 352 { 353 set(DEFAULT_GROUP_NAME, key, value); 354 } 355 } 356 357 /** 358 Sets a config value. 359 360 Params: 361 group = Name of the group key belongs to. 362 key = Name of the key to set. 363 value = The value to be set to. 364 */ 365 void set(T = string)(const string group, const string key, const T value) @trusted 366 { 367 auto foundValue = values_.filter!(a => (a.group == group) && (a.key == key)); 368 369 if(foundValue.empty) 370 { 371 KeyValueData data; 372 373 data.key = key; 374 data.group = group; 375 data.value = value; 376 377 values_ ~= data; 378 } 379 else 380 { 381 foundValue.front.value = value; 382 } 383 384 valuesModified_ = true; 385 } 386 387 /** 388 Determines if the key is found in the config file. 389 The key can be either its name of in the format of groupName.keyName or just the key name. 390 391 Params: 392 key = Name of the key to get the value of 393 394 Returns: 395 true if the config file contains the key false otherwise. 396 */ 397 bool contains(const string key) @safe 398 { 399 if(isGroupString(key)) 400 { 401 immutable auto groupAndKey = getGroupAndKeyFromString(key); 402 return contains(groupAndKey.group, groupAndKey.key); 403 } 404 405 return contains(DEFAULT_GROUP_NAME, key); 406 } 407 408 /** 409 Determines if the key is found in the config file. 410 411 Params: 412 group = Name of the group to get entries from. 413 key = Name of the key to get the value from. 414 415 Returns: 416 true if the config file contains the key false otherwise. 417 */ 418 bool contains(const string group, const string key) @trusted 419 { 420 if(containsGroup(group)) 421 { 422 auto groupValues = getGroup(group); 423 return groupValues.canFind!(a => a.key == key); 424 } 425 426 return false; // The group wasn't found so no point in checking for a group and value. 427 } 428 429 /** 430 Determines if the given group exists. 431 432 Params: 433 group = Name of the group to check for. 434 435 Returns: 436 true if the group exists false otherwise. 437 */ 438 bool containsGroup(const string group) @trusted 439 { 440 return values_.canFind!(a => a.group == group); 441 } 442 443 /** 444 Removes a key/value from config file. 445 The key can be either its name of in the format of groupName.keyName or just the keyName. 446 447 Params: 448 key = Name of the key to remove. Can be in the group.name format. 449 450 Returns: 451 true if it was successfully removed false otherwise. 452 */ 453 bool remove(const string key) @trusted 454 { 455 if(isGroupString(key)) 456 { 457 immutable auto groupAndKey = getGroupAndKeyFromString(key); 458 459 valuesModified_ = true; 460 return remove(groupAndKey.group, groupAndKey.key); 461 } 462 else 463 { 464 valuesModified_ = true; 465 return remove(DEFAULT_GROUP_NAME, key); 466 } 467 } 468 469 /** 470 Removes a key/value from config file. 471 The key can be either its name of in the format of group.keyor just the key. 472 473 Params: 474 group = Name of the group where key is found. 475 key = Name of the key to remove. 476 477 Returns: 478 true if it was successfully removed false otherwise. 479 */ 480 bool remove(const string group, const string key) @trusted 481 { 482 values_ = values_.remove!(a => (a.group == group) && (a.key == key)); 483 valuesModified_ = true; 484 485 return contains(group, key); 486 } 487 488 /** 489 Removes a group from the config file. 490 491 Params: 492 group = Name of the group to remove. 493 494 Returns: 495 true if group was successfully removed false otherwise. 496 */ 497 bool removeGroup(const string group) @trusted 498 { 499 values_ = values_.remove!(a => a.group == group); 500 valuesModified_ = true; 501 502 return containsGroup(group); 503 } 504 505 /** 506 Allows config values to be accessed as you would with an associative array. 507 508 Params: 509 key = Name of the value to retrieve 510 511 Returns: 512 The string value associated with the key. 513 */ 514 Variant opIndex(const string key) @trusted 515 { 516 Variant defaultValue; 517 518 return get(key, defaultValue); 519 } 520 521 /** 522 Allows config values to be assigned as you would with an associative array. 523 524 Params: 525 key = Name of the key to assign the value to. 526 value = The value in which key should be assigned to. 527 */ 528 void opIndexAssign(T)(T value, const string key) @trusted 529 { 530 set(key, value); 531 } 532 533 /** 534 Converts the value of key to type of T. Works the same as std.variant's coerce. 535 536 Params: 537 key = Name of the key to retrieve. 538 defaultValue = The value to use if key isn't found. 539 540 Returns: 541 T = The converted value. 542 */ 543 T coerce(T = string)(const string key, const T defaultValue = T.init) @trusted 544 { 545 Variant value = defaultValue; 546 547 if(contains(key)) 548 { 549 value = get(key, defaultValue); 550 } 551 552 return value.coerce!T; 553 } 554 555 /// Gets the value and converts it to a bool. 556 alias asBool = coerce!bool; 557 558 /// Gets the value and converts it to a int. 559 alias asInt = coerce!int; 560 561 /// Gets the value and converts it to a float. 562 alias asFloat = coerce!float; 563 564 /// Gets the value and converts it to a real. 565 alias asReal = coerce!real; 566 567 /// Gets the value and converts it to a long. 568 alias asLong = coerce!long; 569 570 /// Gets the value and converts it to a byte. 571 alias asByte = coerce!byte; 572 573 /// Gets the value and converts it to a short. 574 alias asShort = coerce!short; 575 576 /// Gets the value and converts it to a double. 577 alias asDouble = coerce!double; 578 579 /// Gets the value and converts it to a string. 580 alias asString = coerce!string; 581 582 /// Simpler alias that conveys the meaning more appropriately. 583 alias as = coerce; 584 585 private: 586 KeyValueData[] values_; 587 bool valuesModified_; 588 string saveToFileName_ = DEFAULT_CONFIG_FILE_NAME; 589 } 590 591 /// 592 unittest 593 { 594 string text = " 595 aBool=true 596 decimal = 3443.443 597 number=12071 598 #Here is a comment 599 sentence=This is a really long sentence to test for a really long value string! 600 time=12:04 601 [section] 602 groupSection=is really cool if this works! 603 japan=true 604 babymetal=the one 605 [another] 606 #And another comment! 607 world=hello 608 japan=false 609 "; 610 611 VariantConfig config; 612 613 immutable bool loaded = config.loadString(text); 614 assert(loaded, "Failed to load string!"); 615 616 assert(config.containsGroup("section")); 617 config.removeGroup("section"); 618 assert(config.containsGroup("section") == false); 619 620 assert(config.get("aBool", true).coerce!bool == true); 621 assert(config.asBool("aBool")); // Syntactic sugar 622 assert(config["aBool"].coerce!bool == true); // Also works but rather awkward 623 assert(config.coerce!bool("aBool") == true); // Also works and more natural 624 625 assert(config.contains("time")); 626 627 immutable auto number = config["number"]; 628 629 assert(number == 12_071); 630 assert(config["decimal"] == 3443.443); 631 632 assert(config.contains("another.world")); 633 assert(config["another.world"] == "hello"); 634 635 config["another.japan"] = true; 636 assert(config["another.japan"] == true); 637 638 config.remove("another.world"); 639 640 assert(config.contains("another.world") == false); 641 assert(config.contains("anothers", "world") == false); 642 643 assert(config.contains("number")); 644 config.remove("number"); 645 assert(config.contains("number") == false); 646 647 // Tests for nonexistent keys 648 assert(config.asString("nonexistent", "Value doesn't exist!") == "Value doesn't exist!"); 649 config["nonexistent"] = "The value now exists!!!"; 650 assert(config.asString("nonexistent", "The value now exists!!!") == "The value now exists!!!"); 651 652 auto group = config.getGroup("another"); 653 654 foreach(value; group) 655 { 656 assert(value.key == "japan"); 657 assert(value.value == true); 658 } 659 660 config.set("aBool", false); 661 assert(config["aBool"] == false); 662 debug config.save(); 663 664 config["aBool"] = true; 665 assert(config["aBool"] == true); 666 assert(config["aBool"].toString == "true"); 667 668 assert(config.get!int("numberGroup", "numberValue", 1234) == 1234); 669 670 immutable string customFileName = "custom-config-format.dat"; 671 debug config.save(customFileName); 672 673 VariantConfig configLoadTest; 674 675 bool isLoadedTest = configLoadTest.loadFile("doesnt-exist.dat"); 676 assert(isLoadedTest == false); 677 678 isLoadedTest = configLoadTest.loadFile(customFileName); 679 assert(isLoadedTest == true); 680 681 string noEqualSign = " 682 equal=sign 683 time=12:04 684 This is a really long sentence to test for a really long value string! 685 "; 686 687 immutable bool equalSignValue = config.loadString(noEqualSign); 688 assert(equalSignValue == false); 689 690 auto groups = config.getGroups(); 691 692 writeln("Listing groups: "); 693 writeln; 694 695 foreach(currGroup; groups) 696 { 697 writeln(currGroup); 698 } 699 700 string invalidGroup = " 701 [first] 702 equal=sign 703 time=12:04 704 [second 705 another=key value is here 706 "; 707 708 immutable bool invalidGroupValue = config.loadString(invalidGroup); 709 assert(invalidGroupValue == false); 710 711 string emptyString; 712 713 immutable bool emptyLoad = config.loadString(emptyString); 714 assert(emptyLoad == false); 715 }