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