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