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.transition;
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 SliceIfD1StaticArray!(T) get ( T ) ( cstring category, cstring key,
580             T default_value )
581     {
582         if ( exists(category, key) )
583         {
584             return getStrict!(SliceIfD1StaticArray!(T))(category, key);
585         }
586         return default_value;
587     }
588 
589 
590     /***************************************************************************
591 
592         Alternative form non-strict config value getter, returning the retrieved
593         value via a reference. (For interface consistency with the reference
594         version of getStrict(), above.)
595 
596         Template can be instantiated with integer, float or string (istring)
597         type.
598 
599         Usage Example:
600 
601         ---
602 
603             Config.parseFile("some-config.ini");
604             istring str;
605             int n;
606 
607             Config.get(str, "some-cat", "some-key", "default value");
608             Config.get(n, "some-cat", "some-key", 23);
609 
610         ---
611 
612         Params:
613             value = output for config value
614             category = category to get key from
615             key = key whose value is to be got
616             default_value = default value to use if missing in the config
617 
618         TODO: perhaps we should discuss removing the other version of
619         get(), above? It seems a little bit confusing having both methods,
620         and I feel the reference version is more convenient to use.
621 
622     ***************************************************************************/
623 
624     public void get ( T ) ( ref T value, cstring category,
625         cstring key, T default_value )
626     {
627         value = this.get(category, key, default_value);
628     }
629 
630 
631     /***************************************************************************
632 
633         Strict method to get a multi-line value. If the requested key cannot be
634         found, an exception is thrown.
635 
636         Retrieves the value list of a configuration key with a multi-line value.
637         If the value is a single line, the list has one element.
638 
639         Params:
640             category = category to get key from
641             key = key whose value is to be got
642 
643         Throws:
644             if the specified key does not exist
645 
646         Returns:
647             list of values
648 
649     ***************************************************************************/
650 
651     public T[] getListStrict ( T = istring ) ( cstring category, cstring key )
652     {
653         auto value = this.getStrict!(istring)(category, key);
654         T[] r;
655         foreach ( elem; delimit(value, "\n") )
656         {
657             r ~= this.conv!(T)(elem);
658         }
659         return r;
660     }
661 
662 
663     /***************************************************************************
664 
665         Non-strict method to get a multi-line value. The existence or
666         non-existence of the key is returned. If the configuration key cannot be
667         found, the output list remains unchanged.
668 
669         If the value is a single line, the output list has one element.
670 
671         Params:
672             category = category to get key from
673             key = key whose value is to be got
674             default_value = default list to use if missing in the config
675 
676         Returns:
677             the list of values corresponding to the given category + key
678             combination if such a combination exists, the given list of default
679             values otherwise
680 
681     ***************************************************************************/
682 
683     public T[] getList ( T = istring ) ( cstring category, cstring key,
684             T[] default_value )
685     {
686         if ( exists(category, key) )
687         {
688             return getListStrict!(T)(category, key);
689         }
690         return default_value;
691     }
692 
693 
694     /***************************************************************************
695 
696         Set Config-Key Property
697 
698         Usage Example:
699 
700         ---
701 
702             Config.parseFile(`etc/config.ini`);
703 
704             Config.set(`category`, `key`, `value`);
705 
706         ---
707 
708         Params:
709             category = category to be set
710             key      = key to be set
711             value    = value of the property
712 
713     ***************************************************************************/
714 
715     public void set ( istring category, istring key, istring value )
716     {
717         if ( category == "" || key == "" || value == "" )
718         {
719             return;
720         }
721 
722         if ( this.exists(category, key) )
723         {
724             (this.properties[category][key]).value = value;
725         }
726         else
727         {
728             ValueNode value_node = { value, true };
729 
730             this.properties[category][key] = value_node;
731         }
732     }
733 
734 
735     /***************************************************************************
736 
737         Remove Config-Key Property
738 
739         Usage Example:
740 
741         ---
742 
743             Config.parseFile(`etc/config.ini`);
744 
745             Config.remove(`category`, `key`);
746 
747         ---
748 
749         Params:
750             category = category from which the property is to be removed
751             key      = key of the property to be removed
752 
753     ***************************************************************************/
754 
755     public void remove ( istring category, istring key )
756     {
757         if ( category == "" || key == "" )
758         {
759             return;
760         }
761 
762         if ( this.exists(category, key) )
763         {
764             (this.properties[category][key]).present_in_config = false;
765 
766             this.pruneConfiguration();
767         }
768     }
769 
770 
771     /***************************************************************************
772 
773          Prints the current configuration to the given formatted text stream.
774 
775          Note that no guarantees can be made about the order of the categories
776          or the order of the key-value pairs within each category.
777 
778          Params:
779              output = formatted text stream in which to print the configuration
780                       (defaults to Stdout)
781 
782     ***************************************************************************/
783 
784     public void print ( FormatOutput output = Stdout )
785     {
786         foreach ( category, key_value_pairs; this.properties )
787         {
788             output.formatln("{}", category);
789 
790             foreach ( key, value_node; key_value_pairs )
791             {
792                 output.formatln("    {} = {}", key, value_node.value);
793             }
794         }
795     }
796 
797 
798     /***************************************************************************
799 
800         Actually performs parsing of the lines of a config file or a string.
801         Each line to be parsed is obtained via an iterator.
802 
803         Params:
804             I = type of the iterator that will supply lines to be parsed
805             iter = iterator that will supply lines to be parsed
806             clean_old = true if the existing configuration should be overwritten
807                         with the result of the current parse, false if the
808                         current parse should only add to or update the existing
809                         configuration.
810 
811     ***************************************************************************/
812 
813     private void parseIter ( I ) ( I iter, bool clean_old )
814     {
815         this.clearParsingContext();
816 
817         if ( clean_old )
818         {
819             this.clearAllValueNodeFlags();
820         }
821 
822         foreach ( ref line; iter )
823         {
824             this.parseLine(line);
825         }
826 
827         this.saveFromParsingContext();
828 
829         this.pruneConfiguration();
830 
831         this.clearParsingContext();
832     }
833 
834 
835     /***************************************************************************
836 
837         Converts a string to a boolean value. The following string values are
838         accepted:
839 
840             false / true, disabled / enabled, off / on, no / yes, 0 / 1
841 
842         Params:
843             property = string to extract boolean value from
844 
845         Throws:
846             if the string does not match one of the possible boolean strings
847 
848         Returns:
849             boolean value interpreted from string
850 
851     ***************************************************************************/
852 
853     private static bool toBool ( cstring property )
854     {
855         static immutable istring[2][] BOOL_IDS =
856         [
857            ["false",    "true"],
858            ["disabled", "enabled"],
859            ["off",      "on"],
860            ["no",       "yes"],
861            ["0",        "1"]
862         ];
863 
864         foreach ( id; BOOL_IDS )
865         {
866             if ( property == id[0] ) return false;
867             if ( property == id[1] ) return true;
868         }
869 
870         throw new IllegalArgumentException(
871                                       "Config.toBool :: invalid boolean value");
872     }
873 
874 
875     /***************************************************************************
876 
877         Converts property to T
878 
879         Params:
880             property = value to convert
881 
882         Returns:
883             property converted to T
884 
885     ***************************************************************************/
886 
887     private static T conv ( T ) ( cstring property )
888     {
889         static if ( is(T : bool) )
890         {
891             return toBool(property);
892         }
893         else static if ( is(T : long) )
894         {
895             auto v = toLong(property);
896             enforce!(IllegalArgumentException)(
897                 v >= T.min && v <= T.max,
898                 "Value of " ~ cast(istring) property ~ " is out of " ~ T.stringof ~ " bounds");
899             return cast(T) v;
900         }
901         else static if ( is(T : real) )
902         {
903             return toFloat(property);
904         }
905         else static if ( is(T U : U[]) &&
906                          ( is(Unqual!(U) : char) || is(Unqual!(U) : wchar)
907                            || is(Unqual!(U) : dchar)) )
908         {
909             auto r = fromString8!(Unqual!(U))(property, T.init);
910             return cast(T) r.dup;
911         }
912         else
913         {
914             static assert(false,
915                           __FILE__ ~ " : get(): type '" ~ T.stringof
916                           ~ "' is not supported");
917         }
918     }
919 
920 
921     /***************************************************************************
922 
923         Saves the current contents of the context into the configuration.
924 
925     ***************************************************************************/
926 
927     private void saveFromParsingContext ( )
928     {
929         auto ctx = &this.context;
930 
931         if ( ctx.category.length == 0 ||
932              ctx.key.length == 0 ||
933              ctx.value.length == 0 )
934         {
935             return;
936         }
937 
938         if ( this.exists(ctx.category, ctx.key) )
939         {
940             ValueNode * value_node = &this.properties[ctx.category][ctx.key];
941 
942             if ( value_node.value != ctx.value )
943             {
944                 value_node.value = idup(ctx.value);
945             }
946 
947             value_node.present_in_config = true;
948         }
949         else
950         {
951             ValueNode value_node = { ctx.value.dup, true };
952 
953             this.properties[idup(ctx.category)][idup(ctx.key)] = value_node;
954         }
955 
956         ctx.value.length = 0;
957         enableStomping(ctx.value);
958     }
959 
960 
961     /***************************************************************************
962 
963         Clears the 'present_in_config' flags associated with all value nodes in
964         the configuration.
965 
966     ***************************************************************************/
967 
968     private void clearAllValueNodeFlags ( )
969     {
970         foreach ( category, key_value_pairs; this.properties )
971         {
972             foreach ( key, ref value_node; key_value_pairs )
973             {
974                 value_node.present_in_config = false;
975             }
976         }
977     }
978 
979 
980     /***************************************************************************
981 
982         Prunes the configuration removing all keys whose value nodes have the
983         'present_in_config' flag set to false. Also removes all categories that
984         have no keys.
985 
986     ***************************************************************************/
987 
988     private void pruneConfiguration ( )
989     {
990         istring[] keys_to_remove;
991         istring[] categories_to_remove;
992 
993         // Remove obsolete keys
994 
995         foreach ( category, ref key_value_pairs; this.properties )
996         {
997             foreach ( key, value_node; key_value_pairs )
998             {
999                 if ( ! value_node.present_in_config )
1000                 {
1001                     keys_to_remove ~= key;
1002                 }
1003             }
1004 
1005             foreach ( key; keys_to_remove )
1006             {
1007                 key_value_pairs.remove(key);
1008             }
1009 
1010             keys_to_remove.length = 0;
1011             enableStomping(keys_to_remove);
1012         }
1013 
1014         // Remove categories that have no keys
1015 
1016         foreach ( category, key_value_pairs; this.properties )
1017         {
1018             if ( key_value_pairs.length == 0 )
1019             {
1020                 categories_to_remove ~= category;
1021             }
1022         }
1023 
1024         foreach ( category; categories_to_remove )
1025         {
1026             this.properties.remove(category);
1027         }
1028     }
1029 
1030 
1031     /***************************************************************************
1032 
1033         Clears the current parsing context.
1034 
1035     ***************************************************************************/
1036 
1037     private void clearParsingContext ( )
1038     {
1039         auto ctx = &this.context;
1040 
1041         ctx.value.length    = 0;
1042         enableStomping(ctx.value);
1043         ctx.category.length = 0;
1044         enableStomping(ctx.category);
1045         ctx.key.length      = 0;
1046         enableStomping(ctx.key);
1047         ctx.multiline_first = true;
1048     }
1049 
1050 
1051     /***************************************************************************
1052 
1053         Parse a line
1054 
1055         See parseFile() for details on the parsed syntax. This method only makes
1056         sense to do partial parsing of a string.
1057 
1058         Usage Example:
1059 
1060         ---
1061 
1062             Config.parseLine("[section]");
1063             Config.parseLine("key = value1\n");
1064             Config.parseLine("      value2\n");
1065             Config.parseLine("      value3\n");
1066 
1067         ---
1068 
1069         Params:
1070             line = line to parse
1071 
1072     ***************************************************************************/
1073 
1074     private void parseLine ( cstring line )
1075     {
1076         auto ctx = &this.context;
1077 
1078         line = trim(line);
1079 
1080         if ( line.length == 0 )
1081         {
1082             // Ignore empty lines.
1083             return;
1084         }
1085 
1086         bool slash_comment = line.length >= 2 && line[0 .. 2] == "//";
1087         bool hash_comment = line[0] == '#';
1088         bool semicolon_comment = line[0] == ';';
1089 
1090         if ( slash_comment || semicolon_comment || hash_comment )
1091         {
1092             // Ignore comment lines.
1093             return;
1094         }
1095 
1096         auto pos = locate(line, '['); // category present in line?
1097 
1098         if ( pos == 0 )
1099         {
1100             this.saveFromParsingContext();
1101 
1102             auto cat = trim(line[pos + 1 .. locate(line, ']')]);
1103 
1104             ctx.category.copy(cat);
1105 
1106             ctx.key.length = 0;
1107             enableStomping(ctx.key);
1108         }
1109         else
1110         {
1111             pos = locate(line, '='); // check for key value pair
1112 
1113             if ( pos < line.length )
1114             {
1115                 this.saveFromParsingContext();
1116 
1117                 ctx.key.copy(trim(line[0 .. pos]));
1118 
1119                 ctx.value.copy(trim(line[pos + 1 .. $]));
1120 
1121                 ctx.multiline_first = !ctx.value.length;
1122             }
1123             else
1124             {
1125                 if ( ! ctx.multiline_first )
1126                 {
1127                     ctx.value ~= '\n';
1128                 }
1129 
1130                 ctx.value ~= line;
1131 
1132                 ctx.multiline_first = false;
1133             }
1134         }
1135     }
1136 }
1137 
1138 /// Usage example
1139 unittest
1140 {
1141     void main ()
1142     {
1143         // Read config file from disk
1144         scope config = new ConfigParser("etc/config.ini");
1145 
1146         // Read a single value
1147         istring value = config.getStrict!(istring)("category", "key");
1148 
1149         // Set a single value
1150         config.set("category", "key", "new value");
1151 
1152         // Read a multi-line value
1153         istring[] values = config.getListStrict("category", "key");
1154     }
1155 }
1156 
1157 
1158 version ( UnitTest )
1159 {
1160     import ocean.core.Test;
1161 }
1162 
1163 unittest
1164 {
1165     struct ConfigSanity
1166     {
1167         uint num_categories;
1168 
1169         cstring[] categories;
1170 
1171         cstring[] keys;
1172     }
1173 
1174     void parsedConfigSanityCheck ( ConfigParser config, ConfigSanity expected,
1175                                    istring test_name )
1176     {
1177         auto t = new NamedTest(test_name);
1178         cstring[] obtained_categories;
1179         cstring[] obtained_keys;
1180 
1181         t.test!("==")(config.isEmpty, (expected.num_categories == 0));
1182 
1183         foreach ( category; config )
1184         {
1185             obtained_categories ~= category;
1186 
1187             foreach ( key; config.iterateCategory(category) )
1188             {
1189                 obtained_keys ~= key;
1190             }
1191         }
1192 
1193         t.test!("==")(obtained_categories.length, expected.num_categories);
1194 
1195         t.test!("==")(sort(obtained_categories), sort(expected.categories));
1196 
1197         t.test!("==")(sort(obtained_keys), sort(expected.keys));
1198     }
1199 
1200     // Wrapper function that just calls the 'parsedConfigSanityCheck' function,
1201     // but appends the line number to the test name. This is useful when
1202     // slightly different variations of the same basic type of test need to be
1203     // performed.
1204     void parsedConfigSanityCheckN ( ConfigParser config, ConfigSanity expected,
1205                                     cstring test_name,
1206                                     typeof(__LINE__) line_num = __LINE__ )
1207     {
1208         parsedConfigSanityCheck(config, expected,
1209                                 format("{} (line: {})", test_name, line_num));
1210     }
1211 
1212     scope Config = new ConfigParser();
1213 
1214     /***************************************************************************
1215 
1216         Section 1: unit-tests to confirm correct parsing of config files
1217 
1218     ***************************************************************************/
1219 
1220     auto str1 =
1221 `
1222 [Section1]
1223 multiline = a
1224 # unittest comment
1225 b
1226 ; comment with a different style in multiline
1227 c
1228 // and the ultimative comment
1229 d
1230 int_arr = 30
1231       40
1232       -60
1233       1111111111
1234       0x10
1235 ulong_arr = 0
1236         50
1237         18446744073709551615
1238         0xa123bcd
1239 float_arr = 10.2
1240         -25.3
1241         90
1242         0.000000001
1243 bool_arr = true
1244        false
1245 `.dup;
1246     ConfigSanity str1_expectations =
1247         { 1,
1248           [ "Section1" ],
1249           [ "multiline", "int_arr", "ulong_arr", "float_arr", "bool_arr" ]
1250         };
1251 
1252     Config.parseString(str1);
1253     parsedConfigSanityCheck(Config, str1_expectations, "basic string");
1254 
1255     scope l = Config.getListStrict("Section1", "multiline");
1256 
1257     test!("==")(l.length, 4);
1258 
1259     test!("==")(l, ["a", "b", "c", "d"][]);
1260 
1261     scope ints = Config.getListStrict!(int)("Section1", "int_arr");
1262     test!("==")(ints, [30, 40, -60, 1111111111, 0x10][]);
1263 
1264     scope ulong_arr = Config.getListStrict!(ulong)("Section1", "ulong_arr");
1265     ulong[] ulong_array = [0, 50, ulong.max, 0xa123bcd];
1266     test!("==")(ulong_arr, ulong_array);
1267 
1268     scope float_arr = Config.getListStrict!(float)("Section1", "float_arr");
1269     float[] float_array = [10.2, -25.3, 90, 0.000000001];
1270     test!("==")(float_arr, float_array);
1271 
1272     scope bool_arr = Config.getListStrict!(bool)("Section1", "bool_arr");
1273     test!("==")(bool_arr, [true, false][]);
1274 
1275     try
1276     {
1277         scope w_bool_arr = Config.getListStrict!(bool)("Section1", "int_arr");
1278     }
1279     catch ( IllegalArgumentException e )
1280     {
1281         test!("==")(e.message(), "Config.toBool :: invalid boolean value"[]);
1282     }
1283 
1284     // Manually set a property (new category).
1285     Config.set("Section2", "set_key", "set_value"[]);
1286 
1287     istring new_val;
1288     Config.getStrict(new_val, "Section2", "set_key");
1289     test!("==")(new_val, "set_value"[]);
1290 
1291     // Manually set a property (existing category, new key).
1292     Config.set("Section2", "another_set_key", "another_set_value"[]);
1293 
1294     Config.getStrict(new_val, "Section2", "another_set_key");
1295     test!("==")(new_val, "another_set_value"[]);
1296 
1297     // Manually set a property (existing category, existing key).
1298     Config.set("Section2", "set_key", "new_set_value");
1299 
1300     Config.getStrict(new_val, "Section2", "set_key");
1301     test!("==")(new_val, "new_set_value"[]);
1302 
1303     // Check if the 'exists' function works as expected.
1304     test( Config.exists("Section1", "int_arr"), "exists API failure");
1305     test(!Config.exists("Section420", "int_arr"), "exists API failure");
1306     test(!Config.exists("Section1", "key420"), "exists API failure");
1307 
1308     ConfigSanity new_str1_expectations =
1309         { 2,
1310           [ "Section1", "Section2" ],
1311           [ "multiline", "int_arr", "ulong_arr", "float_arr", "bool_arr",
1312             "set_key", "another_set_key" ]
1313         };
1314     parsedConfigSanityCheck(Config, new_str1_expectations, "modified string");
1315 
1316     // Remove properties from the config.
1317     Config.remove("Section2", "set_key");
1318     Config.remove("Section2", "another_set_key");
1319     parsedConfigSanityCheck(Config, str1_expectations, "back to basic string");
1320 
1321     // getList tests
1322     scope gl1 = Config.getList("Section1", "dummy",
1323                         ["this", "is", "a", "list", "of", "default", "values"]);
1324     test!("==")(gl1.length, 7);
1325     test!("==")(gl1, ["this", "is", "a", "list", "of", "default", "values"][]);
1326 
1327     scope gl2 = Config.getList("Section1", "multiline",
1328                         ["this", "is", "a", "list", "of", "default", "values"]);
1329     test!("==")(gl2.length, 4);
1330     test!("==")(gl2, ["a", "b", "c", "d"][]);
1331 
1332     // Whitespaces handling
1333 
1334     istring white_str =
1335 `
1336 [ Section1 ]
1337 key = val
1338 `;
1339     ConfigSanity white_str_expectations =
1340         { 1,
1341           [ "Section1" ],
1342           [ "key" ]
1343         };
1344 
1345     Config.parseString(white_str);
1346     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1347 
1348     white_str =
1349 `
1350 [Section1 ]
1351 key = val
1352 `;
1353     Config.parseString(white_str);
1354     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1355 
1356     white_str =
1357 `
1358 [	       Section1]
1359 key = val
1360 `;
1361     Config.parseString(white_str);
1362     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1363 
1364     white_str =
1365 `
1366 [Section1]
1367 key =		   val
1368 `;
1369     Config.parseString(white_str);
1370     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1371 
1372     white_str =
1373 `
1374 [Section1]
1375 key	     = val
1376 `;
1377     Config.parseString(white_str);
1378     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1379 
1380     white_str =
1381 `
1382 [Section1]
1383 	  key	     = val
1384 `;
1385     Config.parseString(white_str);
1386     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1387 
1388     white_str =
1389 `
1390 [	       Section1   ]
1391 	  key	     =		       val
1392 `;
1393     Config.parseString(white_str);
1394     parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces");
1395 
1396     // Parse a new configuration
1397 
1398     auto str2 =
1399 `
1400 [German]
1401 one = eins
1402 two = zwei
1403 three = drei
1404 [Hindi]
1405 one = ek
1406 two = do
1407 three = teen
1408 `;
1409     ConfigSanity str2_expectations =
1410         { 2,
1411           [ "German", "Hindi" ],
1412           [ "one", "two", "three", "one", "two", "three" ],
1413         };
1414 
1415     Config.parseString(str2);
1416     parsedConfigSanityCheck(Config, str2_expectations, "new string");
1417 
1418 
1419     /***************************************************************************
1420 
1421         Section 2: unit-tests to check memory usage
1422 
1423     ***************************************************************************/
1424 
1425     // Test to ensure that an additional parse of the same configuration does
1426     // not allocate at all.
1427 
1428     testNoAlloc(Config.parseString(str2));
1429 
1430     // Test to ensure that a few hundred additional parses of the same
1431     // configuration does not allocate at all.
1432     testNoAlloc({
1433             static immutable num_parses = 200;
1434             for (int i; i < num_parses; i++)
1435             {
1436                 Config.parseString(str2);
1437             }
1438         }());
1439 
1440     Config.clearParsingContext();
1441 }