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