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 }