1 /++ 2 Various functions related to serialising and deserialising structs into/from 3 .ini-like files. 4 5 Example: 6 --- 7 struct FooSettings 8 { 9 string fooasdf; 10 string bar; 11 string bazzzzzzz; 12 @Quoted flerrp; 13 double pi; 14 } 15 16 FooSettings f; 17 18 f.fooasdf = "foo"; 19 f.bar = "bar"; 20 f.bazzzzzzz = "baz"; 21 f.flerrp = "hirr steff "; 22 f.pi = 3.14159; 23 24 enum fooSerialised = 25 `[Foo] 26 fooasdf foo 27 bar bar 28 bazzzzzzz baz 29 flerrp "hirr steff " 30 pi 3.14159`; 31 32 enum fooJustified = 33 `[Foo] 34 fooasdf foo 35 bar bar 36 bazzzzzzz baz 37 flerrp "hirr steff " 38 pi 3.14159`; 39 40 Appender!(char[]) sink; 41 42 sink.serialise(f); 43 assert(sink.data.justifiedEntryValueText == fooJustified); 44 45 FooSettings mirror; 46 deserialise(fooSerialised, mirror); 47 assert(mirror == f); 48 49 FooSettings mirror2; 50 deserialise(fooJustified, mirror2); 51 assert(mirror2 == mirror); 52 --- 53 +/ 54 module lu.serialisation; 55 56 private: 57 58 import std.meta : allSatisfy; 59 import std.range.primitives : isOutputRange; 60 import std.traits : isAggregateType, isMutable; 61 import std.typecons : Flag, No, Yes; 62 63 public: 64 65 import lu.uda : CannotContainComments, Quoted, Separator, Unserialisable; 66 67 68 // serialise 69 /++ 70 Convenience function to call [serialise] on several objects. 71 72 Example: 73 --- 74 struct Foo 75 { 76 // ... 77 } 78 79 struct Bar 80 { 81 // ... 82 } 83 84 Foo foo; 85 Bar bar; 86 87 Appender!(char[]) sink; 88 89 sink.serialise(foo, bar); 90 assert(!sink.data.empty); 91 --- 92 93 Params: 94 sink = Reference output range to write the serialised objects to (in 95 their .ini file-like format). 96 things = Variadic list of objects to serialise. 97 +/ 98 void serialise(Sink, Things...)(auto ref Sink sink, auto ref Things things) 99 if ((Things.length > 1) && isOutputRange!(Sink, char[]) && 100 allSatisfy!(isAggregateType, Things)) 101 { 102 foreach (immutable i, const thing; things) 103 { 104 if (i > 0) sink.put('\n'); 105 sink.serialise(thing); 106 } 107 } 108 109 110 // serialise 111 /++ 112 Serialises the fields of an object into an .ini file-like format. 113 114 It only serialises fields not annotated with 115 [lu.uda.Unserialisable|Unserialisable], and it doesn't recurse into other 116 structs or classes. 117 118 Example: 119 --- 120 struct Foo 121 { 122 // ... 123 } 124 125 Foo foo; 126 127 Appender!(char[]) sink; 128 129 sink.serialise(foo); 130 assert(!sink.data.empty); 131 --- 132 133 Params: 134 sink = Reference output range to write to, usually an 135 [std.array.Appender|Appender]. 136 thing = Object to serialise. 137 +/ 138 void serialise(Sink, QualThing)(auto ref Sink sink, auto ref QualThing thing) 139 if (isOutputRange!(Sink, char[]) && isAggregateType!QualThing) 140 { 141 import lu.string : stripSuffix; 142 import std.format : format, formattedWrite; 143 import std.traits : Unqual; 144 145 static if (__traits(hasMember, Sink, "data")) 146 { 147 // Sink is not empty, place a newline between current content and new 148 if (sink.data.length) sink.put("\n"); 149 } 150 151 alias Thing = Unqual!QualThing; 152 153 sink.formattedWrite("[%s]\n", Thing.stringof.stripSuffix("Settings")); 154 155 foreach (immutable i, member; thing.tupleof) 156 { 157 import lu.traits : isSerialisable; 158 import lu.uda : Separator, Unserialisable; 159 import std.traits : hasUDA, isAggregateType; 160 161 alias T = Unqual!(typeof(member)); 162 163 static if ( 164 isSerialisable!member && 165 !hasUDA!(thing.tupleof[i], Unserialisable) && 166 !isAggregateType!T) 167 { 168 import std.traits : isArray, isSomeString; 169 170 enum memberstring = __traits(identifier, thing.tupleof[i]); 171 172 static if (!isSomeString!T && isArray!T) 173 { 174 import lu.traits : UnqualArray; 175 import std.traits : getUDAs; 176 177 static if (hasUDA!(thing.tupleof[i], Separator)) 178 { 179 alias separators = getUDAs!(thing.tupleof[i], Separator); 180 enum separator = separators[0].token; 181 182 static if (!separator.length) 183 { 184 enum pattern = "`%s.%s` is annotated with an invalid `Separator` (empty)"; 185 static assert(0, pattern.format(Thing.stringof, memberstring)); 186 } 187 } 188 else static if ((__VERSION__ >= 2087L) && hasUDA!(thing.tupleof[i], string)) 189 { 190 alias separators = getUDAs!(thing.tupleof[i], string); 191 enum separator = separators[0]; 192 193 static if (!separator.length) 194 { 195 enum pattern = "`%s.%s` is annotated with an empty separator string"; 196 static assert(0, pattern.format(Thing.stringof, memberstring)); 197 } 198 } 199 else 200 { 201 enum pattern = "`%s.%s` is not annotated with a `Separator`"; 202 static assert (0, pattern.format(Thing.stringof, memberstring)); 203 } 204 205 alias TA = UnqualArray!(typeof(member)); 206 207 enum arrayPattern = "%-(%s" ~ separator ~ "%)"; 208 enum escapedSeparator = '\\' ~ separator; 209 210 SerialisationUDAs udas; 211 udas.separator = separator; 212 udas.arrayPattern = arrayPattern; 213 udas.escapedSeparator = escapedSeparator; 214 215 immutable value = serialiseArrayImpl!TA(thing.tupleof[i], udas); 216 } 217 else static if (is(T == enum)) 218 { 219 import lu.conv : Enum; 220 immutable value = Enum!T.toString(member); 221 } 222 else 223 { 224 auto value = member; 225 } 226 227 import std.range : hasLength; 228 229 static if (is(T == bool) || is(T == enum)) 230 { 231 enum comment = false; 232 } 233 else static if (is(T == float) || is(T == double)) 234 { 235 import std.conv : to; 236 import std.math : isNaN; 237 immutable comment = member.to!T.isNaN; 238 } 239 else static if (hasLength!T || isSomeString!T) 240 { 241 immutable comment = !member.length; 242 } 243 else 244 { 245 immutable comment = (member == T.init); 246 } 247 248 if (i > 0) sink.put('\n'); 249 250 if (comment) 251 { 252 // .init or otherwise disabled 253 sink.put("#" ~ memberstring); 254 } 255 else 256 { 257 import lu.uda : Quoted; 258 259 static if (isSomeString!T && hasUDA!(thing.tupleof[i], Quoted)) 260 { 261 enum pattern = `%s "%s"`; 262 } 263 else 264 { 265 enum pattern = "%s %s"; 266 } 267 268 sink.formattedWrite(pattern, memberstring, value); 269 } 270 } 271 } 272 273 static if (!__traits(hasMember, Sink, "data")) 274 { 275 // Not an Appender, may be stdout.lockingTextWriter 276 sink.put('\n'); 277 } 278 } 279 280 /// 281 unittest 282 { 283 import lu.uda : Separator, Quoted; 284 import std.array : Appender; 285 286 struct FooSettings 287 { 288 string fooasdf = "foo 1"; 289 string bar = "foo 1"; 290 string bazzzzzzz = "foo 1"; 291 @Quoted flerrp = "hirr steff "; 292 double pi = 3.14159; 293 @Separator(",") int[] arr = [ 1, 2, 3 ]; 294 @Separator(";") string[] harbl = [ "harbl;;", ";snarbl;", "dirp" ]; 295 296 static if (__VERSION__ >= 2087L) 297 { 298 @("|") string[] matey = [ "a", "b", "c" ]; 299 } 300 } 301 302 struct BarSettings 303 { 304 string foofdsa = "foo 2"; 305 string bar = "bar 2"; 306 string bazyyyyyyy = "baz 2"; 307 @Quoted flarrp = " hirrsteff"; 308 double pipyon = 3.0; 309 } 310 311 static if (__VERSION__ >= 2087L) 312 { 313 enum fooSerialised = 314 `[Foo] 315 fooasdf foo 1 316 bar foo 1 317 bazzzzzzz foo 1 318 flerrp "hirr steff " 319 pi 3.14159 320 arr 1,2,3 321 harbl harbl\;\;;\;snarbl\;;dirp 322 matey a|b|c`; 323 } 324 else 325 { 326 enum fooSerialised = 327 `[Foo] 328 fooasdf foo 1 329 bar foo 1 330 bazzzzzzz foo 1 331 flerrp "hirr steff " 332 pi 3.14159 333 arr 1,2,3 334 harbl harbl\;\;;\;snarbl\;;dirp`; 335 } 336 337 Appender!(char[]) fooSink; 338 fooSink.reserve(64); 339 340 fooSink.serialise(FooSettings.init); 341 assert((fooSink.data == fooSerialised), '\n' ~ fooSink.data); 342 343 enum barSerialised = 344 `[Bar] 345 foofdsa foo 2 346 bar bar 2 347 bazyyyyyyy baz 2 348 flarrp " hirrsteff" 349 pipyon 3`; 350 351 Appender!(char[]) barSink; 352 barSink.reserve(64); 353 354 barSink.serialise(BarSettings.init); 355 assert((barSink.data == barSerialised), '\n' ~ barSink.data); 356 357 // try two at once 358 Appender!(char[]) bothSink; 359 bothSink.reserve(128); 360 bothSink.serialise(FooSettings.init, BarSettings.init); 361 assert(bothSink.data == fooSink.data ~ "\n\n" ~ barSink.data); 362 363 class C 364 { 365 int i; 366 bool b; 367 } 368 369 C c = new C; 370 c.i = 42; 371 c.b = true; 372 373 enum cSerialised = 374 `[C] 375 i 42 376 b true`; 377 378 Appender!(char[]) cSink; 379 cSink.reserve(128); 380 cSink.serialise(c); 381 assert((cSink.data == cSerialised), '\n' ~ cSink.data); 382 383 enum Letters { abc, def, ghi, } 384 385 struct Struct 386 { 387 Letters let = Letters.def; 388 } 389 390 enum enumTestSerialised = 391 `[Struct] 392 let def`; 393 394 Struct st; 395 Appender!(char[]) enumTestSink; 396 enumTestSink.serialise(st); 397 assert((enumTestSink.data == enumTestSerialised), '\n' ~ enumTestSink.data); 398 } 399 400 401 // SerialisationUDAs 402 /++ 403 Summary of UDAs that an array to be serialised is annotated with. 404 405 UDAs do not persist across function calls, so they must be summarised 406 (such as in a struct like this) and separately passed, at compile-time or runtime. 407 +/ 408 private struct SerialisationUDAs 409 { 410 /++ 411 Whether or not the member was annotated [lu.uda.Unserialisable|Unserialisable]. 412 +/ 413 bool unserialisable; 414 415 /++ 416 Whether or not the member was annotated with a [lu.uda.Separator|Separator]. 417 +/ 418 string separator; 419 420 /++ 421 The escaped form of [separator]. 422 423 --- 424 enum escapedSeparator = '\\' ~ separator; 425 --- 426 +/ 427 string escapedSeparator; 428 429 /++ 430 The [std.format.format|format] pattern used to format the array this struct 431 refers to. This is separator-specific. 432 433 --- 434 enum arrayPattern = "%-(%s" ~ separator ~ "%)"; 435 --- 436 +/ 437 string arrayPattern; 438 } 439 440 441 // serialiseArrayImpl 442 /++ 443 Serialises a non-string array into a single row. To be used when serialising 444 an aggregate with [serialise]. 445 446 Since UDAs do not persist across function calls, they must be summarised 447 in a [SerialisationUDAs] struct separately so we can pass them at runtime. 448 449 Params: 450 array = Array to serialise. 451 udas = Aggregate of UDAs the original array was annotated with, passed as 452 a runtime value. 453 454 Returns: 455 A string, to be saved as a serialised row in an .ini file-like format. 456 +/ 457 private string serialiseArrayImpl(T)(const auto ref T array, const SerialisationUDAs udas) 458 { 459 import std.format : format, formattedWrite; 460 import std.traits : getUDAs, hasUDA; 461 462 static if (is(T == string[])) 463 { 464 /+ 465 Strings must be formatted differently since the specified separator 466 can occur naturally in the string. 467 +/ 468 string value; 469 470 if (array.length) 471 { 472 import std.algorithm.iteration : map; 473 import std.array : replace; 474 475 enum placeholder = "\0\0"; // anything really 476 477 // Replace separator with a placeholder and flatten with format 478 // enum arrayPattern = "%-(%s" ~ separator ~ "%)"; 479 480 auto separatedElements = array.map!(a => a.replace(udas.separator, placeholder)); 481 value = udas.arrayPattern 482 .format(separatedElements) 483 .replace(placeholder, udas.escapedSeparator); 484 } 485 } 486 else 487 { 488 immutable value = udas.arrayPattern.format(array); 489 } 490 491 return value; 492 } 493 494 495 @safe: 496 497 498 // deserialise 499 /++ 500 Takes an input range containing serialised entry-value text and applies the 501 contents therein to one or more passed struct/class objects. 502 503 Example: 504 --- 505 struct Foo 506 { 507 // ... 508 } 509 510 struct Bar 511 { 512 // ... 513 } 514 515 Foo foo; 516 Bar bar; 517 518 string[][string] missingEntries; 519 string[][string] invalidEntries; 520 521 string fromFile = readText("configuration.conf"); 522 523 fromFile 524 .splitter("\n") 525 .deserialise(missingEntries, invalidEntries, foo, bar); 526 --- 527 528 Params: 529 range = Input range from which to read the serialised text. 530 missingEntries = Out reference of an associative array of string arrays 531 of expected entries that were missing. 532 invalidEntries = Out reference of an associative array of string arrays 533 of unexpected entries that did not belong. 534 things = Reference variadic list of one or more objects to apply the 535 deserialised values to. 536 537 Throws: [DeserialisationException] if there were bad lines. 538 +/ 539 void deserialise(Range, Things...) 540 (auto ref Range range, 541 out string[][string] missingEntries, 542 out string[][string] invalidEntries, 543 ref Things things) pure 544 if (allSatisfy!(isAggregateType, Things) && allSatisfy!(isMutable, Things)) 545 { 546 import lu.string : stripSuffix, stripped; 547 import lu.traits : isSerialisable; 548 import lu.uda : Unserialisable; 549 import std.format : format; 550 import std.traits : Unqual, hasUDA; 551 552 string section; 553 bool[Things.length] processedThings; 554 bool[string][string] encounteredOptions; 555 556 // Populate `encounteredOptions` with all the options in `Things`, but 557 // set them to false. Flip to true when we encounter one. 558 foreach (immutable i, thing; things) 559 { 560 alias Thing = Unqual!(typeof(thing)); 561 562 static foreach (immutable n; 0..things[i].tupleof.length) 563 {{ 564 static if (isSerialisable!(Things[i].tupleof[n]) && 565 !hasUDA!(things[i].tupleof[n], Unserialisable)) 566 { 567 enum memberstring = __traits(identifier, Things[i].tupleof[n]); 568 encounteredOptions[Thing.stringof][memberstring] = false; 569 } 570 }} 571 } 572 573 lineloop: 574 foreach (const rawline; range) 575 { 576 string line = rawline.stripped; // mutable 577 if (!line.length) continue; 578 579 bool commented; 580 581 switch (line[0]) 582 { 583 case '#': 584 case ';': 585 // Comment 586 if (!section.length) continue; // e.g. banner 587 588 while (line.length && ((line[0] == '#') || (line[0] == ';') || (line[0] == '/'))) 589 { 590 line = line[1..$]; 591 } 592 593 if (!line.length) continue; 594 595 commented = true; 596 goto default; 597 598 case '/': 599 if ((line.length > 1) && (line[1] == '/')) 600 { 601 // Also a comment; // 602 line = line[2..$]; 603 } 604 605 while (line.length && (line[0] == '/')) 606 { 607 // Consume extra slashes too 608 line = line[1..$]; 609 } 610 611 if (!line.length) continue; 612 613 commented = true; 614 goto default; 615 616 case '[': 617 // New section. Check if there's still something to do 618 immutable sectionBackup = line; 619 bool stillSomethingToProcess; 620 621 static foreach (immutable size_t i; 0..Things.length) 622 { 623 stillSomethingToProcess |= !processedThings[i]; 624 } 625 626 if (!stillSomethingToProcess) break lineloop; // All done, early break 627 628 try 629 { 630 import std.format : formattedRead; 631 line.formattedRead("[%s]", section); 632 } 633 catch (Exception e) 634 { 635 throw new DeserialisationException("Malformed section header \"%s\", %s" 636 .format(sectionBackup, e.msg)); 637 } 638 continue; 639 640 default: 641 // entry-value line 642 if (!section.length) 643 { 644 throw new DeserialisationException("Sectionless orphan \"%s\"" 645 .format(line)); 646 } 647 648 //thingloop: 649 foreach (immutable i, thing; things) 650 { 651 import lu.string : strippedLeft; 652 import lu.traits : isSerialisable; 653 import lu.uda : CannotContainComments; 654 import std.traits : Unqual; 655 656 alias T = Unqual!(typeof(thing)); 657 enum settingslessT = T.stringof.stripSuffix("Settings").idup; 658 659 if (section != settingslessT) continue; // thingloop; 660 processedThings[i] = true; 661 662 immutable result = splitEntryValue(line.strippedLeft); 663 immutable entry = result.entry; 664 if (!entry.length) continue; 665 666 string value = result.value; // mutable for later slicing 667 668 switch (entry) 669 { 670 static foreach (immutable n; 0..things[i].tupleof.length) 671 {{ 672 static if (isSerialisable!(Things[i].tupleof[n]) && 673 !hasUDA!(things[i].tupleof[n], Unserialisable)) 674 { 675 enum memberstring = __traits(identifier, Things[i].tupleof[n]); 676 677 case memberstring: 678 import lu.objmanip : setMemberByName; 679 680 if (!commented) 681 { 682 // Entry is uncommented; set 683 684 static if (hasUDA!(things[i].tupleof[n], CannotContainComments)) 685 { 686 cast(void)things[i].setMemberByName(entry, value); 687 } 688 else 689 { 690 import lu.string : advancePast; 691 import std.string : indexOf; 692 693 // Slice away any comments 694 value = (value.indexOf('#') != -1) ? value.advancePast('#') : value; 695 value = (value.indexOf(';') != -1) ? value.advancePast(';') : value; 696 value = (value.indexOf("//") != -1) ? value.advancePast("//") : value; 697 cast(void)things[i].setMemberByName(entry, value); 698 } 699 } 700 701 encounteredOptions[Unqual!(Things[i]).stringof][memberstring] = true; 702 continue lineloop; 703 } 704 }} 705 706 default: 707 // Unknown setting in known section 708 if (!commented) invalidEntries[section] ~= entry.length ? entry : line; 709 break; 710 } 711 } 712 713 break; 714 } 715 } 716 717 // Compose missing entries and save them as arrays in `missingEntries`. 718 foreach (immutable encounteredSection, const entryMatches; encounteredOptions) 719 { 720 foreach (immutable entry, immutable encountered; entryMatches) 721 { 722 immutable sectionName = encounteredSection.stripSuffix("Settings"); 723 if (!encountered) missingEntries[sectionName] ~= entry; 724 } 725 } 726 } 727 728 /// 729 unittest 730 { 731 import lu.uda : Separator; 732 import std.algorithm.iteration : splitter; 733 import std.conv : text; 734 735 struct FooSettings 736 { 737 enum Bar { blaawp = 5, oorgle = -1 } 738 int i; 739 string s; 740 bool b; 741 float f; 742 double d; 743 Bar bar; 744 string commented; 745 string slashed; 746 int missing; 747 //bool invalid; 748 749 @Separator(",") 750 { 751 int[] ia; 752 string[] sa; 753 bool[] ba; 754 float[] fa; 755 double[] da; 756 Bar[] bara; 757 } 758 } 759 760 enum serialisedFileContents = 761 `[Foo] 762 i 42 763 ia 1,2,-3,4,5 764 s hello world! 765 sa hello,world,! 766 b true 767 ba true,false,true 768 invalid name 769 770 # comment 771 ; other type of comment 772 // third type of comment 773 774 f 3.14 #hirp 775 fa 0.0,1.1,-2.2,3.3 ;herp 776 d 99.9 //derp 777 da 99.9999,0.0001,-1 778 bar oorgle 779 bara blaawp,oorgle,blaawp 780 #commented hi 781 // slashed also commented 782 invalid ho 783 784 [DifferentSection] 785 ignored completely 786 because no DifferentSection struct was passed 787 nil 5 788 naN !"¤%&/`; 789 790 string[][string] missing; 791 string[][string] invalid; 792 793 FooSettings foo; 794 serialisedFileContents 795 .splitter("\n") 796 .deserialise(missing, invalid, foo); 797 798 with (foo) 799 { 800 assert((i == 42), i.text); 801 assert((ia == [ 1, 2, -3, 4, 5 ]), ia.text); 802 assert((s == "hello world!"), s); 803 assert((sa == [ "hello", "world", "!" ]), sa.text); 804 assert(b); 805 assert((ba == [ true, false, true ]), ba.text); 806 assert((f == 3.14f), f.text); 807 assert((fa == [ 0.0f, 1.1f, -2.2f, 3.3f ]), fa.text); 808 assert((d == 99.9), d.text); 809 810 static if (__VERSION__ >= 2091) 811 { 812 import std.math : isClose; 813 } 814 else 815 { 816 import std.math : approxEqual; 817 alias isClose = approxEqual; 818 } 819 820 // rounding errors with LDC on Windows 821 assert(isClose(da[0], 99.9999), da[0].text); 822 assert(isClose(da[1], 0.0001), da[1].text); 823 assert(isClose(da[2], -1.0), da[2].text); 824 825 with (FooSettings.Bar) 826 { 827 assert((bar == oorgle), bar.text); 828 assert((bara == [ blaawp, oorgle, blaawp ]), bara.text); 829 } 830 } 831 832 import std.algorithm.searching : canFind; 833 834 assert("Foo" in missing); 835 assert(missing["Foo"].canFind("missing")); 836 assert(!missing["Foo"].canFind("commented")); 837 assert(!missing["Foo"].canFind("slashed")); 838 assert("Foo" in invalid); 839 assert(invalid["Foo"].canFind("invalid")); 840 841 struct DifferentSection 842 { 843 string ignored; 844 string because; 845 int nil; 846 string naN; 847 } 848 849 // Can read other structs from the same file 850 851 DifferentSection diff; 852 serialisedFileContents 853 .splitter("\n") 854 .deserialise(missing, invalid, diff); 855 856 with (diff) 857 { 858 assert((ignored == "completely"), ignored); 859 assert((because == "no DifferentSection struct was passed"), because); 860 assert((nil == 5), nil.text); 861 assert((naN == `!"¤%&/`), naN); 862 } 863 864 enum Letters { abc, def, ghi, } 865 866 struct Struct 867 { 868 Letters lt = Letters.def; 869 } 870 871 enum configContents = 872 `[Struct] 873 lt ghi 874 `; 875 Struct st; 876 configContents 877 .splitter("\n") 878 .deserialise(missing, invalid, st); 879 880 assert(st.lt == Letters.ghi); 881 882 class Class 883 { 884 enum Bar { blaawp = 5, oorgle = -1 } 885 int i; 886 string s; 887 bool b; 888 float f; 889 double d; 890 Bar bar; 891 string omitted; 892 893 @Separator(",") 894 { 895 int[] ia; 896 string[] sa; 897 bool[] ba; 898 float[] fa; 899 double[] da; 900 Bar[] bara; 901 } 902 } 903 904 enum serialisedFileContentsClass = 905 `[Class] 906 i 42 907 ia 1,2,-3,4,5 908 s hello world! 909 sa hello,world,! 910 b true 911 ba true,false,true 912 wrong name 913 914 # comment 915 ; other type of comment 916 // third type of comment 917 918 f 3.14 #hirp 919 fa 0.0,1.1,-2.2,3.3 ;herp 920 d 99.9 //derp 921 da 99.9999,0.0001,-1 922 bar oorgle 923 bara blaawp,oorgle,blaawp`; 924 925 Class c = new Class; 926 serialisedFileContentsClass 927 .splitter("\n") 928 .deserialise(missing, invalid, c); 929 930 with (c) 931 { 932 assert((i == 42), i.text); 933 assert((ia == [ 1, 2, -3, 4, 5 ]), ia.text); 934 assert((s == "hello world!"), s); 935 assert((sa == [ "hello", "world", "!" ]), sa.text); 936 assert(b); 937 assert((ba == [ true, false, true ]), ba.text); 938 assert((f == 3.14f), f.text); 939 assert((fa == [ 0.0f, 1.1f, -2.2f, 3.3f ]), fa.text); 940 assert((d == 99.9), d.text); 941 942 static if (__VERSION__ >= 2091) 943 { 944 import std.math : isClose; 945 } 946 else 947 { 948 import std.math : approxEqual; 949 alias isClose = approxEqual; 950 } 951 952 // rounding errors with LDC on Windows 953 assert(isClose(da[0], 99.9999), da[0].text); 954 assert(isClose(da[1], 0.0001), da[1].text); 955 assert(isClose(da[2], -1.0), da[2].text); 956 957 with (Class.Bar) 958 { 959 assert((bar == oorgle), b.text); 960 assert((bara == [ blaawp, oorgle, blaawp ]), bara.text); 961 } 962 } 963 } 964 965 966 // justifiedEntryValueText 967 /++ 968 Takes an unformatted string of serialised entry-value text and justifies it 969 into two neat columns. 970 971 It does one pass through it all first to determine the maximum width of the 972 entry names, then another to format it and eventually return a flat string. 973 974 Example: 975 --- 976 struct Foo 977 { 978 // ... 979 } 980 981 struct Bar 982 { 983 // ... 984 } 985 986 Foo foo; 987 Bar bar; 988 989 Appender!(char[]) sink; 990 991 sink.serialise(foo, bar); 992 immutable justified = sink.data.justifiedEntryValueText; 993 --- 994 995 Params: 996 origLines = Unjustified raw serialised text. 997 998 Returns: 999 .ini file-like text, justified into two columns. 1000 +/ 1001 string justifiedEntryValueText(const string origLines) pure 1002 { 1003 import lu.string : stripped; 1004 import std.algorithm.comparison : max; 1005 import std.algorithm.iteration : splitter; 1006 import std.array : Appender; 1007 1008 if (!origLines.length) return string.init; 1009 1010 enum decentReserve = 4096; 1011 1012 Appender!(string[]) unjustified; 1013 unjustified.reserve(decentReserve); 1014 size_t longestEntryLength; 1015 1016 foreach (immutable rawline; origLines.splitter("\n")) 1017 { 1018 immutable line = rawline.stripped; 1019 1020 if (!line.length) 1021 { 1022 unjustified.put(""); 1023 continue; 1024 } 1025 1026 switch (line[0]) 1027 { 1028 case '#': 1029 case ';': 1030 case '[': 1031 // comment or section header 1032 unjustified.put(line); 1033 continue; 1034 1035 case '/': 1036 if ((line.length > 1) && (line[1] == '/')) 1037 { 1038 // Also a comment 1039 goto case '#'; 1040 } 1041 goto default; 1042 1043 default: 1044 import std.format : format; 1045 1046 immutable result = splitEntryValue(line); 1047 longestEntryLength = max(longestEntryLength, result.entry.length); 1048 unjustified.put("%s %s".format(result.entry, result.value)); 1049 break; 1050 } 1051 } 1052 1053 import lu.numeric : getMultipleOf; 1054 import std.algorithm.iteration : joiner; 1055 1056 Appender!(char[]) justified; 1057 justified.reserve(decentReserve); 1058 1059 assert((longestEntryLength > 0), "No longest entry; is the struct empty?"); 1060 assert((unjustified.data.length > 0), "Unjustified data is empty"); 1061 1062 enum minimumWidth = 24; 1063 immutable width = max(minimumWidth, longestEntryLength.getMultipleOf(4, Yes.alwaysOneUp)); 1064 1065 foreach (immutable i, immutable line; unjustified.data) 1066 { 1067 if (!line.length) 1068 { 1069 // Don't add a linebreak at the top of the file 1070 if (justified.data.length) justified.put("\n"); 1071 continue; 1072 } 1073 1074 if (i > 0) justified.put('\n'); 1075 1076 switch (line[0]) 1077 { 1078 case '#': 1079 case ';': 1080 case '[': 1081 justified.put(line); 1082 continue; 1083 1084 case '/': 1085 if ((line.length > 1) && (line[1] == '/')) 1086 { 1087 // Also a comment 1088 goto case '#'; 1089 } 1090 goto default; 1091 1092 default: 1093 import std.format : formattedWrite; 1094 1095 immutable result = splitEntryValue(line); 1096 justified.formattedWrite("%-*s%s", width, result.entry, result.value); 1097 break; 1098 } 1099 } 1100 1101 return justified.data; 1102 } 1103 1104 /// 1105 unittest 1106 { 1107 import std.algorithm.iteration : splitter; 1108 import std.array : Appender; 1109 import lu.uda : Separator; 1110 1111 struct Foo 1112 { 1113 enum Bar { blaawp = 5, oorgle = -1 } 1114 int someInt = 42; 1115 string someString = "hello world!"; 1116 bool someBool = true; 1117 float someFloat = 3.14f; 1118 double someDouble = 99.9; 1119 Bar someBars = Bar.oorgle; 1120 string harbl; 1121 1122 @Separator(",") 1123 { 1124 int[] intArray = [ 1, 2, -3, 4, 5 ]; 1125 string[] stringArrayy = [ "hello", "world", "!" ]; 1126 bool[] boolArray = [ true, false, true ]; 1127 float[] floatArray = [ 0.0, 1.1, -2.2, 3.3 ]; 1128 double[] doubleArray = [ 99.9999, 0.0001, -1.0 ]; 1129 Bar[] barArray = [ Bar.blaawp, Bar.oorgle, Bar.blaawp ]; 1130 string[] yarn; 1131 } 1132 } 1133 1134 struct DifferentSection 1135 { 1136 string ignored = "completely"; 1137 string because = " no DifferentSection struct was passed"; 1138 int nil = 5; 1139 string naN = `!"#¤%&/`; 1140 } 1141 1142 Appender!(char[]) sink; 1143 sink.reserve(512); 1144 Foo foo; 1145 DifferentSection diff; 1146 enum unjustified = 1147 `[Foo] 1148 someInt 42 1149 someString hello world! 1150 someBool true 1151 someFloat 3.14 1152 someDouble 99.9 1153 someBars oorgle 1154 #harbl 1155 intArray 1,2,-3,4,5 1156 stringArrayy hello,world,! 1157 boolArray true,false,true 1158 floatArray 0,1.1,-2.2,3.3 1159 doubleArray 99.9999,0.0001,-1 1160 barArray blaawp,oorgle,blaawp 1161 #yarn 1162 1163 [DifferentSection] 1164 ignored completely 1165 because no DifferentSection struct was passed 1166 nil 5 1167 naN !"#¤%&/`; 1168 1169 enum justified = 1170 `[Foo] 1171 someInt 42 1172 someString hello world! 1173 someBool true 1174 someFloat 3.14 1175 someDouble 99.9 1176 someBars oorgle 1177 #harbl 1178 intArray 1,2,-3,4,5 1179 stringArrayy hello,world,! 1180 boolArray true,false,true 1181 floatArray 0,1.1,-2.2,3.3 1182 doubleArray 99.9999,0.0001,-1 1183 barArray blaawp,oorgle,blaawp 1184 #yarn 1185 1186 [DifferentSection] 1187 ignored completely 1188 because no DifferentSection struct was passed 1189 nil 5 1190 naN !"#¤%&/`; 1191 1192 sink.serialise(foo, diff); 1193 assert((sink.data == unjustified), '\n' ~ sink.data); 1194 immutable configText = justifiedEntryValueText(sink.data.idup); 1195 1196 assert((configText == justified), '\n' ~ configText); 1197 } 1198 1199 1200 // DeserialisationException 1201 /++ 1202 Exception, to be thrown when the specified serialised text could not be 1203 parsed, for whatever reason. 1204 +/ 1205 final class DeserialisationException : Exception 1206 { 1207 /++ 1208 Create a new [DeserialisationException]. 1209 +/ 1210 this(const string message, 1211 const string file = __FILE__, 1212 const size_t line = __LINE__, 1213 Throwable nextInChain = null) pure nothrow @nogc @safe 1214 { 1215 super(message, file, line, nextInChain); 1216 } 1217 } 1218 1219 1220 // splitEntryValue 1221 /++ 1222 Splits a line into an entry and a value component. 1223 1224 This drop-in-replaces the regex: `^(?P<entry>[^ \t]+)[ \t]+(?P<value>.+)`. 1225 1226 Params: 1227 line = String to split up. 1228 1229 Returns: 1230 A Voldemort struct with an `entry` and a `value` member. 1231 +/ 1232 auto splitEntryValue(const string line) pure nothrow @nogc 1233 { 1234 import std.string : representation; 1235 import std.ascii : isWhite; 1236 1237 struct EntryValue 1238 { 1239 string entry; 1240 string value; 1241 } 1242 1243 EntryValue result; 1244 1245 foreach (immutable i, immutable c; line.representation) 1246 { 1247 if (!c.isWhite) 1248 { 1249 if (result.entry.length) 1250 { 1251 result.value = line[i..$]; 1252 break; 1253 } 1254 } 1255 else if (!result.entry.length) 1256 { 1257 result.entry = line[0..i]; 1258 } 1259 } 1260 1261 if (!result.entry.length) result.entry = line; 1262 1263 return result; 1264 } 1265 1266 /// 1267 unittest 1268 { 1269 { 1270 immutable line = "monochrome true"; 1271 immutable result = splitEntryValue(line); 1272 assert((result.entry == "monochrome"), result.entry); 1273 assert((result.value == "true"), result.value); 1274 } 1275 { 1276 immutable line = "monochrome\tfalse"; 1277 immutable result = splitEntryValue(line); 1278 assert((result.entry == "monochrome"), result.entry); 1279 assert((result.value == "false"), result.value); 1280 } 1281 { 1282 immutable line = "harbl "; 1283 immutable result = splitEntryValue(line); 1284 assert((result.entry == "harbl"), result.entry); 1285 assert(!result.value.length, result.value); 1286 } 1287 { 1288 immutable line = "ha\t \t \t\t \t \t \tha"; 1289 immutable result = splitEntryValue(line); 1290 assert((result.entry == "ha"), result.entry); 1291 assert((result.value == "ha"), result.value); 1292 } 1293 { 1294 immutable line = "#sendAfterConnect"; 1295 immutable result = splitEntryValue(line); 1296 assert((result.entry == "#sendAfterConnect"), result.entry); 1297 assert(!result.value.length, result.value); 1298 } 1299 }