1 module boilerplate.autostring; 2 3 import std.format : format; 4 import std.meta : Alias; 5 import std.traits : Unqual; 6 7 version(unittest) 8 { 9 import std.conv : to; 10 import std.datetime : SysTime; 11 import unit_threaded.should; 12 } 13 14 /++ 15 GenerateToString is a mixin string that automatically generates toString functions, 16 both sink-based and classic, customizable with UDA annotations on classes, members and functions. 17 +/ 18 public enum string GenerateToString = ` 19 import boilerplate.autostring : GenerateToStringTemplate; 20 mixin GenerateToStringTemplate; 21 mixin(typeof(this).generateToStringErrCheck()); 22 mixin(typeof(this).generateToStringImpl()); 23 `; 24 25 /++ 26 When used with objects, toString methods of type string toString() are also created. 27 +/ 28 @("generates legacy toString on objects") 29 unittest 30 { 31 class Class 32 { 33 mixin(GenerateToString); 34 } 35 36 (new Class).to!string.shouldEqual("Class()"); 37 (new Class).toString.shouldEqual("Class()"); 38 } 39 40 /++ 41 A trailing underline in member names is removed when labeling. 42 +/ 43 @("removes trailing underline") 44 unittest 45 { 46 struct Struct 47 { 48 int a_; 49 mixin(GenerateToString); 50 } 51 52 Struct.init.to!string.shouldEqual("Struct(a=0)"); 53 } 54 55 /++ 56 The `@(ToString.Exclude)` tag can be used to exclude a member. 57 +/ 58 @("can exclude a member") 59 unittest 60 { 61 struct Struct 62 { 63 @(ToString.Exclude) 64 int a; 65 mixin(GenerateToString); 66 } 67 68 Struct.init.to!string.shouldEqual("Struct()"); 69 } 70 71 /++ 72 The `@(ToString.Optional)` tag can be used to include a member only if it's in some form "present". 73 This means non-empty for arrays, non-null for objects, non-zero for ints. 74 +/ 75 @("can optionally exclude member") 76 unittest 77 { 78 import std.typecons : Nullable, nullable; 79 80 class Class 81 { 82 mixin(GenerateToString); 83 } 84 85 struct Test // some type that is not comparable to null or 0 86 { 87 mixin(GenerateToString); 88 } 89 90 struct Struct 91 { 92 @(ToString.Optional) 93 int a; 94 95 @(ToString.Optional) 96 string s; 97 98 @(ToString.Optional) 99 Class obj; 100 101 @(ToString.Optional) 102 Nullable!Test nullable; 103 104 mixin(GenerateToString); 105 } 106 107 Struct.init.to!string.shouldEqual("Struct()"); 108 Struct(2, "hi", new Class, Test().nullable).to!string 109 .shouldEqual(`Struct(a=2, s="hi", obj=Class(), nullable=Test())`); 110 Struct(0, "", null, Nullable!Test()).to!string.shouldEqual("Struct()"); 111 } 112 113 /++ 114 The `@(ToString.Optional)` tag can be used with a condition parameter 115 indicating when the type is to be _included._ 116 +/ 117 @("can pass exclusion condition to Optional") 118 unittest 119 { 120 struct Struct 121 { 122 @(ToString.Optional!(a => a > 3)) 123 int i; 124 125 mixin(GenerateToString); 126 } 127 128 Struct.init.to!string.shouldEqual("Struct()"); 129 Struct(3).to!string.shouldEqual("Struct()"); 130 Struct(5).to!string.shouldEqual("Struct(i=5)"); 131 } 132 133 /++ 134 The `@(ToString.Include)` tag can be used to explicitly include a member. 135 This is intended to be used on property methods. 136 +/ 137 @("can include a method") 138 unittest 139 { 140 struct Struct 141 { 142 @(ToString.Include) 143 int foo() const { return 5; } 144 mixin(GenerateToString); 145 } 146 147 Struct.init.to!string.shouldEqual("Struct(foo=5)"); 148 } 149 150 /++ 151 The `@(ToString.Unlabeled)` tag will omit a field's name. 152 +/ 153 @("can omit names") 154 unittest 155 { 156 struct Struct 157 { 158 @(ToString.Unlabeled) 159 int a; 160 mixin(GenerateToString); 161 } 162 163 Struct.init.to!string.shouldEqual("Struct(0)"); 164 } 165 166 /++ 167 Parent class `toString()` methods are included automatically as the first entry, except if the parent class is `Object`. 168 +/ 169 @("can be used in both parent and child class") 170 unittest 171 { 172 class ParentClass { mixin(GenerateToString); } 173 174 class ChildClass : ParentClass { mixin(GenerateToString); } 175 176 (new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())"); 177 } 178 179 @("invokes manually implemented parent toString") 180 unittest 181 { 182 class ParentClass 183 { 184 override string toString() const 185 { 186 return "Some string"; 187 } 188 } 189 190 class ChildClass : ParentClass { mixin(GenerateToString); } 191 192 (new ChildClass).to!string.shouldEqual("ChildClass(Some string)"); 193 } 194 195 @("can partially override toString in child class") 196 unittest 197 { 198 class ParentClass 199 { 200 mixin(GenerateToString); 201 } 202 203 class ChildClass : ParentClass 204 { 205 override string toString() const 206 { 207 return "Some string"; 208 } 209 210 mixin(GenerateToString); 211 } 212 213 (new ChildClass).to!string.shouldEqual("Some string"); 214 } 215 216 @("invokes manually implemented string toString in same class") 217 unittest 218 { 219 class Class 220 { 221 override string toString() const 222 { 223 return "Some string"; 224 } 225 226 mixin(GenerateToString); 227 } 228 229 (new Class).to!string.shouldEqual("Some string"); 230 } 231 232 @("invokes manually implemented void toString in same class") 233 unittest 234 { 235 class Class 236 { 237 void toString(scope void delegate(const(char)[]) sink) const 238 { 239 sink("Some string"); 240 } 241 242 mixin(GenerateToString); 243 } 244 245 (new Class).to!string.shouldEqual("Some string"); 246 } 247 248 /++ 249 Inclusion of parent class `toString()` can be prevented using `@(ToString.ExcludeSuper)`. 250 +/ 251 @("can suppress parent class toString()") 252 unittest 253 { 254 class ParentClass { } 255 256 @(ToString.ExcludeSuper) 257 class ChildClass : ParentClass { mixin(GenerateToString); } 258 259 (new ChildClass).to!string.shouldEqual("ChildClass()"); 260 } 261 262 /++ 263 The `@(ToString.Naked)` tag will omit the name of the type and parentheses. 264 +/ 265 @("can omit the type name") 266 unittest 267 { 268 @(ToString.Naked) 269 struct Struct 270 { 271 int a; 272 mixin(GenerateToString); 273 } 274 275 Struct.init.to!string.shouldEqual("a=0"); 276 } 277 278 /++ 279 Fields with the same name (ignoring capitalization) as their type, are unlabeled by default. 280 +/ 281 @("does not label fields with the same name as the type") 282 unittest 283 { 284 struct Struct1 { mixin(GenerateToString); } 285 286 struct Struct2 287 { 288 Struct1 struct1; 289 mixin(GenerateToString); 290 } 291 292 Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 293 } 294 295 @("does not label fields with the same name as the type, even if they're const") 296 unittest 297 { 298 struct Struct1 { mixin(GenerateToString); } 299 300 struct Struct2 301 { 302 const Struct1 struct1; 303 mixin(GenerateToString); 304 } 305 306 Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 307 } 308 309 @("does not label fields with the same name as the type, even if they're nullable") 310 unittest 311 { 312 import std.typecons : Nullable; 313 314 struct Struct1 { mixin(GenerateToString); } 315 316 struct Struct2 317 { 318 const Nullable!Struct1 struct1; 319 mixin(GenerateToString); 320 } 321 322 Struct2(Nullable!Struct1(Struct1())).to!string.shouldEqual("Struct2(Struct1())"); 323 } 324 325 /++ 326 This behavior can be prevented by explicitly tagging the field with `@(ToString.Labeled)`. 327 +/ 328 @("does label fields tagged as labeled") 329 unittest 330 { 331 struct Struct1 { mixin(GenerateToString); } 332 333 struct Struct2 334 { 335 @(ToString.Labeled) 336 Struct1 struct1; 337 mixin(GenerateToString); 338 } 339 340 Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())"); 341 } 342 343 /++ 344 Fields of type 'SysTime' and name 'time' are unlabeled by default. 345 +/ 346 @("does not label SysTime time field correctly") 347 unittest 348 { 349 struct Struct { SysTime time; mixin(GenerateToString); } 350 351 Struct strct; 352 strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z"); 353 354 // see unittest/config/string.d 355 strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)"); 356 } 357 358 /++ 359 Fields named 'id' are unlabeled only if they define their own toString(). 360 +/ 361 @("does not label id fields with toString()") 362 unittest 363 { 364 struct IdType 365 { 366 string toString() const { return "ID"; } 367 } 368 369 struct Struct 370 { 371 IdType id; 372 mixin(GenerateToString); 373 } 374 375 Struct.init.to!string.shouldEqual("Struct(ID)"); 376 } 377 378 /++ 379 Otherwise, they are labeled as normal. 380 +/ 381 @("labels id fields without toString") 382 unittest 383 { 384 struct Struct 385 { 386 int id; 387 mixin(GenerateToString); 388 } 389 390 Struct.init.to!string.shouldEqual("Struct(id=0)"); 391 } 392 393 /++ 394 Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default, 395 as long as the array is NonEmpty. Otherwise, there would be no way to tell what the field contains. 396 +/ 397 @("does not label fields named a plural of the basetype, if the type is an array") 398 unittest 399 { 400 import boilerplate.conditions : NonEmpty; 401 402 struct Value { mixin(GenerateToString); } 403 struct Entity { mixin(GenerateToString); } 404 struct Day { mixin(GenerateToString); } 405 406 struct Struct 407 { 408 @NonEmpty 409 Value[] values; 410 411 @NonEmpty 412 Entity[] entities; 413 414 @NonEmpty 415 Day[] days; 416 417 mixin(GenerateToString); 418 } 419 420 auto value = Struct( 421 [Value()], 422 [Entity()], 423 [Day()]); 424 425 value.to!string.shouldEqual("Struct([Value()], [Entity()], [Day()])"); 426 } 427 428 @("does not label fields named a plural of the basetype, if the type is a BitFlags") 429 unittest 430 { 431 import std.typecons : BitFlags; 432 433 enum Flag 434 { 435 A = 1 << 0, 436 B = 1 << 1, 437 } 438 439 struct Struct 440 { 441 BitFlags!Flag flags; 442 443 mixin(GenerateToString); 444 } 445 446 auto value = Struct(BitFlags!Flag(Flag.A, Flag.B)); 447 448 value.to!string.shouldEqual("Struct(Flag(A, B))"); 449 } 450 451 /++ 452 Fields that are not NonEmpty are always labeled. 453 This is because they can be empty, in which case you can't tell what's in them from naming. 454 +/ 455 @("does label fields that may be empty") 456 unittest 457 { 458 import boilerplate.conditions : NonEmpty; 459 460 struct Value { mixin(GenerateToString); } 461 462 struct Struct 463 { 464 Value[] values; 465 466 mixin(GenerateToString); 467 } 468 469 Struct(null).to!string.shouldEqual("Struct(values=[])"); 470 } 471 472 /++ 473 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue. 474 +/ 475 @("does not collide with accessors") 476 unittest 477 { 478 struct Struct 479 { 480 import boilerplate.accessors : ConstRead, GenerateFieldAccessors; 481 482 @ConstRead 483 private int a_; 484 485 mixin(GenerateFieldAccessors); 486 487 mixin(GenerateToString); 488 } 489 490 Struct.init.to!string.shouldEqual("Struct(a=0)"); 491 } 492 493 @("supports child classes of abstract classes") 494 unittest 495 { 496 static abstract class ParentClass 497 { 498 } 499 class ChildClass : ParentClass 500 { 501 mixin(GenerateToString); 502 } 503 } 504 505 @("supports custom toString handlers") 506 unittest 507 { 508 struct Struct 509 { 510 @ToStringHandler!(i => i ? "yes" : "no") 511 int i; 512 513 mixin(GenerateToString); 514 } 515 516 Struct.init.to!string.shouldEqual("Struct(i=no)"); 517 } 518 519 @("passes nullable unchanged to custom toString handlers") 520 unittest 521 { 522 import std.typecons : Nullable; 523 524 struct Struct 525 { 526 @ToStringHandler!(ni => ni.isNull ? "no" : "yes") 527 Nullable!int ni; 528 529 mixin(GenerateToString); 530 } 531 532 Struct.init.to!string.shouldEqual("Struct(ni=no)"); 533 } 534 535 // see unittest.config.string 536 @("supports optional BitFlags in structs") 537 unittest 538 { 539 import std.typecons : BitFlags; 540 541 enum Enum 542 { 543 A = 1, 544 B = 2, 545 } 546 547 struct Struct 548 { 549 @(ToString.Optional) 550 BitFlags!Enum field; 551 552 mixin(GenerateToString); 553 } 554 555 Struct.init.to!string.shouldEqual("Struct()"); 556 } 557 558 @("prints hashmaps in deterministic order") 559 unittest 560 { 561 struct Struct 562 { 563 string[string] map; 564 565 mixin(GenerateToString); 566 } 567 568 bool foundCollision = false; 569 570 foreach (key1; ["opstop", "opsto"]) 571 { 572 enum key2 = "foo"; // collide 573 574 const first = Struct([key1: null, key2: null]); 575 string[string] backwardsHashmap; 576 577 backwardsHashmap[key2] = null; 578 backwardsHashmap[key1] = null; 579 580 const second = Struct(backwardsHashmap); 581 582 if (first.map.keys != second.map.keys) 583 { 584 foundCollision = true; 585 first.to!string.shouldEqual(second.to!string); 586 } 587 } 588 assert(foundCollision, "none of the listed keys caused a hash collision"); 589 } 590 591 @("applies custom formatters to types in hashmaps") 592 unittest 593 { 594 import std.datetime : SysTime; 595 596 struct Struct 597 { 598 SysTime[string] map; 599 600 mixin(GenerateToString); 601 } 602 603 const expected = "2003-02-01T11:55:00Z"; 604 const value = Struct(["foo": SysTime.fromISOExtString(expected)]); 605 606 value.to!string.shouldEqual(`Struct(map=["foo": ` ~ expected ~ `])`); 607 } 608 609 @("can format associative array of Nullable SysTime") 610 unittest 611 { 612 import std.datetime : SysTime; 613 import std.typecons : Nullable; 614 615 struct Struct 616 { 617 Nullable!SysTime[string] map; 618 619 mixin(GenerateToString); 620 } 621 622 const expected = `Struct(map=["foo": null])`; 623 const value = Struct(["foo": Nullable!SysTime()]); 624 625 value.to!string.shouldEqual(expected); 626 } 627 628 @("can format associative array of type that cannot be sorted") 629 unittest 630 { 631 struct Struct 632 { 633 mixin(GenerateToString); 634 } 635 636 struct Struct2 637 { 638 bool[Struct] hashmap; 639 640 mixin(GenerateToString); 641 } 642 643 const expected = `Struct2(hashmap=[])`; 644 const value = Struct2(null); 645 646 value.to!string.shouldEqual(expected); 647 } 648 649 @("labels nested types with fully qualified names") 650 unittest 651 { 652 import std.datetime : SysTime; 653 import std.typecons : Nullable; 654 655 struct Struct 656 { 657 struct Struct2 658 { 659 mixin(GenerateToString); 660 } 661 662 Struct2 struct2; 663 664 mixin(GenerateToString); 665 } 666 667 const expected = `Struct(Struct.Struct2())`; 668 const value = Struct(Struct.Struct2()); 669 670 value.to!string.shouldEqual(expected); 671 } 672 673 @("supports fully qualified names with quotes") 674 unittest 675 { 676 struct Struct(string s) 677 { 678 struct Struct2 679 { 680 mixin(GenerateToString); 681 } 682 683 Struct2 struct2; 684 685 mixin(GenerateToString); 686 } 687 688 const expected = `Struct!"foo"(Struct!"foo".Struct2())`; 689 const value = Struct!"foo"(Struct!"foo".Struct2()); 690 691 value.to!string.shouldEqual(expected); 692 } 693 694 @("optional-always null Nullable") 695 unittest 696 { 697 import std.typecons : Nullable; 698 699 struct Struct 700 { 701 @(ToString.Optional!(a => true)) 702 Nullable!int i; 703 704 mixin(GenerateToString); 705 } 706 707 Struct().to!string.shouldEqual("Struct(i=Nullable.null)"); 708 } 709 710 @("force-included null Nullable") 711 unittest 712 { 713 import std.typecons : Nullable; 714 715 struct Struct 716 { 717 @(ToString.Include) 718 Nullable!int i; 719 720 mixin(GenerateToString); 721 } 722 723 Struct().to!string.shouldEqual("Struct(i=Nullable.null)"); 724 } 725 726 // test for clean detection of Nullable 727 @("struct with isNull") 728 unittest 729 { 730 struct Inner 731 { 732 bool isNull() const { return false; } 733 734 mixin(GenerateToString); 735 } 736 737 struct Outer 738 { 739 Inner inner; 740 741 mixin(GenerateToString); 742 } 743 744 Outer().to!string.shouldEqual("Outer(Inner())"); 745 } 746 747 // regression 748 @("mutable struct with alias this of sink toString") 749 unittest 750 { 751 struct Inner 752 { 753 public void toString(scope void delegate(const(char)[]) sink) const 754 { 755 sink("Inner()"); 756 } 757 } 758 759 struct Outer 760 { 761 Inner inner; 762 763 alias inner this; 764 765 mixin(GenerateToString); 766 } 767 } 768 769 @("immutable struct with alias this of const toString") 770 unittest 771 { 772 struct Inner 773 { 774 string toString() const { return "Inner()"; } 775 } 776 777 immutable struct Outer 778 { 779 Inner inner; 780 781 alias inner this; 782 783 mixin(GenerateToString); 784 } 785 786 Outer().to!string.shouldEqual("Outer(Inner())"); 787 } 788 789 mixin template GenerateToStringTemplate() 790 { 791 // this is a separate function to reduce the 792 // "warning: unreachable code" spam that is falsely created from static foreach 793 private static generateToStringErrCheck() 794 { 795 if (!__ctfe) 796 { 797 return null; 798 } 799 800 import boilerplate.autostring : ToString, typeName; 801 import boilerplate.util : GenNormalMemberTuple; 802 import std.string : format; 803 804 bool udaIncludeSuper; 805 bool udaExcludeSuper; 806 807 foreach (uda; __traits(getAttributes, typeof(this))) 808 { 809 static if (is(typeof(uda) == ToString)) 810 { 811 switch (uda) 812 { 813 case ToString.IncludeSuper: udaIncludeSuper = true; break; 814 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 815 default: break; 816 } 817 } 818 } 819 820 if (udaIncludeSuper && udaExcludeSuper) 821 { 822 return format!(`static assert(false, ` ~ 823 `"Contradictory tags on '" ~ %(%s%) ~ "': IncludeSuper and ExcludeSuper");`) 824 ([typeName!(typeof(this))]); 825 } 826 827 mixin GenNormalMemberTuple!true; 828 829 foreach (member; NormalMemberTuple) 830 { 831 enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member))); 832 833 static if (error) 834 { 835 return format!error(member); 836 } 837 } 838 839 return ``; 840 } 841 842 private static generateToStringImpl() 843 { 844 if (!__ctfe) 845 { 846 return null; 847 } 848 849 import boilerplate.autostring : 850 hasOwnStringToString, hasOwnVoidToString, isMemberUnlabeledByDefault, ToString, typeName; 851 import boilerplate.conditions : NonEmpty; 852 import boilerplate.util : GenNormalMemberTuple, udaIndex; 853 import std.meta : Alias; 854 import std.string : endsWith, format, split, startsWith, strip; 855 import std.traits : BaseClassesTuple, getUDAs, Unqual; 856 import std.typecons : Nullable; 857 858 // synchronized without lock contention is basically free, so always do it 859 // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 860 enum synchronize = false && is(typeof(this) == class); 861 862 const constExample = typeof(this).init; 863 auto normalExample = typeof(this).init; 864 865 enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString") 866 && is(typeof(normalExample.toString()) == string); 867 enum alreadyHaveUsableStringToString = alreadyHaveStringToString 868 && is(typeof(constExample.toString()) == string); 869 870 enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString") 871 && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void); 872 enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString 873 && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void); 874 875 enum isObject = is(typeof(this): Object); 876 877 static if (isObject) 878 { 879 enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super)); 880 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super)); 881 } 882 else 883 { 884 enum userDefinedStringToString = hasOwnStringToString!(typeof(this)); 885 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this)); 886 } 887 888 static if (userDefinedStringToString && userDefinedVoidToString) 889 { 890 string result = ``; // Nothing to be done. 891 } 892 // if the user has defined their own string toString() in this aggregate: 893 else static if (userDefinedStringToString) 894 { 895 // just call it. 896 static if (alreadyHaveUsableStringToString) 897 { 898 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~ 899 ` sink(this.toString());` ~ 900 ` }`; 901 902 static if (isObject 903 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 904 { 905 result = `override ` ~ result; 906 } 907 } 908 else 909 { 910 string result = `static assert(false, "toString is not const in this class.");`; 911 } 912 } 913 // if the user has defined their own void toString() in this aggregate: 914 else 915 { 916 string result = null; 917 918 static if (!userDefinedVoidToString) 919 { 920 bool nakedMode; 921 bool udaIncludeSuper; 922 bool udaExcludeSuper; 923 924 foreach (uda; __traits(getAttributes, typeof(this))) 925 { 926 static if (is(typeof(uda) == ToStringEnum)) 927 { 928 switch (uda) 929 { 930 case ToString.Naked: nakedMode = true; break; 931 case ToString.IncludeSuper: udaIncludeSuper = true; break; 932 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 933 default: break; 934 } 935 } 936 } 937 938 string NamePlusOpenParen = typeName!(typeof(this)) ~ "("; 939 940 version(AutoStringDebug) 941 { 942 result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString); 943 } 944 945 static if (isObject 946 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 947 { 948 result ~= `override `; 949 } 950 951 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {` 952 ~ `import boilerplate.autostring: ToStringHandler;` 953 ~ `import boilerplate.util: sinkWrite;` 954 ~ `import std.traits: getUDAs;`; 955 956 static if (synchronize) 957 { 958 result ~= `synchronized (this) { `; 959 } 960 961 if (!nakedMode) 962 { 963 result ~= format!`sink(%(%s%));`([NamePlusOpenParen]); 964 } 965 966 bool includeSuper = false; 967 968 static if (isObject) 969 { 970 if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString) 971 { 972 includeSuper = true; 973 } 974 } 975 976 if (udaIncludeSuper) 977 { 978 includeSuper = true; 979 } 980 else if (udaExcludeSuper) 981 { 982 includeSuper = false; 983 } 984 985 static if (isObject) 986 { 987 if (includeSuper) 988 { 989 static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString) 990 { 991 return `static assert(false, ` 992 ~ `"cannot include super class in GenerateToString: ` 993 ~ `parent class has no usable toString!");`; 994 } 995 else { 996 static if (alreadyHaveUsableVoidToString) 997 { 998 result ~= `super.toString(sink);`; 999 } 1000 else 1001 { 1002 result ~= `sink(super.toString());`; 1003 } 1004 result ~= `bool comma = true;`; 1005 } 1006 } 1007 else 1008 { 1009 result ~= `bool comma = false;`; 1010 } 1011 } 1012 else 1013 { 1014 result ~= `bool comma = false;`; 1015 } 1016 1017 result ~= `{`; 1018 1019 mixin GenNormalMemberTuple!(true); 1020 1021 foreach (member; NormalMemberTuple) 1022 { 1023 mixin("alias symbol = typeof(this)." ~ member ~ ";"); 1024 1025 enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1; 1026 enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1; 1027 enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1; 1028 enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1; 1029 enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1; 1030 enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1; 1031 enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1; 1032 1033 // see std.traits.isFunction!() 1034 static if ( 1035 is(symbol == function) 1036 || is(typeof(symbol) == function) 1037 || (is(typeof(&symbol) U : U*) && is(U == function))) 1038 { 1039 enum isFunction = true; 1040 } 1041 else 1042 { 1043 enum isFunction = false; 1044 } 1045 1046 enum includeOverride = udaInclude || udaOptional; 1047 1048 enum includeMember = (!isFunction || includeOverride) && !udaExclude; 1049 1050 static if (includeMember) 1051 { 1052 string memberName = member; 1053 1054 if (memberName.endsWith("_")) 1055 { 1056 memberName = memberName[0 .. $ - 1]; 1057 } 1058 1059 bool labeled = true; 1060 1061 static if (udaUnlabeled) 1062 { 1063 labeled = false; 1064 } 1065 1066 if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty)) 1067 { 1068 labeled = false; 1069 } 1070 1071 static if (udaLabeled) 1072 { 1073 labeled = true; 1074 } 1075 1076 string membervalue = `this.` ~ member; 1077 1078 bool escapeStrings = true; 1079 1080 static if (udaToStringHandler) 1081 { 1082 alias Handlers = getUDAs!(symbol, ToStringHandler); 1083 1084 static assert(Handlers.length == 1); 1085 1086 static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init))) 1087 { 1088 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(` 1089 ~ membervalue 1090 ~ `)`; 1091 1092 escapeStrings = false; 1093 } 1094 else 1095 { 1096 return `static assert(false, "cannot determine how to call ToStringHandler");`; 1097 } 1098 } 1099 1100 string readMemberValue = membervalue; 1101 string conditionalWritestmt; // formatted with sink.sinkWrite(... readMemberValue ... ) 1102 1103 static if (udaOptional) 1104 { 1105 import std.array : empty; 1106 1107 enum optionalIndex = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)); 1108 alias optionalUda = Alias!(__traits(getAttributes, symbol)[optionalIndex]); 1109 1110 static if (is(optionalUda == struct)) 1111 { 1112 conditionalWritestmt = format!q{ 1113 if (__traits(getAttributes, %s)[%s].condition(%s)) { %%s } 1114 } (membervalue, optionalIndex, membervalue); 1115 } 1116 else static if (__traits(compiles, typeof(symbol).init.isNull)) 1117 { 1118 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1119 (membervalue); 1120 1121 static if (is(typeof(symbol) : Nullable!T, T)) 1122 { 1123 readMemberValue = membervalue ~ ".get"; 1124 } 1125 } 1126 else static if (__traits(compiles, typeof(symbol).init.empty)) 1127 { 1128 conditionalWritestmt = format!q{import std.array : empty; if (!%s.empty) { %%s }} 1129 (membervalue); 1130 } 1131 else static if (__traits(compiles, typeof(symbol).init !is null)) 1132 { 1133 conditionalWritestmt = format!q{if (%s !is null) { %%s }} 1134 (membervalue); 1135 } 1136 else static if (__traits(compiles, typeof(symbol).init != 0)) 1137 { 1138 conditionalWritestmt = format!q{if (%s != 0) { %%s }} 1139 (membervalue); 1140 } 1141 else static if (__traits(compiles, { if (typeof(symbol).init) { } })) 1142 { 1143 conditionalWritestmt = format!q{if (%s) { %%s }} 1144 (membervalue); 1145 } 1146 else 1147 { 1148 return format!(`static assert(false, ` 1149 ~ `"don't know how to figure out whether %s is present.");`) 1150 (member); 1151 } 1152 } 1153 else 1154 { 1155 // Nullables (without handler, that aren't force-included) fall back to optional 1156 static if (!udaToStringHandler && !udaInclude && 1157 __traits(compiles, typeof(symbol).init.isNull)) 1158 { 1159 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1160 (membervalue); 1161 1162 static if (is(typeof(symbol) : Nullable!T, T)) 1163 { 1164 readMemberValue = membervalue ~ ".get"; 1165 } 1166 } 1167 else 1168 { 1169 conditionalWritestmt = q{ %s }; 1170 } 1171 } 1172 1173 string writestmt; 1174 1175 if (labeled) 1176 { 1177 writestmt = format!`sink.sinkWrite(comma, %s, "%s=%%s", %s);` 1178 (escapeStrings, memberName, readMemberValue); 1179 } 1180 else 1181 { 1182 writestmt = format!`sink.sinkWrite(comma, %s, "%%s", %s);` 1183 (escapeStrings, readMemberValue); 1184 } 1185 1186 result ~= format(conditionalWritestmt, writestmt); 1187 } 1188 } 1189 1190 result ~= `} `; 1191 1192 if (!nakedMode) 1193 { 1194 result ~= `sink(")");`; 1195 } 1196 1197 static if (synchronize) 1198 { 1199 result ~= `} `; 1200 } 1201 1202 result ~= `} `; 1203 } 1204 1205 // generate fallback string toString() 1206 // that calls, specifically, *our own* toString impl. 1207 // (this is important to break cycles when a subclass implements a toString that calls super.toString) 1208 static if (isObject) 1209 { 1210 result ~= `override `; 1211 } 1212 1213 result ~= `public string toString() const {` 1214 ~ `string result;` 1215 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });` 1216 ~ `return result;` 1217 ~ `}`; 1218 } 1219 return result; 1220 } 1221 } 1222 1223 template checkAttributeConsistency(Attributes...) 1224 { 1225 enum checkAttributeConsistency = checkAttributeHelper(); 1226 1227 private string checkAttributeHelper() 1228 { 1229 if (!__ctfe) 1230 { 1231 return null; 1232 } 1233 1234 import std.string : format; 1235 1236 bool include, exclude, optional, labeled, unlabeled; 1237 1238 foreach (uda; Attributes) 1239 { 1240 static if (is(typeof(uda) == ToStringEnum)) 1241 { 1242 switch (uda) 1243 { 1244 case ToString.Include: include = true; break; 1245 case ToString.Exclude: exclude = true; break; 1246 case ToString.Labeled: labeled = true; break; 1247 case ToString.Unlabeled: unlabeled = true; break; 1248 default: break; 1249 } 1250 } 1251 else static if (is(uda == struct) && __traits(isSame, uda, ToString.Optional)) 1252 { 1253 optional = true; 1254 } 1255 } 1256 1257 if (include && exclude) 1258 { 1259 return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`; 1260 } 1261 1262 if (include && optional) 1263 { 1264 return `static assert(false, "Redundant tags on '%s': Optional implies Include");`; 1265 } 1266 1267 if (exclude && optional) 1268 { 1269 return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`; 1270 } 1271 1272 if (labeled && unlabeled) 1273 { 1274 return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`; 1275 } 1276 1277 return null; 1278 } 1279 } 1280 1281 struct ToStringHandler(alias Handler_) 1282 { 1283 alias Handler = Handler_; 1284 } 1285 1286 enum ToStringEnum 1287 { 1288 // these go on the class 1289 Naked, 1290 IncludeSuper, 1291 ExcludeSuper, 1292 1293 // these go on the field/method 1294 Unlabeled, 1295 Labeled, 1296 Exclude, 1297 Include, 1298 } 1299 1300 struct ToString 1301 { 1302 static foreach (name; __traits(allMembers, ToStringEnum)) 1303 { 1304 mixin(format!q{enum %s = ToStringEnum.%s;}(name, name)); 1305 } 1306 1307 static struct Optional(alias condition_) 1308 { 1309 alias condition = condition_; 1310 } 1311 } 1312 1313 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty) 1314 { 1315 import std.datetime : SysTime; 1316 import std.range.primitives : ElementType, isInputRange; 1317 // Types whose toString starts with the contained type 1318 import std.typecons : BitFlags, Nullable; 1319 1320 field = field.toLower; 1321 1322 static if (isInputRange!Type) 1323 { 1324 alias BaseType = ElementType!Type; 1325 1326 if (field == BaseType.stringof.toLower.pluralize && attribNonEmpty) 1327 { 1328 return true; 1329 } 1330 } 1331 else static if (is(Type: const BitFlags!BaseType, BaseType)) 1332 { 1333 if (field == BaseType.stringof.toLower.pluralize) 1334 { 1335 return true; 1336 } 1337 } 1338 else static if (is(Type: const Nullable!BaseType, BaseType)) 1339 { 1340 if (field == BaseType.stringof.toLower) 1341 { 1342 return true; 1343 } 1344 } 1345 1346 return field == Type.stringof.toLower 1347 || (field == "time" && is(Type == SysTime)) 1348 || (field == "id" && is(typeof(Type.toString))); 1349 } 1350 1351 private string toLower(string text) 1352 { 1353 import std.string : stdToLower = toLower; 1354 1355 string result = null; 1356 1357 foreach (ub; cast(immutable(ubyte)[]) text) 1358 { 1359 if (ub >= 0x80) // utf-8, non-ascii 1360 { 1361 return text.stdToLower; 1362 } 1363 if (ub >= 'A' && ub <= 'Z') 1364 { 1365 result ~= cast(char) (ub + ('a' - 'A')); 1366 } 1367 else 1368 { 1369 result ~= cast(char) ub; 1370 } 1371 } 1372 return result; 1373 } 1374 1375 // http://code.activestate.com/recipes/82102/ 1376 private string pluralize(string label) 1377 { 1378 import std.algorithm.searching : contain = canFind; 1379 1380 string postfix = "s"; 1381 if (label.length > 2) 1382 { 1383 enum vowels = "aeiou"; 1384 1385 if (label.stringEndsWith("ch") || label.stringEndsWith("sh")) 1386 { 1387 postfix = "es"; 1388 } 1389 else if (auto before = label.stringEndsWith("y")) 1390 { 1391 if (!vowels.contain(label[$ - 2])) 1392 { 1393 postfix = "ies"; 1394 label = before; 1395 } 1396 } 1397 else if (auto before = label.stringEndsWith("is")) 1398 { 1399 postfix = "es"; 1400 label = before; 1401 } 1402 else if ("sxz".contain(label[$-1])) 1403 { 1404 postfix = "es"; // glasses 1405 } 1406 } 1407 return label ~ postfix; 1408 } 1409 1410 @("has functioning pluralize()") 1411 unittest 1412 { 1413 "dog".pluralize.shouldEqual("dogs"); 1414 "ash".pluralize.shouldEqual("ashes"); 1415 "day".pluralize.shouldEqual("days"); 1416 "entity".pluralize.shouldEqual("entities"); 1417 "thesis".pluralize.shouldEqual("theses"); 1418 "glass".pluralize.shouldEqual("glasses"); 1419 } 1420 1421 private string stringEndsWith(const string text, const string suffix) 1422 { 1423 import std.range : dropBack; 1424 import std.string : endsWith; 1425 1426 if (text.endsWith(suffix)) 1427 { 1428 return text.dropBack(suffix.length); 1429 } 1430 return null; 1431 } 1432 1433 @("has functioning stringEndsWith()") 1434 unittest 1435 { 1436 "".stringEndsWith("").shouldNotBeNull; 1437 "".stringEndsWith("x").shouldBeNull; 1438 "Hello".stringEndsWith("Hello").shouldNotBeNull; 1439 "Hello".stringEndsWith("Hello").shouldEqual(""); 1440 "Hello".stringEndsWith("lo").shouldEqual("Hel"); 1441 } 1442 1443 template hasOwnFunction(Aggregate, Super, string Name, Type) 1444 { 1445 import std.meta : AliasSeq, Filter; 1446 import std.traits : Unqual; 1447 enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1448 1449 alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name)); 1450 alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions); 1451 enum hasFunction = MatchingFunctions.length == 1; 1452 1453 alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name)); 1454 alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions); 1455 enum superHasFunction = SuperMatchingFunctions.length == 1; 1456 1457 static if (hasFunction) 1458 { 1459 static if (superHasFunction) 1460 { 1461 enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]); 1462 } 1463 else 1464 { 1465 enum hasOwnFunction = true; 1466 } 1467 } 1468 else 1469 { 1470 enum hasOwnFunction = false; 1471 } 1472 } 1473 1474 /** 1475 * Find qualified name of `T` including any containing types; not including containing functions or modules. 1476 */ 1477 public template typeName(T) 1478 { 1479 static if (__traits(compiles, __traits(parent, T))) 1480 { 1481 alias parent = Alias!(__traits(parent, T)); 1482 enum isSame = __traits(isSame, T, parent); 1483 1484 static if (!isSame && ( 1485 is(parent == struct) || is(parent == union) || is(parent == enum) || 1486 is(parent == class) || is(parent == interface))) 1487 { 1488 enum typeName = typeName!parent ~ "." ~ Unqual!T.stringof; 1489 } 1490 else 1491 { 1492 enum typeName = Unqual!T.stringof; 1493 } 1494 } 1495 else 1496 { 1497 enum typeName = Unqual!T.stringof; 1498 } 1499 } 1500 1501 public template hasOwnStringToString(Aggregate, Super) 1502 if (is(Aggregate: Object)) 1503 { 1504 enum hasOwnStringToString = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString)); 1505 } 1506 1507 public template hasOwnStringToString(Aggregate) 1508 if (is(Aggregate == struct)) 1509 { 1510 static if (is(typeof(Aggregate.init.toString()) == string)) 1511 { 1512 enum hasOwnStringToString = !isFromAliasThis!( 1513 Aggregate, "toString", typeof(StringToStringSample.toString)); 1514 } 1515 else 1516 { 1517 enum hasOwnStringToString = false; 1518 } 1519 } 1520 1521 public template hasOwnVoidToString(Aggregate, Super) 1522 if (is(Aggregate: Object)) 1523 { 1524 enum hasOwnVoidToString = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString)); 1525 } 1526 1527 public template hasOwnVoidToString(Aggregate) 1528 if (is(Aggregate == struct)) 1529 { 1530 static if (is(typeof(Aggregate.init.toString((void delegate(const(char)[])).init)) == void)) 1531 { 1532 enum hasOwnVoidToString = !isFromAliasThis!( 1533 Aggregate, "toString", typeof(VoidToStringSample.toString)); 1534 } 1535 else 1536 { 1537 enum hasOwnVoidToString = false; 1538 } 1539 } 1540 1541 private final abstract class StringToStringSample 1542 { 1543 override string toString(); 1544 } 1545 1546 private final abstract class VoidToStringSample 1547 { 1548 void toString(scope void delegate(const(char)[]) sink); 1549 } 1550 1551 public template isFromAliasThis(T, string member, Type) 1552 { 1553 import std.meta : AliasSeq, anySatisfy, Filter; 1554 1555 enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1556 1557 private template isFromThatAliasThis(string field) 1558 { 1559 alias aliasMembers = AliasSeq!(__traits(getOverloads, __traits(getMember, T.init, field), member)); 1560 alias ownMembers = AliasSeq!(__traits(getOverloads, T, member)); 1561 1562 enum bool isFromThatAliasThis = __traits(isSame, 1563 Filter!(FunctionMatchesType, aliasMembers), 1564 Filter!(FunctionMatchesType, ownMembers)); 1565 } 1566 1567 enum bool isFromAliasThis = anySatisfy!(isFromThatAliasThis, __traits(getAliasThis, T)); 1568 } 1569 1570 @("correctly recognizes the existence of string toString() in a class") 1571 unittest 1572 { 1573 class Class1 1574 { 1575 override string toString() { return null; } 1576 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1577 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1578 } 1579 1580 class Class2 1581 { 1582 override string toString() const { return null; } 1583 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1584 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1585 } 1586 1587 class Class3 1588 { 1589 void toString(scope void delegate(const(char)[]) sink) const { } 1590 override string toString() const { return null; } 1591 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1592 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1593 } 1594 1595 class Class4 1596 { 1597 void toString(scope void delegate(const(char)[]) sink) const { } 1598 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1599 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1600 } 1601 1602 class Class5 1603 { 1604 mixin(GenerateToString); 1605 } 1606 1607 class ChildClass1 : Class1 1608 { 1609 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1610 } 1611 1612 class ChildClass2 : Class2 1613 { 1614 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1615 } 1616 1617 class ChildClass3 : Class3 1618 { 1619 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1620 } 1621 1622 class ChildClass5 : Class5 1623 { 1624 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1625 } 1626 }