1 /******************************************************************************* 2 3 Load Configuration from Config File 4 5 Copyright: 6 Copyright (c) 2009-2017 dunnhumby Germany GmbH. 7 All rights reserved. 8 9 License: 10 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 11 Alternatively, this file may be distributed under the terms of the Tango 12 3-Clause BSD License (see LICENSE_BSD.txt for details). 13 14 *******************************************************************************/ 15 16 module ocean.util.config.ConfigParser; 17 18 19 import ocean.meta.types.Qualifiers; 20 21 import ocean.core.Array; 22 import ocean.core.ExceptionDefinitions; 23 import ocean.core.Exception; 24 import ocean.core.Enforce; 25 import ocean.meta.types.Arrays /* : ElementTypeOf */; 26 import ocean.io.Stdout; 27 import ocean.io.stream.Format; 28 import ocean.io.stream.TextFile; 29 import ocean.text.convert.Float: toFloat; 30 import ocean.text.convert.Formatter; 31 import ocean.text.convert.Integer_tango: toLong; 32 import ocean.text.convert.Utf; 33 import ocean.text.Util: locate, trim, delimit, lines; 34 35 /****************************************************************************** 36 37 ConfigException 38 39 *******************************************************************************/ 40 41 /// Exception thrown whenever an error happened while parsing / using 42 /// the configuration, such as `getStrict` on a missing field 43 public class ConfigException : Exception 44 { 45 mixin DefaultExceptionCtor; 46 } 47 48 /******************************************************************************* 49 50 Config reads all properties of the application from an INI style of the 51 following format: 52 53 --- 54 // -------------------------- 55 // Config Example 56 // -------------------------- 57 58 ; Database config parameters 59 [DATABASE] 60 table1 = "name_of_table1" 61 table2 = "name_of_table2" 62 63 ; An example of a multi-value parameter 64 fields = "create_time" 65 "update_time" 66 "count" 67 68 ; Logging config parameters 69 [LOGGING] 70 level = info 71 file = "access.log" 72 --- 73 74 The properties defined in the file are read and stored in an internal array, 75 which can then be accessed through get and set methods as follows: 76 77 The parseFile() method only needs to be called once, though may be called 78 multiple times if the config file needs to be re-read from the file on disk. 79 80 TODO: 81 At the moment it's possible to set a key (se `set`), but it's not possible 82 to write back to the file. 83 84 *******************************************************************************/ 85 86 public class ConfigParser 87 { 88 /*************************************************************************** 89 90 Variable Iterator. Iterates over keys or key/value pairs of a category. 91 The values are converted to T, unless T is istring. 92 93 ***************************************************************************/ 94 95 public struct VarIterator ( T = istring ) 96 { 97 ValueNode[istring]* vars; 98 99 100 /*********************************************************************** 101 102 Variable Iterator. Iterates over key/value pairs of a category. 103 104 ***********************************************************************/ 105 106 public int opApply ( scope int delegate ( ref istring key, ref T val ) dg ) 107 { 108 if ( this.vars !is null ) 109 { 110 foreach ( key, valnode; *this.vars ) 111 { 112 auto val = conv!(T)(valnode.value); 113 114 if ( int result = dg(key, val) ) 115 return result; 116 } 117 } 118 119 return 0; 120 } 121 122 123 /*********************************************************************** 124 125 Variable Iterator. Iterates over keys of a category. 126 127 ***********************************************************************/ 128 129 public int opApply ( scope int delegate ( ref istring x ) dg ) 130 { 131 return this.opApply( 132 (ref istring key, ref istring val) 133 { 134 return dg(key); 135 }); 136 } 137 } 138 139 140 /*************************************************************************** 141 142 Immediate context of the current line being parsed 143 144 ***************************************************************************/ 145 146 private struct ParsingContext 147 { 148 /*********************************************************************** 149 150 Current category being parsed 151 152 ***********************************************************************/ 153 154 mstring category; 155 156 157 /*********************************************************************** 158 159 Current key being parsed 160 161 ***********************************************************************/ 162 163 mstring key; 164 165 166 /*********************************************************************** 167 168 Current value being parsed 169 170 ***********************************************************************/ 171 172 mstring value; 173 174 175 /*********************************************************************** 176 177 True if we are at the first multiline value when parsing 178 179 ***********************************************************************/ 180 181 bool multiline_first = true; 182 } 183 184 private ParsingContext context; 185 186 187 /*************************************************************************** 188 189 Structure representing a single value node in the configuration. 190 191 ***************************************************************************/ 192 193 private struct ValueNode 194 { 195 /*********************************************************************** 196 197 The actual value. 198 199 ***********************************************************************/ 200 201 istring value; 202 203 204 /*********************************************************************** 205 206 Flag used to allow a config file to be parsed, even when a different 207 configuration has already been parsed in the past. 208 209 At the start of every new parse, the flags of all value nodes in an 210 already parsed configuration are set to false. If this value node is 211 found during the parse, its flag is set to true. All new value nodes 212 added will also have the flag set to true. At the end of the parse, 213 all value nodes that have the flag set to false are removed. 214 215 **********************************************************************/ 216 217 bool present_in_config; 218 } 219 220 221 /*************************************************************************** 222 223 Config Keys and Properties 224 225 ***************************************************************************/ 226 227 alias istring String; 228 private ValueNode[String][String] properties; 229 230 231 /*************************************************************************** 232 233 Config File Location 234 235 ***************************************************************************/ 236 237 private istring config_file; 238 239 240 /*************************************************************************** 241 242 Constructor 243 244 ***************************************************************************/ 245 246 public this ( ) 247 { } 248 249 250 /*************************************************************************** 251 252 Constructor 253 254 Params: 255 config = path to the configuration file 256 257 ***************************************************************************/ 258 259 public this ( istring config ) 260 { 261 this.parseFile(config); 262 } 263 264 265 /*************************************************************************** 266 267 Returns an iterator over keys or key/value pairs in a category. 268 The values are converted to T, unless T is istring. 269 270 Params: 271 category = category to iterate over 272 273 Returns: 274 an iterator over the keys or key/value pairs in category. 275 276 ***************************************************************************/ 277 278 public VarIterator!(T) iterateCategory ( T = istring ) ( cstring category ) 279 { 280 return VarIterator!(T)(category in this.properties); 281 } 282 283 284 /*************************************************************************** 285 286 Iterator. Iterates over categories of the config file 287 288 ***************************************************************************/ 289 290 public int opApply ( scope int delegate ( ref istring x ) dg ) 291 { 292 int result = 0; 293 294 foreach ( key, val; this.properties ) 295 { 296 result = dg(key); 297 298 if ( result ) break; 299 } 300 301 return result; 302 } 303 304 305 /*************************************************************************** 306 307 Read Config File 308 309 Reads the content of the configuration file and copies to a static 310 array buffer. 311 312 Each property in the ini file belongs to a category. A property always 313 has a key and a value associated with the key. The function parses the 314 following different elements: 315 316 i. Categories 317 [Example Category] 318 319 ii. Comments 320 // comments start with two slashes, 321 ; a semi-colon 322 # or a hash 323 324 iii. Property 325 key = value 326 327 iv. Multi-value property 328 key = value1 329 value2 330 value3 331 332 Params: 333 file_path = string that contains the path to the configuration file 334 clean_old = true if the existing configuration should be overwritten 335 with the result of the current parse, false if the 336 current parse should only add to or update the existing 337 configuration. (defaults to true) 338 339 ***************************************************************************/ 340 341 public void parseFile ( istring file_path = "etc/config.ini", 342 bool clean_old = true ) 343 { 344 this.config_file = file_path; 345 346 auto get_line = new TextFileInput(this.config_file); 347 348 this.parseIter(get_line, clean_old); 349 } 350 351 /// 352 unittest 353 { 354 void main () 355 { 356 scope config = new ConfigParser(); 357 config.parseFile("etc/config.ini"); 358 } 359 } 360 361 362 /*************************************************************************** 363 364 Parse a string 365 366 See parseFile() for details on the parsed syntax. 367 368 Usage Example: 369 370 --- 371 372 Config.parseString( 373 "[section]\n" 374 "key = value1\n" 375 " value2\n" 376 " value3\n" 377 ); 378 379 --- 380 381 Params: 382 T = Type of characters in the string 383 str = string to parse 384 clean_old = true if the existing configuration should be overwritten 385 with the result of the current parse, false if the 386 current parse should only add to or update the existing 387 configuration. (defaults to true) 388 389 ***************************************************************************/ 390 391 public void parseString (T : dchar) ( T[] str, bool clean_old = true ) 392 { 393 static struct Iterator 394 { 395 T[] data; 396 397 int opApply ( scope int delegate ( ref T[] x ) dg ) 398 { 399 int result = 0; 400 401 foreach ( ref line; lines(this.data) ) 402 { 403 result = dg(line); 404 405 if ( result ) break; 406 } 407 408 return result; 409 } 410 } 411 412 this.parseIter(Iterator(str), clean_old); 413 } 414 415 416 /*************************************************************************** 417 418 Tells whether the config object has no values loaded. 419 420 Returns: 421 true if it doesn't have any values, false otherwise 422 423 ***************************************************************************/ 424 425 public bool isEmpty() 426 { 427 return this.properties.length == 0; 428 } 429 430 431 /*************************************************************************** 432 433 Checks if Key exists in Category 434 435 Params: 436 category = category in which to look for the key 437 key = key to be checked 438 439 Returns: 440 true if the configuration key exists in this category 441 442 ***************************************************************************/ 443 444 public bool exists ( cstring category, cstring key ) 445 { 446 return ((category in this.properties) && 447 (key in this.properties[category])); 448 } 449 450 451 /*************************************************************************** 452 453 Strict method to get the value of a config key. If the requested key 454 cannot be found, an exception is thrown. 455 456 Template can be instantiated with integer, float or string (istring) 457 type. 458 459 Usage Example: 460 461 --- 462 463 Config.parseFile("some-config.ini"); 464 // throws if not found 465 auto str = Config.getStrict!(istring)("some-cat", "some-key"); 466 auto n = Config.getStrict!(int)("some-cat", "some-key"); 467 468 --- 469 470 Params: 471 category = category to get key from 472 key = key whose value is to be got 473 474 Throws: 475 if the specified key does not exist 476 477 Returns: 478 value of a configuration key, or null if none 479 480 ***************************************************************************/ 481 482 public T getStrict ( T ) ( cstring category, cstring key ) 483 { 484 enforce!(ConfigException)( 485 exists(category, key), 486 format("Critical Error: No configuration key '{}:{}' found", 487 category, key) 488 ); 489 try 490 { 491 auto value_node = this.properties[category][key]; 492 493 return conv!(T)(value_node.value); 494 } 495 catch ( IllegalArgumentException ) 496 { 497 throw new ConfigException( 498 format("Critical Error: Configuration key '{}:{}' " 499 ~ "appears not to be of type '{}'", 500 category, key, T.stringof)); 501 } 502 503 assert(0); 504 } 505 506 507 /*************************************************************************** 508 509 Alternative form strict config value getter, returning the retrieved 510 value via a reference. (The advantage being that the template type can 511 then be inferred by the compiler.) 512 513 Template can be instantiated with integer, float or string (istring) 514 type. 515 516 Usage Example: 517 518 --- 519 520 Config.parseFile("some-config.ini"); 521 // throws if not found 522 istring str; 523 int n; 524 525 Config.getStrict(str, "some-cat", "some-key"); 526 Config.getStrict(n, "some-cat", "some-key"); 527 528 --- 529 530 Params: 531 value = output for config value 532 category = category to get key from 533 key = key whose value is to be got 534 535 Throws: 536 if the specified key does not exist 537 538 TODO: perhaps we should discuss removing the other version of 539 getStrict(), above? It seems a little bit confusing having both methods, 540 and I feel this version is more convenient to use. 541 542 ***************************************************************************/ 543 544 public void getStrict ( T ) ( ref T value, cstring category, cstring key ) 545 { 546 value = this.getStrict!(T)(category, key); 547 } 548 549 550 /*************************************************************************** 551 552 Non-strict method to get the value of a config key into the specified 553 output value. If the config key does not exist, the given default value 554 is returned. 555 556 Template can be instantiated with integer, float or string (istring) 557 type. 558 559 Usage Example: 560 561 --- 562 563 Config.parseFile("some-config.ini"); 564 auto str = Config.get("some-cat", "some-key", "my_default_value"); 565 int n = Config.get("some-cat", "some-int", 5); 566 567 --- 568 569 Params: 570 category = category to get key from 571 key = key whose value is to be got 572 default_value = default value to use if missing in the config 573 574 Returns: 575 config value, if existing, otherwise default value 576 577 ***************************************************************************/ 578 579 public T get ( T ) ( cstring category, cstring key, T default_value ) 580 { 581 if ( exists(category, key) ) 582 { 583 return getStrict!(T)(category, key); 584 } 585 return default_value; 586 } 587 588 589 /*************************************************************************** 590 591 Alternative form non-strict config value getter, returning the retrieved 592 value via a reference. (For interface consistency with the reference 593 version of getStrict(), above.) 594 595 Template can be instantiated with integer, float or string (istring) 596 type. 597 598 Usage Example: 599 600 --- 601 602 Config.parseFile("some-config.ini"); 603 istring str; 604 int n; 605 606 Config.get(str, "some-cat", "some-key", "default value"); 607 Config.get(n, "some-cat", "some-key", 23); 608 609 --- 610 611 Params: 612 value = output for config value 613 category = category to get key from 614 key = key whose value is to be got 615 default_value = default value to use if missing in the config 616 617 TODO: perhaps we should discuss removing the other version of 618 get(), above? It seems a little bit confusing having both methods, 619 and I feel the reference version is more convenient to use. 620 621 ***************************************************************************/ 622 623 public void get ( T ) ( ref T value, cstring category, 624 cstring key, T default_value ) 625 { 626 value = this.get(category, key, default_value); 627 } 628 629 630 /*************************************************************************** 631 632 Strict method to get a multi-line value. If the requested key cannot be 633 found, an exception is thrown. 634 635 Retrieves the value list of a configuration key with a multi-line value. 636 If the value is a single line, the list has one element. 637 638 Params: 639 category = category to get key from 640 key = key whose value is to be got 641 642 Throws: 643 if the specified key does not exist 644 645 Returns: 646 list of values 647 648 ***************************************************************************/ 649 650 public T[] getListStrict ( T = istring ) ( cstring category, cstring key ) 651 { 652 auto value = this.getStrict!(istring)(category, key); 653 T[] r; 654 foreach ( elem; delimit(value, "\n") ) 655 { 656 r ~= this.conv!(T)(elem); 657 } 658 return r; 659 } 660 661 662 /*************************************************************************** 663 664 Non-strict method to get a multi-line value. The existence or 665 non-existence of the key is returned. If the configuration key cannot be 666 found, the output list remains unchanged. 667 668 If the value is a single line, the output list has one element. 669 670 Params: 671 category = category to get key from 672 key = key whose value is to be got 673 default_value = default list to use if missing in the config 674 675 Returns: 676 the list of values corresponding to the given category + key 677 combination if such a combination exists, the given list of default 678 values otherwise 679 680 ***************************************************************************/ 681 682 public T[] getList ( T = istring ) ( cstring category, cstring key, 683 T[] default_value ) 684 { 685 if ( exists(category, key) ) 686 { 687 return getListStrict!(T)(category, key); 688 } 689 return default_value; 690 } 691 692 693 /*************************************************************************** 694 695 Set Config-Key Property 696 697 Usage Example: 698 699 --- 700 701 Config.parseFile(`etc/config.ini`); 702 703 Config.set(`category`, `key`, `value`); 704 705 --- 706 707 Params: 708 category = category to be set 709 key = key to be set 710 value = value of the property 711 712 ***************************************************************************/ 713 714 public void set ( istring category, istring key, istring value ) 715 { 716 if ( category == "" || key == "" || value == "" ) 717 { 718 return; 719 } 720 721 if ( this.exists(category, key) ) 722 { 723 (this.properties[category][key]).value = value; 724 } 725 else 726 { 727 ValueNode value_node = { value, true }; 728 729 this.properties[category][key] = value_node; 730 } 731 } 732 733 734 /*************************************************************************** 735 736 Remove Config-Key Property 737 738 Usage Example: 739 740 --- 741 742 Config.parseFile(`etc/config.ini`); 743 744 Config.remove(`category`, `key`); 745 746 --- 747 748 Params: 749 category = category from which the property is to be removed 750 key = key of the property to be removed 751 752 ***************************************************************************/ 753 754 public void remove ( istring category, istring key ) 755 { 756 if ( category == "" || key == "" ) 757 { 758 return; 759 } 760 761 if ( this.exists(category, key) ) 762 { 763 (this.properties[category][key]).present_in_config = false; 764 765 this.pruneConfiguration(); 766 } 767 } 768 769 770 /*************************************************************************** 771 772 Prints the current configuration to the given formatted text stream. 773 774 Note that no guarantees can be made about the order of the categories 775 or the order of the key-value pairs within each category. 776 777 Params: 778 output = formatted text stream in which to print the configuration 779 (defaults to Stdout) 780 781 ***************************************************************************/ 782 783 public void print ( FormatOutput output = Stdout ) 784 { 785 foreach ( category, key_value_pairs; this.properties ) 786 { 787 output.formatln("{}", category); 788 789 foreach ( key, value_node; key_value_pairs ) 790 { 791 output.formatln(" {} = {}", key, value_node.value); 792 } 793 } 794 } 795 796 797 /*************************************************************************** 798 799 Actually performs parsing of the lines of a config file or a string. 800 Each line to be parsed is obtained via an iterator. 801 802 Params: 803 I = type of the iterator that will supply lines to be parsed 804 iter = iterator that will supply lines to be parsed 805 clean_old = true if the existing configuration should be overwritten 806 with the result of the current parse, false if the 807 current parse should only add to or update the existing 808 configuration. 809 810 ***************************************************************************/ 811 812 private void parseIter ( I ) ( I iter, bool clean_old ) 813 { 814 this.clearParsingContext(); 815 816 if ( clean_old ) 817 { 818 this.clearAllValueNodeFlags(); 819 } 820 821 foreach ( ref line; iter ) 822 { 823 this.parseLine(line); 824 } 825 826 this.saveFromParsingContext(); 827 828 this.pruneConfiguration(); 829 830 this.clearParsingContext(); 831 } 832 833 834 /*************************************************************************** 835 836 Converts a string to a boolean value. The following string values are 837 accepted: 838 839 false / true, disabled / enabled, off / on, no / yes, 0 / 1 840 841 Params: 842 property = string to extract boolean value from 843 844 Throws: 845 if the string does not match one of the possible boolean strings 846 847 Returns: 848 boolean value interpreted from string 849 850 ***************************************************************************/ 851 852 private static bool toBool ( cstring property ) 853 { 854 static immutable istring[2][] BOOL_IDS = 855 [ 856 ["false", "true"], 857 ["disabled", "enabled"], 858 ["off", "on"], 859 ["no", "yes"], 860 ["0", "1"] 861 ]; 862 863 foreach ( id; BOOL_IDS ) 864 { 865 if ( property == id[0] ) return false; 866 if ( property == id[1] ) return true; 867 } 868 869 throw new IllegalArgumentException( 870 "Config.toBool :: invalid boolean value"); 871 } 872 873 874 /*************************************************************************** 875 876 Converts property to T 877 878 Params: 879 property = value to convert 880 881 Returns: 882 property converted to T 883 884 ***************************************************************************/ 885 886 private static T conv ( T ) ( cstring property ) 887 { 888 static if ( is(T : bool) ) 889 { 890 return toBool(property); 891 } 892 else static if ( is(T : long) ) 893 { 894 auto v = toLong(property); 895 enforce!(IllegalArgumentException)( 896 v >= T.min && v <= T.max, 897 "Value of " ~ cast(istring) property ~ " is out of " ~ T.stringof ~ " bounds"); 898 return cast(T) v; 899 } 900 else static if ( is(T : real) ) 901 { 902 return toFloat(property); 903 } 904 else static if ( is(T U : U[]) && 905 ( is(Unqual!(U) : char) || is(Unqual!(U) : wchar) 906 || is(Unqual!(U) : dchar)) ) 907 { 908 auto r = fromString8!(Unqual!(U))(property, T.init); 909 return cast(T) r.dup; 910 } 911 else 912 { 913 static assert(false, 914 __FILE__ ~ " : get(): type '" ~ T.stringof 915 ~ "' is not supported"); 916 } 917 } 918 919 920 /*************************************************************************** 921 922 Saves the current contents of the context into the configuration. 923 924 ***************************************************************************/ 925 926 private void saveFromParsingContext ( ) 927 { 928 auto ctx = &this.context; 929 930 if ( ctx.category.length == 0 || 931 ctx.key.length == 0 || 932 ctx.value.length == 0 ) 933 { 934 return; 935 } 936 937 if ( this.exists(ctx.category, ctx.key) ) 938 { 939 ValueNode * value_node = &this.properties[ctx.category][ctx.key]; 940 941 if ( value_node.value != ctx.value ) 942 { 943 value_node.value = idup(ctx.value); 944 } 945 946 value_node.present_in_config = true; 947 } 948 else 949 { 950 ValueNode value_node = { ctx.value.dup, true }; 951 952 this.properties[idup(ctx.category)][idup(ctx.key)] = value_node; 953 } 954 955 ctx.value.length = 0; 956 assumeSafeAppend(ctx.value); 957 } 958 959 960 /*************************************************************************** 961 962 Clears the 'present_in_config' flags associated with all value nodes in 963 the configuration. 964 965 ***************************************************************************/ 966 967 private void clearAllValueNodeFlags ( ) 968 { 969 foreach ( category, key_value_pairs; this.properties ) 970 { 971 foreach ( key, ref value_node; key_value_pairs ) 972 { 973 value_node.present_in_config = false; 974 } 975 } 976 } 977 978 979 /*************************************************************************** 980 981 Prunes the configuration removing all keys whose value nodes have the 982 'present_in_config' flag set to false. Also removes all categories that 983 have no keys. 984 985 ***************************************************************************/ 986 987 private void pruneConfiguration ( ) 988 { 989 istring[] keys_to_remove; 990 istring[] categories_to_remove; 991 992 // Remove obsolete keys 993 994 foreach ( category, ref key_value_pairs; this.properties ) 995 { 996 foreach ( key, value_node; key_value_pairs ) 997 { 998 if ( ! value_node.present_in_config ) 999 { 1000 keys_to_remove ~= key; 1001 } 1002 } 1003 1004 foreach ( key; keys_to_remove ) 1005 { 1006 key_value_pairs.remove(key); 1007 } 1008 1009 keys_to_remove.length = 0; 1010 assumeSafeAppend(keys_to_remove); 1011 } 1012 1013 // Remove categories that have no keys 1014 1015 foreach ( category, key_value_pairs; this.properties ) 1016 { 1017 if ( key_value_pairs.length == 0 ) 1018 { 1019 categories_to_remove ~= category; 1020 } 1021 } 1022 1023 foreach ( category; categories_to_remove ) 1024 { 1025 this.properties.remove(category); 1026 } 1027 } 1028 1029 1030 /*************************************************************************** 1031 1032 Clears the current parsing context. 1033 1034 ***************************************************************************/ 1035 1036 private void clearParsingContext ( ) 1037 { 1038 auto ctx = &this.context; 1039 1040 ctx.value.length = 0; 1041 assumeSafeAppend(ctx.value); 1042 ctx.category.length = 0; 1043 assumeSafeAppend(ctx.category); 1044 ctx.key.length = 0; 1045 assumeSafeAppend(ctx.key); 1046 ctx.multiline_first = true; 1047 } 1048 1049 1050 /*************************************************************************** 1051 1052 Parse a line 1053 1054 See parseFile() for details on the parsed syntax. This method only makes 1055 sense to do partial parsing of a string. 1056 1057 Usage Example: 1058 1059 --- 1060 1061 Config.parseLine("[section]"); 1062 Config.parseLine("key = value1\n"); 1063 Config.parseLine(" value2\n"); 1064 Config.parseLine(" value3\n"); 1065 1066 --- 1067 1068 Params: 1069 line = line to parse 1070 1071 ***************************************************************************/ 1072 1073 private void parseLine ( cstring line ) 1074 { 1075 auto ctx = &this.context; 1076 1077 line = trim(line); 1078 1079 if ( line.length == 0 ) 1080 { 1081 // Ignore empty lines. 1082 return; 1083 } 1084 1085 bool slash_comment = line.length >= 2 && line[0 .. 2] == "//"; 1086 bool hash_comment = line[0] == '#'; 1087 bool semicolon_comment = line[0] == ';'; 1088 1089 if ( slash_comment || semicolon_comment || hash_comment ) 1090 { 1091 // Ignore comment lines. 1092 return; 1093 } 1094 1095 auto pos = locate(line, '['); // category present in line? 1096 1097 if ( pos == 0 ) 1098 { 1099 this.saveFromParsingContext(); 1100 1101 auto cat = trim(line[pos + 1 .. locate(line, ']')]); 1102 1103 ctx.category.copy(cat); 1104 1105 ctx.key.length = 0; 1106 assumeSafeAppend(ctx.key); 1107 } 1108 else 1109 { 1110 pos = locate(line, '='); // check for key value pair 1111 1112 if ( pos < line.length ) 1113 { 1114 this.saveFromParsingContext(); 1115 1116 ctx.key.copy(trim(line[0 .. pos])); 1117 1118 ctx.value.copy(trim(line[pos + 1 .. $])); 1119 1120 ctx.multiline_first = !ctx.value.length; 1121 } 1122 else 1123 { 1124 if ( ! ctx.multiline_first ) 1125 { 1126 ctx.value ~= '\n'; 1127 } 1128 1129 ctx.value ~= line; 1130 1131 ctx.multiline_first = false; 1132 } 1133 } 1134 } 1135 } 1136 1137 /// Usage example 1138 unittest 1139 { 1140 void main () 1141 { 1142 // Read config file from disk 1143 scope config = new ConfigParser("etc/config.ini"); 1144 1145 // Read a single value 1146 istring value = config.getStrict!(istring)("category", "key"); 1147 1148 // Set a single value 1149 config.set("category", "key", "new value"); 1150 1151 // Read a multi-line value 1152 istring[] values = config.getListStrict("category", "key"); 1153 } 1154 } 1155 1156 1157 version (unittest) 1158 { 1159 import ocean.core.Test; 1160 } 1161 1162 unittest 1163 { 1164 struct ConfigSanity 1165 { 1166 uint num_categories; 1167 1168 cstring[] categories; 1169 1170 cstring[] keys; 1171 } 1172 1173 void parsedConfigSanityCheck ( ConfigParser config, ConfigSanity expected, 1174 istring test_name ) 1175 { 1176 auto t = new NamedTest(test_name); 1177 cstring[] obtained_categories; 1178 cstring[] obtained_keys; 1179 1180 t.test!("==")(config.isEmpty, (expected.num_categories == 0)); 1181 1182 foreach ( category; config ) 1183 { 1184 obtained_categories ~= category; 1185 1186 foreach ( key; config.iterateCategory(category) ) 1187 { 1188 obtained_keys ~= key; 1189 } 1190 } 1191 1192 t.test!("==")(obtained_categories.length, expected.num_categories); 1193 1194 t.test!("==")(sort(obtained_categories), sort(expected.categories)); 1195 1196 t.test!("==")(sort(obtained_keys), sort(expected.keys)); 1197 } 1198 1199 // Wrapper function that just calls the 'parsedConfigSanityCheck' function, 1200 // but appends the line number to the test name. This is useful when 1201 // slightly different variations of the same basic type of test need to be 1202 // performed. 1203 void parsedConfigSanityCheckN ( ConfigParser config, ConfigSanity expected, 1204 cstring test_name, 1205 typeof(__LINE__) line_num = __LINE__ ) 1206 { 1207 parsedConfigSanityCheck(config, expected, 1208 format("{} (line: {})", test_name, line_num)); 1209 } 1210 1211 scope Config = new ConfigParser(); 1212 1213 /*************************************************************************** 1214 1215 Section 1: unit-tests to confirm correct parsing of config files 1216 1217 ***************************************************************************/ 1218 1219 auto str1 = 1220 ` 1221 [Section1] 1222 multiline = a 1223 # unittest comment 1224 b 1225 ; comment with a different style in multiline 1226 c 1227 // and the ultimative comment 1228 d 1229 int_arr = 30 1230 40 1231 -60 1232 1111111111 1233 0x10 1234 ulong_arr = 0 1235 50 1236 18446744073709551615 1237 0xa123bcd 1238 float_arr = 10.2 1239 -25.3 1240 90 1241 0.000000001 1242 bool_arr = true 1243 false 1244 `.dup; 1245 ConfigSanity str1_expectations = 1246 { 1, 1247 [ "Section1" ], 1248 [ "multiline", "int_arr", "ulong_arr", "float_arr", "bool_arr" ] 1249 }; 1250 1251 Config.parseString(str1); 1252 parsedConfigSanityCheck(Config, str1_expectations, "basic string"); 1253 1254 scope l = Config.getListStrict("Section1", "multiline"); 1255 1256 test!("==")(l.length, 4); 1257 1258 test!("==")(l, ["a", "b", "c", "d"][]); 1259 1260 scope ints = Config.getListStrict!(int)("Section1", "int_arr"); 1261 test!("==")(ints, [30, 40, -60, 1111111111, 0x10][]); 1262 1263 scope ulong_arr = Config.getListStrict!(ulong)("Section1", "ulong_arr"); 1264 ulong[] ulong_array = [0, 50, ulong.max, 0xa123bcd]; 1265 test!("==")(ulong_arr, ulong_array); 1266 1267 scope float_arr = Config.getListStrict!(float)("Section1", "float_arr"); 1268 float[] float_array = [10.2, -25.3, 90, 0.000000001]; 1269 test!("==")(float_arr, float_array); 1270 1271 scope bool_arr = Config.getListStrict!(bool)("Section1", "bool_arr"); 1272 test!("==")(bool_arr, [true, false][]); 1273 1274 try 1275 { 1276 scope w_bool_arr = Config.getListStrict!(bool)("Section1", "int_arr"); 1277 } 1278 catch ( IllegalArgumentException e ) 1279 { 1280 test!("==")(e.message(), "Config.toBool :: invalid boolean value"[]); 1281 } 1282 1283 // Manually set a property (new category). 1284 Config.set("Section2", "set_key", "set_value"[]); 1285 1286 istring new_val; 1287 Config.getStrict(new_val, "Section2", "set_key"); 1288 test!("==")(new_val, "set_value"[]); 1289 1290 // Manually set a property (existing category, new key). 1291 Config.set("Section2", "another_set_key", "another_set_value"[]); 1292 1293 Config.getStrict(new_val, "Section2", "another_set_key"); 1294 test!("==")(new_val, "another_set_value"[]); 1295 1296 // Manually set a property (existing category, existing key). 1297 Config.set("Section2", "set_key", "new_set_value"); 1298 1299 Config.getStrict(new_val, "Section2", "set_key"); 1300 test!("==")(new_val, "new_set_value"[]); 1301 1302 // Check if the 'exists' function works as expected. 1303 test( Config.exists("Section1", "int_arr"), "exists API failure"); 1304 test(!Config.exists("Section420", "int_arr"), "exists API failure"); 1305 test(!Config.exists("Section1", "key420"), "exists API failure"); 1306 1307 ConfigSanity new_str1_expectations = 1308 { 2, 1309 [ "Section1", "Section2" ], 1310 [ "multiline", "int_arr", "ulong_arr", "float_arr", "bool_arr", 1311 "set_key", "another_set_key" ] 1312 }; 1313 parsedConfigSanityCheck(Config, new_str1_expectations, "modified string"); 1314 1315 // Remove properties from the config. 1316 Config.remove("Section2", "set_key"); 1317 Config.remove("Section2", "another_set_key"); 1318 parsedConfigSanityCheck(Config, str1_expectations, "back to basic string"); 1319 1320 // getList tests 1321 scope gl1 = Config.getList("Section1", "dummy", 1322 ["this", "is", "a", "list", "of", "default", "values"]); 1323 test!("==")(gl1.length, 7); 1324 test!("==")(gl1, ["this", "is", "a", "list", "of", "default", "values"][]); 1325 1326 scope gl2 = Config.getList("Section1", "multiline", 1327 ["this", "is", "a", "list", "of", "default", "values"]); 1328 test!("==")(gl2.length, 4); 1329 test!("==")(gl2, ["a", "b", "c", "d"][]); 1330 1331 // Whitespaces handling 1332 1333 istring white_str = 1334 ` 1335 [ Section1 ] 1336 key = val 1337 `; 1338 ConfigSanity white_str_expectations = 1339 { 1, 1340 [ "Section1" ], 1341 [ "key" ] 1342 }; 1343 1344 Config.parseString(white_str); 1345 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1346 1347 white_str = 1348 ` 1349 [Section1 ] 1350 key = val 1351 `; 1352 Config.parseString(white_str); 1353 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1354 1355 white_str = 1356 ` 1357 [ Section1] 1358 key = val 1359 `; 1360 Config.parseString(white_str); 1361 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1362 1363 white_str = 1364 ` 1365 [Section1] 1366 key = val 1367 `; 1368 Config.parseString(white_str); 1369 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1370 1371 white_str = 1372 ` 1373 [Section1] 1374 key = val 1375 `; 1376 Config.parseString(white_str); 1377 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1378 1379 white_str = 1380 ` 1381 [Section1] 1382 key = val 1383 `; 1384 Config.parseString(white_str); 1385 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1386 1387 white_str = 1388 ` 1389 [ Section1 ] 1390 key = val 1391 `; 1392 Config.parseString(white_str); 1393 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1394 1395 // Parse a new configuration 1396 1397 auto str2 = 1398 ` 1399 [German] 1400 one = eins 1401 two = zwei 1402 three = drei 1403 [Hindi] 1404 one = ek 1405 two = do 1406 three = teen 1407 `; 1408 ConfigSanity str2_expectations = 1409 { 2, 1410 [ "German", "Hindi" ], 1411 [ "one", "two", "three", "one", "two", "three" ], 1412 }; 1413 1414 Config.parseString(str2); 1415 parsedConfigSanityCheck(Config, str2_expectations, "new string"); 1416 1417 1418 /*************************************************************************** 1419 1420 Section 2: unit-tests to check memory usage 1421 1422 ***************************************************************************/ 1423 1424 // Test to ensure that an additional parse of the same configuration does 1425 // not allocate at all. 1426 1427 testNoAlloc(Config.parseString(str2)); 1428 1429 // Test to ensure that a few hundred additional parses of the same 1430 // configuration does not allocate at all. 1431 testNoAlloc({ 1432 static immutable num_parses = 200; 1433 for (int i; i < num_parses; i++) 1434 { 1435 Config.parseString(str2); 1436 } 1437 }()); 1438 1439 Config.clearParsingContext(); 1440 }