1 module inifiled; 2 3 import std.range : isInputRange, isOutputRange, ElementType; 4 5 string genINIparser(T)() { 6 return ""; 7 } 8 9 struct INI { 10 string msg; 11 string name; 12 13 static INI opCall(string s, string name = null) pure { 14 INI ret; 15 ret.msg = s; 16 ret.name = name; 17 18 return ret; 19 } 20 } 21 22 INI getINI(T)() pure @trusted { 23 import std.traits : hasUDA; 24 foreach(it; __traits(getAttributes, T)) { 25 static if(is(it == INI)) { 26 return INI(null, null); 27 } 28 static if(is(typeof(it) == INI)) { 29 return it; 30 } 31 } 32 assert(false); 33 } 34 35 INI getINI(T, string mem)() @trusted { 36 import std.traits : hasUDA; 37 foreach(it; __traits(getAttributes, __traits(getMember, T, mem))) { 38 static if(is(it == INI)) { 39 return INI(null, null); 40 } 41 static if(is(typeof(it) == INI)) { 42 return it; 43 } 44 } 45 assert(false, mem); 46 } 47 48 pure string getTypeName(T)() @trusted { 49 import std.traits : fullyQualifiedName; 50 return fullyQualifiedName!T; 51 } 52 53 void readINIFile(T)(ref T t, string filename) { 54 import std.stdio : File; 55 auto iFile = File(filename, "r"); 56 auto iRange = iFile.byLine(); 57 readINIFileImpl(t, iRange); 58 } 59 60 pure bool isSection(T)(T line) @safe nothrow if(isInputRange!T) { 61 bool f; 62 bool b; 63 64 foreach(it; line) { 65 if(it == ' ' || it == '\t') { 66 continue; 67 } else if(it == '[') { 68 f = true; 69 break; 70 } else { 71 break; 72 } 73 } 74 75 foreach_reverse(it; line) { 76 if(it == ' ' || it == '\t') { 77 continue; 78 } else if(it == ']') { 79 b = true; 80 break; 81 } else { 82 break; 83 } 84 } 85 86 return f && b; 87 } 88 89 @safe pure unittest { 90 assert(isSection("[initest.Person]")); 91 assert(isSection(" [initest.Person]")); 92 assert(isSection(" [initest.Person] ")); 93 assert(!isSection(";[initest.Person] ")); 94 } 95 96 pure string getSection(T)(T line) @safe if(isInputRange!T) { 97 return getTimpl!('[',']')(line); 98 } 99 100 pure string getValue(T)(T line) @safe if(isInputRange!T) { 101 return getTimpl!('"','"')(line); 102 } 103 104 pure string getValueArray(T)(T line) @safe if(isInputRange!T) { 105 return getTimpl!('"','"')(line); 106 } 107 108 @safe pure unittest { 109 assert(getValue("firstname=\"Foo\"") == "Foo"); 110 assert(getValue("firstname=\"Foo\",\"Bar\"") == "Foo\",\"Bar"); 111 } 112 113 pure string getKey(T)(T line) @safe if(isInputRange!T) { 114 import std.string : indexOf, strip; 115 import std.exception : enforce; 116 117 ptrdiff_t eq = line.indexOf('='); 118 enforce(eq != -1, "key value pair needs equal sign"); 119 120 return line[0 .. eq].strip(); 121 } 122 123 @safe pure unittest { 124 assert(getKey("firstname=\"Foo\"") == "firstname"); 125 assert(getKey("lastname =\"Foo\",\"Bar\"") == "lastname"); 126 } 127 128 pure string getTimpl(char l, char r, T)(T line) @safe if(isInputRange!T) { 129 import std.string : indexOf, lastIndexOf; 130 import std.format : format; 131 ptrdiff_t l = line.indexOf(l); 132 ptrdiff_t r = line.lastIndexOf(r); 133 134 assert(l+1 < line.length, format("l+1 %u line %u", l+1, line.length)); 135 return line[l+1 .. r].idup; 136 } 137 138 pure bool isKeyValue(T)(T line) @safe if(isInputRange!T) { 139 import std.string : indexOf; 140 ptrdiff_t idx = line.indexOf('='); 141 return idx != -1; 142 } 143 144 @safe pure unittest { 145 assert(getSection("[initest.Person]") == "initest.Person", 146 getSection("[initest.Person]")); 147 assert(getSection(" [initest.Person]") == "initest.Person", 148 getSection("[initest.Person]")); 149 assert(getSection(" [initest.Person] ") == "initest.Person", 150 getSection("[initest.Person]")); 151 assert(getSection("[initest.Person] ") == "initest.Person", 152 getSection("[initest.Person]")); 153 154 assert(getValue("\"initest.Person\"") == "initest.Person", 155 getValue("\"initest.Person\"")); 156 assert(getValue(" \"initest.Person\"") == "initest.Person", 157 getValue("\"initest.Person\"")); 158 assert(getValue(" \"initest.Person\" ") == "initest.Person", 159 getValue("\"initest.Person\"")); 160 assert(getValue("\"initest.Person\" ") == "initest.Person", 161 getValue("\"initest.Person\"")); 162 } 163 164 string buildSectionParse(T)() @safe { 165 import std.traits : hasUDA, fullyQualifiedName, isBasicType, isSomeString, 166 isArray; 167 import std.array : join; 168 import std.format : format; 169 string[] ret; 170 171 foreach(it; __traits(allMembers, T)) { 172 if(hasUDA!(__traits(getMember, T, it), INI) 173 && !isBasicType!(typeof(__traits(getMember, T, it))) 174 && !isSomeString!(typeof(__traits(getMember, T, it))) 175 && !isArray!(typeof(__traits(getMember, T, it)))) 176 { 177 alias MemberType = typeof(__traits(getMember, T, it)); 178 static if(__traits(compiles, getINI!(MemberType))) { 179 const name = getINI!(MemberType).name is null 180 ? fullyQualifiedName!(typeof(__traits(getMember, T, it))) 181 : getINI!(MemberType).name; 182 } else { 183 const name = fullyQualifiedName!(typeof(__traits(getMember, T, it))); 184 } 185 ret ~= ("case \"%s\": { line = readINIFileImpl" ~ 186 "(t.%s, input, depth+1); } ").format(name,it); 187 } 188 } 189 190 // Avoid DMD switch fallthrough warnings 191 if(ret.length) { 192 return "switch(getSection(line)) { // " ~ fullyQualifiedName!T ~ "\n" ~ 193 ret.join("goto case; \n") ~ "goto default;\n default: return line;\n}\n"; 194 } else { 195 return "return line;"; 196 } 197 } 198 199 string buildValueParse(T)() @safe { 200 import std.traits : hasUDA, fullyQualifiedName, isArray, isBasicType, isSomeString; 201 import std.format : format; 202 string ret = "switch(getKey(line)) { // " ~ fullyQualifiedName!T ~ "\n"; 203 204 foreach(it; __traits(allMembers, T)) { 205 if(hasUDA!(__traits(getMember, T, it), INI) && (isBasicType!(typeof(__traits(getMember, T, it))) 206 || isSomeString!(typeof(__traits(getMember, T, it))))) 207 { 208 const string name = getINI!(T, it).name is null ? it : getINI!(T, it).name; 209 ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))(" 210 ~ "getValue(line)); break; }\n").format(name, it, it); 211 } else if(hasUDA!(__traits(getMember, T, it), INI) 212 && isArray!(typeof(__traits(getMember, T, it)))) 213 { 214 const string name = getINI!(T, it).name is null ? it : getINI!(T, it).name; 215 ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))(" 216 ~ "getValueArray(line).split(',')); break; }\n").format(name, it, it); 217 } 218 } 219 220 return ret ~ "default: break;\n}\n"; 221 } 222 223 string readINIFileImpl(T,IRange)(ref T t, ref IRange input, int depth = 0) 224 if(isInputRange!IRange) 225 { 226 import std.conv : to; 227 import std.string : split; 228 import std.algorithm.searching : startsWith; 229 import std.traits : hasUDA, fullyQualifiedName; 230 debug version(debugLogs) { 231 import std.stdio : writefln; 232 } 233 debug version(debugLogs) { 234 writefln("%*s%d %s %x", depth, "", __LINE__, fullyQualifiedName!(typeof(t)), 235 cast(void*)&input); 236 } 237 string line; 238 bool isMultiLine; 239 while(!input.empty()) { 240 import std.algorithm : endsWith; 241 import std.string : stripRight; 242 immutable bool wasMultiLine = isMultiLine; 243 auto currentLine = input.front.stripRight; 244 isMultiLine = currentLine.endsWith(`\`); 245 // remove backslash if existent 246 if(isMultiLine) { 247 currentLine = currentLine[0 .. $ - 1]; 248 } 249 250 if(wasMultiLine) { 251 line ~= currentLine; 252 } else { 253 line = currentLine.idup; 254 } 255 256 input.popFront(); 257 258 if(line.startsWith(";") || isMultiLine) { 259 continue; 260 } 261 debug version(debugLogs) { 262 writefln("%*s%d %s %s %b", depth, "", __LINE__, line, fullyQualifiedName!T, 263 isSection(line)); 264 } 265 266 static if(hasUDA!(T, INI)) { 267 const name = getINI!T().name is null ? fullyQualifiedName!T : getINI!T().name; 268 } else { 269 const name = fullyQualifiedName!T; 270 } 271 if(isSection(line) && getSection(line) != name) { 272 debug version(debugLogs) { 273 pragma(msg, buildSectionParse!(T)); 274 writefln("%*s%d %s", depth, "", __LINE__, getSection(line)); 275 writefln("%*s%d %x", depth, "", __LINE__, 276 cast(void*)&input); 277 } 278 279 mixin(buildSectionParse!(T)); 280 } else if(isKeyValue(line)) { 281 debug version(debugLogs) { 282 pragma(msg, buildValueParse!(T)); 283 writefln("%*s%d %s %s", depth, "", __LINE__, getKey(line), 284 getValue(line)); 285 } 286 287 mixin(buildValueParse!(T)); 288 } 289 } 290 291 return line; 292 } 293 294 void writeComment(ORange,IRange)(ORange orange, IRange irange) @trusted 295 if(isOutputRange!(ORange, ElementType!IRange) && isInputRange!IRange) 296 { 297 size_t idx = 0; 298 foreach(it; irange) { 299 if(idx % 77 == 0) { 300 orange.put("; "); 301 } 302 orange.put(it); 303 304 if((idx+1) % 77 == 0) { 305 orange.put('\n'); 306 } 307 308 ++idx; 309 } 310 orange.put('\n'); 311 } 312 313 void writeValue(ORange,T)(ORange orange, string name, T value) @trusted 314 if(isOutputRange!(ORange, string)) 315 { 316 import std.traits : isArray, isSomeString; 317 import std.format : formattedWrite; 318 static if(isArray!T && !isSomeString!T) { 319 orange.formattedWrite("%s=\"", name); 320 foreach(idx, it; value) { 321 if(idx != 0) { 322 orange.put(','); 323 } 324 orange.formattedWrite("%s", it); 325 } 326 orange.formattedWrite("\""); 327 } else { 328 orange.formattedWrite("%s=\"%s\"\n", name, value); 329 } 330 } 331 332 string removeFromLastPoint(string input) @safe { 333 import std.string : lastIndexOf; 334 ptrdiff_t lDot = input.lastIndexOf('.'); 335 if(lDot != -1 && lDot+1 != input.length) { 336 return input[lDot+1 .. $]; 337 } else { 338 return input; 339 } 340 } 341 342 void writeValues(ORange,T)(ORange oRange, string name, T value) @trusted 343 if(isOutputRange!(ORange, string)) 344 { 345 import std.traits : isBasicType, isSomeString; 346 import std.format : formattedWrite; 347 static if(isSomeString!(ElementType!T) || isBasicType!(ElementType!T)) { 348 oRange.formattedWrite("%s=\"", removeFromLastPoint(name)); 349 foreach(idx, it; value) { 350 if(idx != 0) { 351 oRange.put(','); 352 } 353 oRange.formattedWrite("%s", it); 354 } 355 oRange.put('"'); 356 oRange.put('\n'); 357 } else { 358 for(size_t i = 0; i < value.length; ++i) { 359 oRange.formattedWrite("[%s]\n", name); 360 writeINIFileImpl(value[i], oRange, false); 361 } 362 } 363 } 364 365 void writeINIFile(T)(ref T t, string filename) @trusted { 366 import std.stdio : File; 367 auto oFile = File(filename, "w"); 368 auto oRange = oFile.lockingTextWriter(); 369 writeINIFileImpl(t, oRange, true); 370 } 371 372 void writeINIFileImpl(T,ORange)(ref T t, ORange oRange, bool section) 373 @trusted 374 { 375 import std.traits : getUDAs, hasUDA, Unqual, isArray, isBasicType, 376 isSomeString; 377 import std.format : formattedWrite; 378 if(hasUDA!(T, INI) && section) { 379 writeComment(oRange, getINI!T().msg); 380 } 381 382 if(section) { 383 if(hasUDA!(T, INI)) { 384 auto ini = getINI!(T); 385 if(ini.name !is null) { 386 oRange.formattedWrite("[%s]\n", ini.name); 387 } else { 388 oRange.formattedWrite("[%s]\n", getTypeName!T); 389 } 390 } else { 391 oRange.formattedWrite("[%s]\n", getTypeName!T); 392 } 393 } 394 395 foreach(it; __traits(allMembers, T)) { 396 if(hasUDA!(__traits(getMember, T, it), INI)) { 397 static if(isBasicType!(typeof(__traits(getMember, T, it))) || 398 isSomeString!(typeof(__traits(getMember, T, it)))) 399 { 400 const ini = getINI!(T,it); 401 const name = ini.name is null ? it : ini.name; 402 writeComment(oRange, ini.msg); 403 writeValue(oRange, name, __traits(getMember, t, it)); 404 } else static if(isArray!(typeof(__traits(getMember, T, it)))) { 405 const ini = getINI!(T,it); 406 const name = getTypeName!T ~ "." ~ (ini.name is null ? it : ini.name); 407 writeComment(oRange, ini.msg); 408 writeValues(oRange, name, __traits(getMember, t, it)); 409 } else static if(hasUDA!(__traits(getMember, t, it),INI)) { 410 writeINIFileImpl(__traits(getMember, t, it), oRange, true); 411 } 412 } 413 } 414 } 415 416 version(unittest) { 417 @INI("A child must have a parent") 418 struct Child { 419 @INI("The firstname of the child") 420 string firstname; 421 422 @INI("The age of the child") 423 int age; 424 425 bool opEquals(Child other) { 426 return this.firstname == other.firstname 427 && this.age == other.age; 428 } 429 } 430 431 @INI("A Spouse") 432 struct Spouse { 433 @INI("The firstname of the spouse") 434 string firstname; 435 436 @INI("The age of the spouse") 437 int age; 438 439 @INI("The House of the spouse") 440 House house; 441 442 bool opEquals(Spouse other) { 443 return this.firstname == other.firstname 444 && this.age == other.age 445 && this.house == other.house; 446 } 447 } 448 449 @INI("A Dog") 450 struct Dog { 451 @INI("The name of the Dog") 452 string name; 453 454 @INI("The food consumed") 455 float kg; 456 457 bool opEquals(Dog other) { 458 import std.math : approxEqual, isNaN; 459 return this.name == other.name 460 && (approxEqual(this.kg, other.kg) 461 || (isNaN(this.kg) && isNaN(other.kg)) 462 ); 463 } 464 } 465 466 @INI("A Person") 467 struct Person { 468 @INI("The firstname of the Person") 469 string firstname; 470 471 @INI("The lastname of the Person") 472 string lastname; 473 474 @INI("The age of the Person") 475 int age; 476 477 @INI("The height of the Person") 478 float height; 479 480 @INI("Some strings with a very long long INI description that is longer" ~ 481 " than eigthy lines hopefully." 482 ) 483 string[] someStrings = [":::60180", "asd"]; 484 485 @INI("Some ints") 486 int[] someInts; 487 488 int dontShowThis; 489 490 @INI("A Spouse") 491 Spouse spouse; 492 493 @INI("The family dog") 494 Dog dog; 495 496 bool opEquals(Person other) { 497 import std.math : approxEqual, isNaN; 498 import std.algorithm.comparison : equal; 499 return this.firstname == other.firstname 500 && this.lastname == other.lastname 501 && this.age == other.age 502 && (approxEqual(this.height, other.height) 503 || (isNaN(this.height) && isNaN(other.height))) 504 && equal(this.someStrings, other.someStrings) 505 && equal(this.someInts, other.someInts) 506 && this.spouse == other.spouse 507 && this.dog == other.dog; 508 } 509 } 510 511 @INI("A House") 512 struct House { 513 @INI("Number of Rooms") 514 uint rooms; 515 516 @INI("Number of Floors") 517 uint floors; 518 519 bool opEquals(House other) { 520 return this.rooms == other.rooms 521 && this.floors == other.floors; 522 } 523 } 524 } 525 526 unittest { 527 import std.stdio : writefln; 528 Person p; 529 p.firstname = "Foo"; 530 p.lastname = "Bar"; 531 p.age = 1337; 532 p.height = 7331.0; 533 534 p.someStrings ~= "Hello"; 535 p.someStrings ~= "World"; 536 537 p.someInts ~= [1,2]; 538 539 p.spouse.firstname = "World"; 540 p.spouse.age = 72; 541 542 p.spouse.house.rooms = 5; 543 p.spouse.house.floors = 2; 544 545 p.dog.name = "Wuff"; 546 p.dog.kg = 3.14; 547 548 Person p2; 549 readINIFile(p2, "test/filename.ini"); 550 writefln("\n%s\n", p2); 551 writeINIFile(p2, "test/filenameTmp.ini"); 552 553 Person p3; 554 readINIFile(p3, "test/filenameTmp.ini"); 555 556 if(p2 != p3) { 557 writefln("\n%s\n%s", p2, p3); 558 writefln("Spouse equal %b", p2.spouse == p3.spouse); 559 writefln("Dog equal %b", p2.dog == p3.dog); 560 assert(false); 561 } 562 if(p != p3) { 563 writefln("\n%s\n%s", p, p3); 564 writefln("Spouse equal %b", p.spouse == p3.spouse); 565 writefln("Dog equal %b", p.dog == p3.dog); 566 assert(false); 567 } 568 if(p != p2) { 569 writefln("\n%s\n%s", p, p2); 570 writefln("Spouse equal %b", p.spouse == p2.spouse); 571 writefln("Dog equal %b", p.dog == p2.dog); 572 assert(false); 573 } 574 } 575 576 version(unittest) { 577 enum Check : string { disabled = "disabled"} 578 579 @INI 580 struct StaticAnalysisConfig { 581 @INI 582 string style_check = Check.disabled; 583 584 @INI 585 ModuleFilters filters; 586 587 @INI @ConfuseTheParser 588 string multi_line; 589 } 590 591 private template ModuleFiltersMixin(A) { 592 const string ModuleFiltersMixin = () { 593 string s; 594 foreach (mem; __traits(allMembers, StaticAnalysisConfig)) 595 static if( 596 is( 597 typeof(__traits(getMember, StaticAnalysisConfig, mem)) 598 == string 599 ) 600 ) { 601 s ~= `@INI string[] ` ~ mem ~ ";\n"; 602 } 603 604 return s; 605 }(); 606 } 607 608 @INI 609 struct ModuleFilters { mixin(ModuleFiltersMixin!int); } 610 } 611 612 unittest { 613 StaticAnalysisConfig config; 614 readINIFile(config, "test/dscanner.ini"); 615 assert(config.style_check == "disabled"); 616 assert(config.multi_line == `+std.algorithm -std.foo `); 617 assert(config.filters.style_check == ["+std.algorithm"]); 618 } 619 620 version(unittest) { 621 struct ConfuseTheParser; 622 } 623 624 unittest { 625 @INI("Reactor Configuration", "reactorConfig") 626 struct NukeConfig 627 { 628 @INI double a; 629 @INI double b; 630 @INI double c; 631 } 632 633 @INI("General Configuration", "general") 634 struct Configuration 635 { 636 @INI("Color of the bikeshed", "shedColor") string shedC; 637 @INI @ConfuseTheParser NukeConfig nConfig; 638 } 639 640 Configuration c = Configuration("blue"); 641 c.nConfig.a = 1; 642 c.nConfig.b = 2; 643 c.nConfig.c = 3; 644 c.writeINIFile("test/bikeShed.ini"); 645 Configuration c2; 646 readINIFile(c2, "test/bikeShed.ini"); 647 assert(c2.shedC == "blue"); 648 assert(c2.nConfig.a == 1); 649 assert(c2.nConfig.b == 2); 650 assert(c2.nConfig.c == 3); 651 }