1 /**
2  * Copyright 2016 Stefan Brus
3  *
4  * inid config parser module
5  *
6  * Usage example:
7  *
8  * enum CONFIG_STR = `[Category]
9  * num_val = 42
10  * str_val = foo`;
11  *
12  * struct Config
13  * {
14  *     struct Category
15  *     {
16  *         uint num_val;
17  *         string str_val;
18  *     }
19  *
20  *     Category category;
21  * }
22  *
23  * auto config = ConfigParser!Config(CONFIG_STR);
24  *
25  * assert(config.category.num_val == 42);
26  * assert(config.category.str_val == "foo");
27  */
28 
29 module inid.parser;
30 
31 /**
32  * Helper function to parse a config.ini file
33  *
34  * Template_params:
35  *      Config = The type of the config struct
36  *
37  * Params:
38  *      path = The path to the config.ini file
39  *
40  * Returns:
41  *      The config parser
42  *
43  * Throws:
44  *      ConfigException on parse error
45  */
46 
47 ConfigParser!Config parseConfigFile ( Config ) ( string path )
48 {
49     import std.file;
50 
51     return ConfigParser!Config(readText(path));
52 }
53 
54 /**
55  * Exception thrown during config parser errors
56  */
57 
58 class ConfigException : Exception
59 {
60     /**
61      * Constructor
62      *
63      * Params:
64      *      msg = The message
65      *      file = The file
66      *      line = The line
67      */
68 
69     this ( string msg, string file = __FILE__, uint line = __LINE__ )
70     {
71         super(msg, file, line);
72     }
73 }
74 
75 /**
76  * Config parser struct
77  *
78  * Template_params:
79  *      Config = The type of the struct to try to parse
80  */
81 
82 struct ConfigParser ( Config )
83 {
84     static assert(is(Config == struct), "ConfigParser type argument must be a struct");
85 
86     /**
87      * The config struct
88      */
89 
90     Config config;
91 
92     alias config this;
93 
94     /**
95      * Constructor
96      *
97      * Params:
98      *      str = The config string
99      */
100 
101     this ( string str )
102     {
103         this.parse(str);
104     }
105 
106     /**
107      * Parse a config string
108      *
109      * Params:
110      *      str = The config string
111      *
112      * Throws:
113      *      ConfigException on parse error
114      */
115 
116     void parse ( string str )
117     {
118         import std.algorithm;
119         import std.array;
120         import std.exception;
121         import std.format;
122         import std.string;
123         import std.traits;
124 
125         // Reset the config struct
126         this.config = Config.init;
127 
128         // Field name tuple of the config struct
129         alias CategoryNames = FieldNameTuple!Config;
130 
131         // Split the string into lines, strip whitespace, remove empty strings, remove comments
132         auto stripped_lines = str.split('\n').map!(strip).array().remove!(a => a.length == 0).remove!(a => a[0] == ';');
133 
134         // The current [CATEGORY] index
135         size_t cat_idx;
136 
137         foreach ( idx, ref field; this.config.tupleof )
138         {
139             static assert(is(typeof(field) == struct), "ConfigParser fields must be structs");
140 
141             // Enforce that we have not reached the end while there are still categories to parse
142             enforce!ConfigException(cat_idx < stripped_lines.length, format("Expected category %s", CategoryNames[idx].toUpper()));
143 
144             // Attempt to parse a [CATEGORY] name
145             auto cat_line = stripped_lines[cat_idx];
146 
147             // Enforce that the current line is a category
148             assert(cat_line.length > 0);
149             enforce!ConfigException(cat_line[0] == '[' && cat_line[$ - 1] == ']', format("Expected a category, got: %s", cat_line));
150 
151             // Strip the whitespace from inside the brackets
152             assert(cat_line.length > 1);
153             auto cat_name = cat_line[1 .. $ - 1].strip();
154 
155             // Enforce that the category name is the same as the struct type name
156             enforce!ConfigException(cat_name.toLower() == typeof(field).stringof.toLower(), format("Expected category %s", typeof(field).stringof));
157 
158             // Find the index of the next category
159             size_t next_cat_idx;
160             for ( auto i = cat_idx + 1; i < stripped_lines.length; i++ )
161             {
162                 assert(stripped_lines[i].length > 0);
163 
164                 if ( stripped_lines[i][0] == '[' )
165                 {
166                     next_cat_idx = i;
167                     break;
168                 }
169             }
170 
171             // If no category was found, set the next index to the end of the config string
172             if ( next_cat_idx == 0 )
173             {
174                 next_cat_idx = stripped_lines.length;
175             }
176 
177             // Enforce that there is at least one line between this category and the next
178             enforce!ConfigException(next_cat_idx > cat_idx + 1, format("Category %s is empty", cat_name.toUpper()));
179 
180             // Parse the category into a struct
181             field = this.parseStruct!(typeof(field))(stripped_lines[cat_idx + 1 .. next_cat_idx]);
182 
183             // Update the category index
184             cat_idx = next_cat_idx;
185         }
186     }
187 
188     alias opCall = parse;
189 
190     /**
191      * Static parse function
192      *
193      * Creates a config parser, attempts to parse, and returns the result
194      *
195      * Params:
196      *      str = The config string
197      *
198      * Returns:
199      *      The config struct
200      *
201      * Throws:
202      *      ConfigException on parse error
203      */
204 
205     static Config parseResult ( string str )
206     {
207         auto parser = ConfigParser(str);
208         return parser;
209     }
210 
211     static alias opCall = parseResult;
212 
213     /**
214      * Helper function to parse a struct from a list of lines
215      *
216      * Template_params:
217      *      T = The struct type
218      *
219      * Params:
220      *      lines = The lines
221      *
222      * Returns:
223      *      The parsed struct
224      *
225      * Throws:
226      *      ConfigException on parse error
227      */
228 
229     private T parseStruct ( T ) ( string[] lines )
230     {
231         import std.array;
232         import std.conv;
233         import std.exception;
234         import std.format;
235         import std.string;
236         import std.traits;
237 
238         // Field name tuple of the struct to parse
239         alias FieldNames = FieldNameTuple!T;
240 
241         // The category name
242         auto category = T.stringof.toUpper();
243 
244         // Enforce that the category has the expected number of lines
245         enforce!ConfigException(T.tupleof.length == lines.length, format("[%s] Expected %d fields", category, T.tupleof.length));
246 
247         // Build an associative array of the config key value pairs
248         string[string] field_map;
249 
250         // Parse the lines as key value pairs of the "key = value" format
251         foreach ( line; lines )
252         {
253             auto kv = line.split('=');
254 
255             // Enforce that the line contains one '='
256             enforce!ConfigException(kv.length == 2, format("[%s] Fields must be \"key = value\" pairs", category));
257 
258             auto key = kv[0].strip();
259             auto val = kv[1].strip();
260 
261             // Enforce that the entry has both a key and a value
262             enforce!ConfigException(key.length > 0 && val.length > 0, format("[%s] Fields must be \"key = value\" pairs", category));
263 
264             field_map[key.toLower()] = val;
265         }
266 
267         // Build the result struct based on the associative array
268         T result;
269         foreach ( i, ref field; result.tupleof )
270         {
271             auto field_name = FieldNames[i].toLower();
272 
273             // Enforce that the field is configured
274             enforce!ConfigException(field_name in field_map, format("[%s] Expected field: %s", category, field_name));
275 
276             // Attempt to convert the value to the appropriate type
277             try
278             {
279                 field = to!(typeof(field))(field_map[field_name]);
280             }
281             catch ( Exception e )
282             {
283                 throw new ConfigException(format("[%s] Field %s must be of type %s", category, field_name, typeof(field).stringof));
284             }
285         }
286 
287         return result;
288     }
289 }
290 
291 /**
292  * Simple test case with one category
293  */
294 
295 unittest
296 {
297     struct Config
298     {
299         struct Entry
300         {
301             ulong key;
302             string value;
303         }
304 
305         Entry entry;
306     }
307 
308     enum CONFIG_STR = `
309 ; The first test config
310   [ ENTRY ]
311   value = the value
312   key = 1234567891011
313 `;
314 
315     auto parser = ConfigParser!Config(CONFIG_STR);
316     assert(parser.entry.key == 1234567891011);
317     assert(parser.entry.value == "the value");
318 }
319 
320 /**
321  * Test case for multiple categories
322  */
323 
324 unittest
325 {
326     struct Config
327     {
328         struct Server
329         {
330             string address;
331             ushort port;
332         }
333 
334         Server server;
335 
336         struct Route
337         {
338             string url;
339             string path;
340             uint response_code;
341         }
342 
343         Route route;
344     }
345 
346     enum CONFIG_STR = `
347 
348 ;
349 ; Server configuration
350 ;
351 
352 [ Server ]
353 
354   ADDRESS       = 127.0.0.1
355 
356   PORT          = 32768
357 
358 ;
359 ; Index route
360 ;
361 
362 [ Route ]
363 
364   URL           = /index.html
365 
366   PATH          = public/index.html
367 
368   RESPONSE_CODE = 200
369 
370 `;
371 
372     auto parser = ConfigParser!Config(CONFIG_STR);
373     assert(parser.server.address == "127.0.0.1");
374     assert(parser.server.port == 32768);
375     assert(parser.route.url == "/index.html");
376     assert(parser.route.path == "public/index.html");
377     assert(parser.route.response_code == 200);
378 }
379 
380 /**
381  * Test case for different value types
382  */
383 
384 unittest
385 {
386     struct Config
387     {
388         struct MixedValues
389         {
390             uint integer;
391             double decimal;
392             bool flag;
393             string text;
394         }
395 
396         MixedValues mixed_values;
397     }
398 
399     enum CONFIG_STR = `
400 [MixedValues]
401 integer = 42
402 decimal = 66.6
403 flag = true
404 text = This is some text
405 `;
406 
407     auto parser = ConfigParser!Config(CONFIG_STR);
408     assert(parser.mixed_values.integer == 42);
409     assert(parser.mixed_values.decimal == 66.6);
410     assert(parser.mixed_values.flag == true);
411     assert(parser.mixed_values.text == "This is some text");
412 }
413 
414 /**
415  * Error test cases
416  */
417 
418 unittest
419 {
420     import std.exception;
421 
422     /**
423      * Too few categories
424      */
425 
426     struct Config
427     {
428         struct CatOne
429         {
430             uint x;
431         }
432 
433         CatOne one;
434 
435         struct CatTwo
436         {
437             uint y;
438             uint z;
439         }
440 
441         CatTwo two;
442     }
443 
444     enum CONFIG_STR_TOO_FEW_CATS = `
445 [CatOne]
446 x = 1
447 `;
448 
449     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_TOO_FEW_CATS));
450 
451     /**
452      * Expected category
453      */
454 
455     enum CONFIG_STR_CAT_EXPECTED = `
456 x = 1
457 [CatOne]
458 x = 1
459 
460 [CatTwo]
461 y = 2
462 z = 3
463 `;
464 
465     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_CAT_EXPECTED));
466 
467     /**
468      * Wrong category name
469      */
470 
471     enum CONFIG_STR_WRONG_CAT = `
472 [CatOne]
473 x = 1
474 [CatTwoooo]
475 y = 2
476 z = 3
477 `;
478 
479     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_WRONG_CAT));
480 
481     /**
482      * Empty category
483      */
484 
485     enum CONFIG_STR_EMPTY_CAT = `
486 [CatOne]
487 [CatTwo]
488 y = 2
489 z = 3
490 `;
491 
492     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_EMPTY_CAT));
493 
494     /**
495      * Too many fields
496      */
497 
498     enum CONFIG_STR_TOO_MANY_FIELDS = `
499 [CatOne]
500 x = 1
501 y = 2
502 [CatTwo]
503 y = 2
504 z = 3
505 `;
506 
507     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_TOO_MANY_FIELDS));
508 
509     /**
510      * Too few fields
511      */
512 
513     enum CONFIG_STR_TOO_FEW_FIELDS = `
514 [CatOne]
515 x = 1
516 [CatTwo]
517 y = 2
518 `;
519 
520     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_TOO_FEW_FIELDS));
521 
522     /**
523      * Missing =
524      */
525 
526     enum CONFIG_STR_MISSING_EQUALS = `
527 [CatOne]
528 x = 1
529 [CatTwo]
530 y 2
531 z = 3
532 `;
533 
534     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_MISSING_EQUALS));
535 
536     /**
537      * Missing assignment
538      */
539 
540     enum CONFIG_STR_MISSING_ASSIGN = `
541 [CatOne]
542 x
543 [CatTwo]
544 y = 2
545 z = 3
546 `;
547 
548     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_MISSING_ASSIGN));
549 
550     /**
551      * Wrong field
552      */
553 
554     enum CONFIG_STR_WRONG_FIELD = `
555 [CatOne]
556 x = 1
557 [CatTwo]
558 b = 2
559 c = 3
560 `;
561     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_WRONG_FIELD));
562 
563     /**
564      * Wrong type
565      */
566 
567     enum CONFIG_STR_WRONG_TYPE = `
568 [CatOne]
569 x = 1
570 [CatTwo]
571 y = hello
572 z = 3
573 `;
574 
575     assertThrown!ConfigException(ConfigParser!Config(CONFIG_STR_WRONG_TYPE));
576 }