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 }