1 /** 2 * Configuration management 3 * 4 * Configuration entries and 5 * a registry in which to 6 * manage a set of them 7 * 8 * Authors: Tristan Brice Velloza Kildaire (deavmi) 9 */ 10 module niknaks.config; 11 12 import std.string : format; 13 14 version(unittest) 15 { 16 import std.stdio : writeln; 17 } 18 19 /** 20 * A union which expands to 21 * the byte-width of its 22 * biggest member, allowing 23 * us to have space enough 24 * for any one exclusive member 25 * at a time 26 * 27 * See_Also: ConfigEntry 28 */ 29 private union ConfigValue 30 { 31 string text; 32 size_t integer; 33 bool flag; 34 string[] textArray; 35 } 36 37 /** 38 * The type of the entry 39 */ 40 public enum ConfigType 41 { 42 /** 43 * A string 44 */ 45 TEXT, 46 47 /** 48 * An integer 49 */ 50 NUMERIC, 51 52 /** 53 * A boolean 54 */ 55 FLAG, 56 57 /** 58 * A string array 59 */ 60 ARRAY 61 } 62 63 /** 64 * An exception thrown when you misuse 65 * a configuration entry 66 */ 67 public final class ConfigException : Exception 68 { 69 private this(string msg) 70 { 71 super(msg); 72 } 73 } 74 75 /** 76 * A configuration entry which 77 * acts as a typed union and 78 * supports certain fixed types 79 */ 80 public struct ConfigEntry 81 { 82 private ConfigValue value; 83 private ConfigType type; 84 85 /** 86 * A flag which is used to 87 * know if a value has been 88 * set at all. This helps 89 * with the fact that 90 * an entry can be constructed 91 * without having a value set 92 */ 93 private bool isSet = false; 94 95 /** 96 * Ensures a value is set 97 */ 98 private void ensureSet() 99 { 100 if(!this.isSet) 101 { 102 throw new ConfigException("This config entry has not yet been set"); 103 } 104 } 105 106 /** 107 * Marks this entry as having 108 * a value set 109 */ 110 private void set() 111 { 112 this.isSet = true; 113 } 114 115 // TODO: Must have an unset flag 116 // @disable 117 // private this(); 118 import std.traits : isIntegral; 119 120 this(EntryType)(EntryType value) 121 if 122 ( 123 __traits(isSame, EntryType, string[]) || 124 __traits(isSame, EntryType, string) || 125 isIntegral!(EntryType) || 126 __traits(isSame, EntryType, bool) 127 ) 128 { 129 ConfigValue _v; 130 ConfigType _t; 131 static if(__traits(isSame, EntryType, string[])) 132 { 133 _v.textArray = value; 134 _t = ConfigType.ARRAY; 135 } 136 else static if(__traits(isSame, EntryType, string)) 137 { 138 _v.text = value; 139 _t = ConfigType.TEXT; 140 } 141 else static if(isIntegral!(EntryType)) 142 { 143 _v.integer = cast(size_t)value; 144 _t = ConfigType.NUMERIC; 145 } 146 else static if(__traits(isSame, EntryType, bool)) 147 { 148 _v.flag = value; 149 _t = ConfigType.FLAG; 150 } 151 152 this(_v, _t); 153 } 154 155 /** 156 * Constructs a new `ConfigEntry` 157 * with the given value and type 158 * 159 * Params: 160 * value = the value itself 161 * type = the value's type 162 */ 163 private this(ConfigValue value, ConfigType type) 164 { 165 this.value = value; 166 this.type = type; 167 168 set(); 169 } 170 171 /** 172 * Creates a new configuration entry 173 * containing text 174 * 175 * Params: 176 * text = the text 177 * Returns: a `ConfigEntry` 178 */ 179 public static ConfigEntry ofText(string text) 180 { 181 return ConfigEntry(text); 182 } 183 184 /** 185 * Creates a new configuration entry 186 * containing an integer 187 * 188 * Params: 189 * i = the integer 190 * Returns: a `ConfigEntry` 191 */ 192 public static ConfigEntry ofNumeric(size_t i) 193 { 194 return ConfigEntry(i); 195 } 196 197 /** 198 * Creates a new configuration entry 199 * containing a flag 200 * 201 * Params: 202 * flag = the flag 203 * Returns: a `ConfigEntry` 204 */ 205 public static ConfigEntry ofFlag(bool flag) 206 { 207 return ConfigEntry(flag); 208 } 209 210 /** 211 * Creates a new configuration entry 212 * containing a textual array 213 * 214 * Params: 215 * array = the textual array 216 * Returns: a `ConfigEntry` 217 */ 218 public static ConfigEntry ofArray(string[] array) 219 { 220 return ConfigEntry(array); 221 } 222 223 /** 224 * Returns the type of the 225 * entry's value 226 * 227 * Returns: a `ConfigType` 228 */ 229 public ConfigType getType() 230 { 231 return this.type; 232 } 233 234 /** 235 * Ensures the requested type 236 * matches the current type 237 * set 238 * 239 * Params: 240 * requested = the requested 241 * type 242 * Returns: `true` if the types 243 * are the same, `false` otherwise 244 */ 245 private bool ensureTypeMatch0(ConfigType requested) 246 { 247 return getType() == requested; 248 } 249 250 /** 251 * A version of the type 252 * matcher but which throws 253 * an exception on type mismatch 254 * 255 * See_Also: `ensureTypeMatch0(ConfigType)` 256 */ 257 private void ensureTypeMatch(ConfigType requested) 258 { 259 if(!ensureTypeMatch0(requested)) 260 { 261 throw new ConfigException(format("The entry is not of type '%s'", requested)); 262 } 263 } 264 265 /** 266 * Obtains the numeric value 267 * of this entry 268 * 269 * Returns: an integer 270 * Throws: ConfigException if 271 * the type of the value in this 272 * entry is not numeric 273 */ 274 public size_t numeric() 275 { 276 ensureSet; 277 ensureTypeMatch(ConfigType.NUMERIC); 278 return this.value.integer; 279 } 280 281 /** 282 * Obtains the textual array 283 * value of this entry 284 * 285 * Returns: a `string[]` 286 * Throws: ConfigException if 287 * the type of the value in this 288 * entry is not a textual array 289 */ 290 public string[] array() 291 { 292 ensureSet; 293 ensureTypeMatch(ConfigType. ARRAY); 294 return this.value.textArray; 295 } 296 297 /** 298 * See_Also: `array()` 299 */ 300 public string[] opSlice() 301 { 302 return array(); 303 } 304 305 /** 306 * Obtains the flag value 307 * of this entry 308 * 309 * Returns: a `string[]` 310 * Throws: ConfigException if 311 * the type of the value in this 312 * entry is not a flag 313 */ 314 public bool flag() 315 { 316 ensureSet; 317 ensureTypeMatch(ConfigType.FLAG); 318 return this.value.flag; 319 } 320 321 /** 322 * See_Also: `flag()` 323 */ 324 public bool isTrue() 325 { 326 return flag() == true; 327 } 328 329 /** 330 * See_Also: `flag()` 331 */ 332 public bool isFalse() 333 { 334 return flag() == false; 335 } 336 337 /** 338 * Obtains the text value 339 * of this entry 340 * 341 * Returns: a string 342 * Throws: ConfigException if 343 * the type of the value in this 344 * entry is not a string 345 */ 346 public string text() 347 { 348 ensureSet; 349 ensureTypeMatch(ConfigType.TEXT); 350 return this.value.text; 351 } 352 353 /** 354 * Obtains the value of this 355 * configuration entry dependant 356 * on the requested casting type 357 * and matching that to the supported 358 * types of the configuration entry 359 * 360 * Returns: a value of type `T` 361 */ 362 public T opCast(T)() 363 { 364 static if(__traits(isSame, T, bool)) 365 { 366 return flag(); 367 } 368 else static if(__traits(isSame, T, string)) 369 { 370 return text(); 371 } 372 else static if(isIntegral!(T)) 373 { 374 return cast(T)numeric(); 375 } 376 else static if(__traits(isSame, T, string[])) 377 { 378 return array(); 379 } 380 else 381 { 382 pragma(msg, "ConfigEntry opCast(): Cannot cast to a type '", T, "'"); 383 static assert(false); 384 } 385 } 386 } 387 388 /** 389 * Tests out using the configuration 390 * entry and its various operator 391 * overloads 392 */ 393 unittest 394 { 395 ConfigEntry entry = ConfigEntry.ofArray(["hello", "world"]); 396 assert(entry[] == ["hello", "world"]); 397 398 entry = ConfigEntry.ofNumeric(1); 399 assert(entry.numeric() == 1); 400 401 entry = ConfigEntry.ofText("hello"); 402 assert(cast(string)entry == "hello"); 403 404 entry = ConfigEntry.ofFlag(true); 405 assert(entry); 406 } 407 408 /** 409 * Tests out the erroneous usage of a 410 * configuration entry 411 */ 412 unittest 413 { 414 ConfigEntry entry = ConfigEntry.ofText("hello"); 415 416 try 417 { 418 entry[]; 419 assert(false); 420 } 421 catch(ConfigException e) 422 { 423 424 } 425 } 426 427 /** 428 * Tests out the erroneous usage of a 429 * configuration entry 430 */ 431 unittest 432 { 433 ConfigEntry entry; 434 435 try 436 { 437 entry[]; 438 assert(false); 439 } 440 catch(ConfigException e) 441 { 442 443 } 444 } 445 446 /** 447 * An entry derived from 448 * the `Registry` containing 449 * the name and the configuration 450 * entry itself 451 */ 452 public struct RegistryEntry 453 { 454 private string name; 455 private ConfigEntry val; 456 457 /** 458 * Constructs a new `RegistryEntry` 459 * with the given name and configuration 460 * entry 461 * 462 * Params: 463 * name = the name 464 * entry = the entry itself 465 */ 466 this(string name, ConfigEntry entry) 467 { 468 this.name = name; 469 this.val = entry; 470 } 471 472 /** 473 * Constructs a new `RegistryEntry` 474 * with the given name and configuration 475 * entry 476 * 477 * Params: 478 * name = the name 479 * entry = the entry itself 480 */ 481 this(T)(string name, T entry) 482 { 483 this.name = name; 484 this.val = ConfigEntry(entryVal); 485 } 486 487 /** 488 * Obtains the entry's name 489 * 490 * Returns: the name 491 */ 492 public string getName() 493 { 494 return this.name; 495 } 496 497 /** 498 * Obtains the entry itself 499 * 500 * Returns: a `ConfigEntry` 501 */ 502 public ConfigEntry getEntry() 503 { 504 return this.val; 505 } 506 507 /** 508 * Returns the configugration 509 * entry's type 510 * 511 * See_Also: `ConfigEntry.getType()` 512 * Returns: a `ConfigType` 513 */ 514 public ConfigType getType() 515 { 516 return getEntry().getType(); 517 } 518 } 519 520 /** 521 * An exception thrown when something 522 * goes wrong with your usage of the 523 * `Registry` 524 */ 525 public final class RegistryException : Exception 526 { 527 private this(string msg) 528 { 529 super(msg); 530 } 531 } 532 533 /** 534 * A registry for managing 535 * multiple mappings of 536 * string-based names to 537 * configuration entries 538 */ 539 public struct Registry 540 { 541 private ConfigEntry[string] entries; 542 private bool allowOverwriteEntry; 543 544 /** 545 * Creates a new `Registry` 546 * and sets the overwriting policy 547 * 548 * Params: 549 * allowOverwritingOfEntries = `true` 550 * if you want to allow overwriting of 551 * previously added entries, otherwise 552 * `false` 553 */ 554 this(bool allowOverwritingOfEntries) 555 { 556 setAllowOverwrite(allowOverwritingOfEntries); 557 } 558 559 /** 560 * Checks if an entry is present 561 * 562 * Params: 563 * name = the name 564 * Returns: `true` if present, 565 * otherwise `false` 566 */ 567 public bool hasEntry(string name) 568 { 569 return getEntry0(name) !is null; 570 } 571 572 /** 573 * Ontains a pointer to the configuration 574 * entry at the given key. 575 * 576 * Params: 577 * name = the key 578 * Returns: a `ConfigEntry*` if found, 579 * otherwise `null` 580 */ 581 private ConfigEntry* getEntry0(string name) 582 { 583 ConfigEntry* potEntry = name in this.entries; 584 return potEntry; 585 } 586 587 /** 588 * Obtains a pointer to the configuration 589 * entry at the given key. Allowing you 590 * to swap out its contents directly if 591 * you want to. 592 * 593 * Params: 594 * name = the key 595 * Returns: a `ConfigEntry*` if found, 596 * otherwise `null` 597 */ 598 public ConfigEntry* opBinaryRight(string op)(string name) 599 if(op == "in") 600 { 601 return getEntry0(name); 602 } 603 604 /** 605 * Obtain a configuration entry 606 * at the given key 607 * 608 * Params: 609 * name = the key 610 * entry = the found entry 611 * (if any) 612 * Returns: `true` if found, 613 * otherwise `false` 614 */ 615 public bool getEntry_nothrow(string name, ref ConfigEntry entry) 616 { 617 ConfigEntry* potEntry = getEntry0(name); 618 if(potEntry is null) 619 { 620 return false; 621 } 622 623 entry = *potEntry; 624 return true; 625 } 626 627 /** 628 * Obtain a configuration entry 629 * at the given key 630 * 631 * Params: 632 * name = the key 633 * Returns: a configuration entry 634 * Throws: RegistryException if 635 * there is no entry at that key 636 */ 637 public ConfigEntry opIndex(string name) 638 { 639 ConfigEntry entry; 640 if(!getEntry_nothrow(name, entry)) 641 { 642 throw new RegistryException(format("Cannot find an entry by the name of '%s'", name)); 643 } 644 645 return entry; 646 } 647 648 /** 649 * Set whether or not the overwriting 650 * of an entry should be allowed 651 * 652 * Params: 653 * flag = `true` if to allow, `false` 654 * if to deny 655 */ 656 public void setAllowOverwrite(bool flag) 657 { 658 this.allowOverwriteEntry = flag; 659 } 660 661 /** 662 * Adds a new configuration entry at the 663 * given key and allows you to choose 664 * certain behaviors based on the 665 * existence or non-existence of 666 * an entry at the same key. 667 * 668 * Params: 669 * name = the name of the entry 670 * entry = the entry itself 671 * allowOverWriteNow = if `true` 672 * then if an entry exists already 673 * at that key it will be overwritten, 674 * otherwise an exception will be thrown 675 * allowSetOnCreation = if there is 676 * no entry at the given key then, 677 * if `true`, an entry will be created, 678 * otherwise an exception will be thrown 679 */ 680 private void newEntry(string name, ConfigEntry entry, bool allowOverWriteNow, bool allowSetOnCreation) 681 { 682 // Obtain the address of the value that occupies the value 683 // the key in the map 684 ConfigEntry* entryExist = getEntry0(name); 685 686 // If something is present but overwiritng is disabled 687 if((entryExist !is null) && !allowOverWriteNow) 688 { 689 throw new RegistryException(format("An entry already exists at '%s' and overwriting is not allowed", name)); 690 } 691 // If something is present and overwiring is enabled 692 else if(entryExist !is null) 693 { 694 // Now simply update the data in-place 695 *entryExist = entry; 696 } 697 // If nothing is present but setting-on-creation is enabled 698 else if(allowSetOnCreation) 699 { 700 // Then create the entry 701 this.entries[name] = entry; 702 } 703 // If nothing is present BUT setting-on-creation was NOT allowed 704 else 705 { 706 throw new RegistryException(format("Cannot set-on-creation for entry '%s' as it is not allowed", name)); 707 } 708 } 709 710 /** 711 * Creates a new entry and adds it 712 * 713 * An exception is thrown if an entry 714 * at that key exists and the policy 715 * for overwriting is to deny 716 * 717 * Params: 718 * name = the key 719 * entry = the configuration entry 720 */ 721 public void newEntry(string name, ConfigEntry entry) 722 { 723 newEntry(name, entry, this.allowOverwriteEntry, true); 724 } 725 726 /** 727 * See_Also: `newEntry(name, ConfigEntry)` 728 */ 729 public void newEntry(string name, int numeric) 730 { 731 newEntry(name, ConfigEntry.ofNumeric(numeric)); 732 } 733 734 /** 735 * See_Also: `newEntry(name, ConfigEntry)` 736 */ 737 public void newEntry(string name, string text) 738 { 739 newEntry(name, ConfigEntry.ofText(text)); 740 } 741 742 /** 743 * See_Also: `newEntry(name, ConfigEntry)` 744 */ 745 public void newEntry(string name, bool flag) 746 { 747 newEntry(name, ConfigEntry.ofFlag(flag)); 748 } 749 750 /** 751 * See_Also: `newEntry(name, ConfigEntry)` 752 */ 753 public void newEntry(string name, string[] array) 754 { 755 newEntry(name, ConfigEntry.ofArray(array)); 756 } 757 758 /** 759 * Sets the entry at the given name 760 * to the provided entry 761 * 762 * This will throw an exception if 763 * the entry trying to be set does 764 * not yet exist. 765 * 766 * Overwriting will only be allowed 767 * if the policy allows it. 768 * 769 * Params: 770 * name = the key 771 * entry = the configuration 772 * entry 773 */ 774 public void setEntry(string name, ConfigEntry entry) 775 { 776 newEntry(name, entry, this.allowOverwriteEntry, false); 777 } 778 779 /** 780 * Assigns the provided configuration 781 * entry to the provided name 782 * 783 * Take note that using this method 784 * will create the entry if it does 785 * not yet exist. 786 * 787 * It will also ALWAYS allow overwriting. 788 * 789 * Params: 790 * entry = the entry to add 791 * name = the name at which to 792 * add the entry 793 */ 794 public void opIndexAssign(ConfigEntry entry, string name) 795 { 796 newEntry(name, entry, true, true); 797 } 798 799 /** 800 * See_Also: `opIndexAssign(ConfigEntry, string)` 801 */ 802 public void opIndexAssign(size_t numeric, string name) 803 { 804 opIndexAssign(ConfigEntry.ofNumeric(numeric), name); 805 } 806 807 /** 808 * See_Also: `opIndexAssign(ConfigEntry, string)` 809 */ 810 public void opIndexAssign(string entry, string name) 811 { 812 opIndexAssign(ConfigEntry.ofText(entry), name); 813 } 814 815 /** 816 * See_Also: `opIndexAssign(ConfigEntry, string)` 817 */ 818 public void opIndexAssign(bool flag, string name) 819 { 820 opIndexAssign(ConfigEntry.ofFlag(flag), name); 821 } 822 823 /** 824 * See_Also: `opIndexAssign(ConfigEntry, string)` 825 */ 826 public void opIndexAssign(string[] array, string name) 827 { 828 opIndexAssign(ConfigEntry.ofArray(array), name); 829 } 830 831 /** 832 * Returns all the entries in the 833 * registry as a mapping of their 834 * name to their configuration entry 835 * 836 * See_Also: RegistryEntry 837 * Returns: an array of registry 838 * entries 839 */ 840 public RegistryEntry[] opSlice() 841 { 842 RegistryEntry[] entrieS; 843 foreach(string entryName; this.entries.keys()) 844 { 845 entrieS ~= RegistryEntry(entryName, this.entries[entryName]); 846 } 847 848 return entrieS; 849 } 850 } 851 852 /** 853 * Tests out the working with the 854 * registry in order to manage 855 * a set of named configuration 856 * entries 857 */ 858 unittest 859 { 860 Registry reg = Registry(false); 861 862 // Add an entry 863 reg.newEntry("name", "Tristan"); 864 865 // Check it exists 866 assert(reg.hasEntry("name")); 867 868 // Adding it again should fail 869 try 870 { 871 reg.newEntry("name", "Tristan2"); 872 assert(false); 873 } 874 catch(RegistryException e) 875 { 876 877 } 878 879 // Check that the entry still has the right value 880 assert(cast(string)reg["name"] == "Tristan"); 881 882 // // Add a new entry and test its prescence 883 reg["age"] = 24; 884 assert(cast(int)reg["age"]); 885 886 // // Update it 887 reg["age"] = 25; 888 assert(cast(int)reg["age"] == 25); 889 890 // Obtain a handle on the configuration 891 // entry, then update it and read it back 892 // to confirm 893 ConfigEntry* ageEntry = "age" in reg; 894 *ageEntry = ConfigEntry.ofNumeric(69_420); 895 assert(cast(int)reg["age"] == 69_420); 896 897 // Should not be able to set entry it not yet existent 898 try 899 { 900 reg.setEntry("male", ConfigEntry.ofFlag(true)); 901 assert(false); 902 } 903 catch(RegistryException e) 904 { 905 906 } 907 908 // All entries 909 RegistryEntry[] all = reg[]; 910 assert(all.length == 2); 911 writeln(all); 912 }