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 }