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