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 @("immutable struct with alias this of const toString") 732 unittest 733 { 734 struct Inner 735 { 736 string toString() const { return "Inner()"; } 737 } 738 739 immutable struct Outer 740 { 741 Inner inner; 742 743 alias inner this; 744 745 mixin(GenerateToString); 746 } 747 748 Outer().to!string.shouldEqual("Outer(Inner())"); 749 } 750 751 mixin template GenerateToStringTemplate() 752 { 753 754 // this is a separate function to reduce the 755 // "warning: unreachable code" spam that is falsely created from static foreach 756 private static generateToStringErrCheck() 757 { 758 if (!__ctfe) 759 { 760 return null; 761 } 762 763 import boilerplate.autostring : ToString, typeName; 764 import boilerplate.util : GenNormalMemberTuple; 765 import std.string : format; 766 767 bool udaIncludeSuper; 768 bool udaExcludeSuper; 769 770 foreach (uda; __traits(getAttributes, typeof(this))) 771 { 772 static if (is(typeof(uda) == ToString)) 773 { 774 switch (uda) 775 { 776 case ToString.IncludeSuper: udaIncludeSuper = true; break; 777 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 778 default: break; 779 } 780 } 781 } 782 783 if (udaIncludeSuper && udaExcludeSuper) 784 { 785 return format!(`static assert(false, ` ~ 786 `"Contradictory tags on '" ~ %(%s%) ~ "': IncludeSuper and ExcludeSuper");`) 787 ([typeName!(typeof(this))]); 788 } 789 790 mixin GenNormalMemberTuple!true; 791 792 foreach (member; NormalMemberTuple) 793 { 794 enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member))); 795 796 static if (error) 797 { 798 return format!error(member); 799 } 800 } 801 802 return ``; 803 } 804 805 private static generateToStringImpl() 806 { 807 if (!__ctfe) 808 { 809 return null; 810 } 811 812 import boilerplate.autostring : isFromAliasThis, isMemberUnlabeledByDefault, ToString, typeName; 813 import boilerplate.conditions : NonEmpty; 814 import boilerplate.util : GenNormalMemberTuple, udaIndex; 815 import std.meta : Alias; 816 import std.string : endsWith, format, split, startsWith, strip; 817 import std.traits : BaseClassesTuple, getUDAs, Unqual; 818 import std.typecons : Nullable; 819 820 // synchronized without lock contention is basically free, so always do it 821 // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 822 enum synchronize = false && is(typeof(this) == class); 823 824 const constExample = typeof(this).init; 825 auto normalExample = typeof(this).init; 826 827 enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString") 828 && is(typeof(normalExample.toString()) == string); 829 enum alreadyHaveUsableStringToString = alreadyHaveStringToString 830 && is(typeof(constExample.toString()) == string); 831 832 enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString") 833 && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void); 834 enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString 835 && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void); 836 837 enum isObject = is(typeof(this): Object); 838 839 static if (isObject) 840 { 841 enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super)); 842 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super)); 843 } 844 else 845 { 846 static if (alreadyHaveStringToString) 847 { 848 enum userDefinedStringToString = !isFromAliasThis!(typeof(this), "toString"); 849 } 850 else 851 { 852 enum userDefinedStringToString = false; 853 } 854 enum userDefinedVoidToString = alreadyHaveVoidToString; 855 } 856 857 static if (userDefinedStringToString && userDefinedVoidToString) 858 { 859 string result = ``; // Nothing to be done. 860 } 861 // if the user has defined their own string toString() in this aggregate: 862 else static if (userDefinedStringToString) 863 { 864 // just call it. 865 static if (alreadyHaveUsableStringToString) 866 { 867 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~ 868 ` sink(this.toString());` ~ 869 ` }`; 870 871 static if (isObject 872 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 873 { 874 result = `override ` ~ result; 875 } 876 } 877 else 878 { 879 string result = `static assert(false, "toString is not const in this class.");`; 880 } 881 } 882 // if the user has defined their own void toString() in this aggregate: 883 else 884 { 885 string result = null; 886 887 static if (!userDefinedVoidToString) 888 { 889 bool nakedMode; 890 bool udaIncludeSuper; 891 bool udaExcludeSuper; 892 893 foreach (uda; __traits(getAttributes, typeof(this))) 894 { 895 static if (is(typeof(uda) == ToStringEnum)) 896 { 897 switch (uda) 898 { 899 case ToString.Naked: nakedMode = true; break; 900 case ToString.IncludeSuper: udaIncludeSuper = true; break; 901 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 902 default: break; 903 } 904 } 905 } 906 907 string NamePlusOpenParen = typeName!(typeof(this)) ~ "("; 908 909 version(AutoStringDebug) 910 { 911 result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString); 912 } 913 914 static if (isObject && alreadyHaveVoidToString) result ~= `override `; 915 916 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {` 917 ~ `import boilerplate.autostring: ToStringHandler;` 918 ~ `import boilerplate.util: sinkWrite;` 919 ~ `import std.traits: getUDAs;`; 920 921 static if (synchronize) 922 { 923 result ~= `synchronized (this) { `; 924 } 925 926 if (!nakedMode) 927 { 928 result ~= format!`sink(%(%s%));`([NamePlusOpenParen]); 929 } 930 931 bool includeSuper = false; 932 933 static if (isObject) 934 { 935 if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString) 936 { 937 includeSuper = true; 938 } 939 } 940 941 if (udaIncludeSuper) 942 { 943 includeSuper = true; 944 } 945 else if (udaExcludeSuper) 946 { 947 includeSuper = false; 948 } 949 950 static if (isObject) 951 { 952 if (includeSuper) 953 { 954 static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString) 955 { 956 return `static assert(false, ` 957 ~ `"cannot include super class in GenerateToString: ` 958 ~ `parent class has no usable toString!");`; 959 } 960 else { 961 static if (alreadyHaveUsableVoidToString) 962 { 963 result ~= `super.toString(sink);`; 964 } 965 else 966 { 967 result ~= `sink(super.toString());`; 968 } 969 result ~= `bool comma = true;`; 970 } 971 } 972 else 973 { 974 result ~= `bool comma = false;`; 975 } 976 } 977 else 978 { 979 result ~= `bool comma = false;`; 980 } 981 982 result ~= `{`; 983 984 mixin GenNormalMemberTuple!(true); 985 986 foreach (member; NormalMemberTuple) 987 { 988 mixin("alias symbol = typeof(this)." ~ member ~ ";"); 989 990 enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1; 991 enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1; 992 enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1; 993 enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1; 994 enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1; 995 enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1; 996 enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1; 997 998 // see std.traits.isFunction!() 999 static if ( 1000 is(symbol == function) 1001 || is(typeof(symbol) == function) 1002 || (is(typeof(&symbol) U : U*) && is(U == function))) 1003 { 1004 enum isFunction = true; 1005 } 1006 else 1007 { 1008 enum isFunction = false; 1009 } 1010 1011 enum includeOverride = udaInclude || udaOptional; 1012 1013 enum includeMember = (!isFunction || includeOverride) && !udaExclude; 1014 1015 static if (includeMember) 1016 { 1017 string memberName = member; 1018 1019 if (memberName.endsWith("_")) 1020 { 1021 memberName = memberName[0 .. $ - 1]; 1022 } 1023 1024 bool labeled = true; 1025 1026 static if (udaUnlabeled) 1027 { 1028 labeled = false; 1029 } 1030 1031 if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty)) 1032 { 1033 labeled = false; 1034 } 1035 1036 static if (udaLabeled) 1037 { 1038 labeled = true; 1039 } 1040 1041 string membervalue = `this.` ~ member; 1042 1043 bool escapeStrings = true; 1044 1045 static if (udaToStringHandler) 1046 { 1047 alias Handlers = getUDAs!(symbol, ToStringHandler); 1048 1049 static assert(Handlers.length == 1); 1050 1051 static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init))) 1052 { 1053 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(` 1054 ~ membervalue 1055 ~ `)`; 1056 1057 escapeStrings = false; 1058 } 1059 else 1060 { 1061 return `static assert(false, "cannot determine how to call ToStringHandler");`; 1062 } 1063 } 1064 1065 string readMemberValue = membervalue; 1066 string conditionalWritestmt; // formatted with sink.sinkWrite(... readMemberValue ... ) 1067 1068 static if (udaOptional) 1069 { 1070 import std.array : empty; 1071 1072 enum optionalIndex = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)); 1073 alias optionalUda = Alias!(__traits(getAttributes, symbol)[optionalIndex]); 1074 1075 static if (is(optionalUda == struct)) 1076 { 1077 conditionalWritestmt = format!q{ 1078 if (__traits(getAttributes, %s)[%s].condition(%s)) { %%s } 1079 } (membervalue, optionalIndex, membervalue); 1080 } 1081 else static if (__traits(compiles, typeof(symbol).init.isNull)) 1082 { 1083 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1084 (membervalue); 1085 1086 static if (is(typeof(symbol) : Nullable!T, T)) 1087 { 1088 readMemberValue = membervalue ~ ".get"; 1089 } 1090 } 1091 else static if (__traits(compiles, typeof(symbol).init.empty)) 1092 { 1093 conditionalWritestmt = format!q{import std.array : empty; if (!%s.empty) { %%s }} 1094 (membervalue); 1095 } 1096 else static if (__traits(compiles, typeof(symbol).init !is null)) 1097 { 1098 conditionalWritestmt = format!q{if (%s !is null) { %%s }} 1099 (membervalue); 1100 } 1101 else static if (__traits(compiles, typeof(symbol).init != 0)) 1102 { 1103 conditionalWritestmt = format!q{if (%s != 0) { %%s }} 1104 (membervalue); 1105 } 1106 else static if (__traits(compiles, { if (typeof(symbol).init) { } })) 1107 { 1108 conditionalWritestmt = format!q{if (%s) { %%s }} 1109 (membervalue); 1110 } 1111 else 1112 { 1113 return format!(`static assert(false, ` 1114 ~ `"don't know how to figure out whether %s is present.");`) 1115 (member); 1116 } 1117 } 1118 else 1119 { 1120 // Nullables (without handler, that aren't force-included) fall back to optional 1121 static if (!udaToStringHandler && !udaInclude && 1122 __traits(compiles, typeof(symbol).init.isNull)) 1123 { 1124 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1125 (membervalue); 1126 1127 static if (is(typeof(symbol) : Nullable!T, T)) 1128 { 1129 readMemberValue = membervalue ~ ".get"; 1130 } 1131 } 1132 else 1133 { 1134 conditionalWritestmt = q{ %s }; 1135 } 1136 } 1137 1138 string writestmt; 1139 1140 if (labeled) 1141 { 1142 writestmt = format!`sink.sinkWrite(comma, %s, "%s=%%s", %s);` 1143 (escapeStrings, memberName, readMemberValue); 1144 } 1145 else 1146 { 1147 writestmt = format!`sink.sinkWrite(comma, %s, "%%s", %s);` 1148 (escapeStrings, readMemberValue); 1149 } 1150 1151 result ~= format(conditionalWritestmt, writestmt); 1152 } 1153 } 1154 1155 result ~= `} `; 1156 1157 if (!nakedMode) 1158 { 1159 result ~= `sink(")");`; 1160 } 1161 1162 static if (synchronize) 1163 { 1164 result ~= `} `; 1165 } 1166 1167 result ~= `} `; 1168 } 1169 1170 // generate fallback string toString() 1171 // that calls, specifically, *our own* toString impl. 1172 // (this is important to break cycles when a subclass implements a toString that calls super.toString) 1173 static if (isObject) 1174 { 1175 result ~= `override `; 1176 } 1177 1178 result ~= `public string toString() const {` 1179 ~ `string result;` 1180 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });` 1181 ~ `return result;` 1182 ~ `}`; 1183 } 1184 return result; 1185 } 1186 } 1187 1188 template checkAttributeConsistency(Attributes...) 1189 { 1190 enum checkAttributeConsistency = checkAttributeHelper(); 1191 1192 private string checkAttributeHelper() 1193 { 1194 if (!__ctfe) 1195 { 1196 return null; 1197 } 1198 1199 import std.string : format; 1200 1201 bool include, exclude, optional, labeled, unlabeled; 1202 1203 foreach (uda; Attributes) 1204 { 1205 static if (is(typeof(uda) == ToStringEnum)) 1206 { 1207 switch (uda) 1208 { 1209 case ToString.Include: include = true; break; 1210 case ToString.Exclude: exclude = true; break; 1211 case ToString.Labeled: labeled = true; break; 1212 case ToString.Unlabeled: unlabeled = true; break; 1213 default: break; 1214 } 1215 } 1216 else static if (is(uda == struct) && __traits(isSame, uda, ToString.Optional)) 1217 { 1218 optional = true; 1219 } 1220 } 1221 1222 if (include && exclude) 1223 { 1224 return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`; 1225 } 1226 1227 if (include && optional) 1228 { 1229 return `static assert(false, "Redundant tags on '%s': Optional implies Include");`; 1230 } 1231 1232 if (exclude && optional) 1233 { 1234 return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`; 1235 } 1236 1237 if (labeled && unlabeled) 1238 { 1239 return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`; 1240 } 1241 1242 return null; 1243 } 1244 } 1245 1246 struct ToStringHandler(alias Handler_) 1247 { 1248 alias Handler = Handler_; 1249 } 1250 1251 enum ToStringEnum 1252 { 1253 // these go on the class 1254 Naked, 1255 IncludeSuper, 1256 ExcludeSuper, 1257 1258 // these go on the field/method 1259 Unlabeled, 1260 Labeled, 1261 Exclude, 1262 Include, 1263 } 1264 1265 struct ToString 1266 { 1267 static foreach (name; __traits(allMembers, ToStringEnum)) 1268 { 1269 mixin(format!q{enum %s = ToStringEnum.%s;}(name, name)); 1270 } 1271 1272 static struct Optional(alias condition_) 1273 { 1274 alias condition = condition_; 1275 } 1276 } 1277 1278 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty) 1279 { 1280 import std.datetime : SysTime; 1281 import std.range.primitives : ElementType, isInputRange; 1282 import std.typecons : BitFlags; 1283 1284 field = field.toLower; 1285 1286 static if (isInputRange!Type) 1287 { 1288 alias BaseType = ElementType!Type; 1289 1290 if (field == BaseType.stringof.toLower.pluralize && attribNonEmpty) 1291 { 1292 return true; 1293 } 1294 } 1295 else static if (is(Type: const BitFlags!BaseType, BaseType)) 1296 { 1297 if (field == BaseType.stringof.toLower.pluralize) 1298 { 1299 return true; 1300 } 1301 } 1302 1303 return field == Type.stringof.toLower 1304 || (field == "time" && is(Type == SysTime)) 1305 || (field == "id" && is(typeof(Type.toString))); 1306 } 1307 1308 private string toLower(string text) 1309 { 1310 import std.string : stdToLower = toLower; 1311 1312 string result = null; 1313 1314 foreach (ub; cast(immutable(ubyte)[]) text) 1315 { 1316 if (ub >= 0x80) // utf-8, non-ascii 1317 { 1318 return text.stdToLower; 1319 } 1320 if (ub >= 'A' && ub <= 'Z') 1321 { 1322 result ~= cast(char) (ub + ('a' - 'A')); 1323 } 1324 else 1325 { 1326 result ~= cast(char) ub; 1327 } 1328 } 1329 return result; 1330 } 1331 1332 // http://code.activestate.com/recipes/82102/ 1333 private string pluralize(string label) 1334 { 1335 import std.algorithm.searching : contain = canFind; 1336 1337 string postfix = "s"; 1338 if (label.length > 2) 1339 { 1340 enum vowels = "aeiou"; 1341 1342 if (label.stringEndsWith("ch") || label.stringEndsWith("sh")) 1343 { 1344 postfix = "es"; 1345 } 1346 else if (auto before = label.stringEndsWith("y")) 1347 { 1348 if (!vowels.contain(label[$ - 2])) 1349 { 1350 postfix = "ies"; 1351 label = before; 1352 } 1353 } 1354 else if (auto before = label.stringEndsWith("is")) 1355 { 1356 postfix = "es"; 1357 label = before; 1358 } 1359 else if ("sxz".contain(label[$-1])) 1360 { 1361 postfix = "es"; // glasses 1362 } 1363 } 1364 return label ~ postfix; 1365 } 1366 1367 @("has functioning pluralize()") 1368 unittest 1369 { 1370 "dog".pluralize.shouldEqual("dogs"); 1371 "ash".pluralize.shouldEqual("ashes"); 1372 "day".pluralize.shouldEqual("days"); 1373 "entity".pluralize.shouldEqual("entities"); 1374 "thesis".pluralize.shouldEqual("theses"); 1375 "glass".pluralize.shouldEqual("glasses"); 1376 } 1377 1378 private string stringEndsWith(const string text, const string suffix) 1379 { 1380 import std.range : dropBack; 1381 import std.string : endsWith; 1382 1383 if (text.endsWith(suffix)) 1384 { 1385 return text.dropBack(suffix.length); 1386 } 1387 return null; 1388 } 1389 1390 @("has functioning stringEndsWith()") 1391 unittest 1392 { 1393 "".stringEndsWith("").shouldNotBeNull; 1394 "".stringEndsWith("x").shouldBeNull; 1395 "Hello".stringEndsWith("Hello").shouldNotBeNull; 1396 "Hello".stringEndsWith("Hello").shouldEqual(""); 1397 "Hello".stringEndsWith("lo").shouldEqual("Hel"); 1398 } 1399 1400 template hasOwnFunction(Aggregate, Super, string Name, Type) 1401 { 1402 import std.meta : AliasSeq, Filter; 1403 import std.traits : Unqual; 1404 enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1405 1406 alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name)); 1407 alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions); 1408 enum hasFunction = MatchingFunctions.length == 1; 1409 1410 alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name)); 1411 alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions); 1412 enum superHasFunction = SuperMatchingFunctions.length == 1; 1413 1414 static if (hasFunction) 1415 { 1416 static if (superHasFunction) 1417 { 1418 enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]); 1419 } 1420 else 1421 { 1422 enum hasOwnFunction = true; 1423 } 1424 } 1425 else 1426 { 1427 enum hasOwnFunction = false; 1428 } 1429 } 1430 1431 /** 1432 * Find qualified name of `T` including any containing types; not including containing functions or modules. 1433 */ 1434 public template typeName(T) 1435 { 1436 static if (__traits(compiles, __traits(parent, T))) 1437 { 1438 alias parent = Alias!(__traits(parent, T)); 1439 enum isSame = __traits(isSame, T, parent); 1440 1441 static if (!isSame && ( 1442 is(parent == struct) || is(parent == union) || is(parent == enum) || 1443 is(parent == class) || is(parent == interface))) 1444 { 1445 enum typeName = typeName!parent ~ "." ~ Unqual!T.stringof; 1446 } 1447 else 1448 { 1449 enum typeName = Unqual!T.stringof; 1450 } 1451 } 1452 else 1453 { 1454 enum typeName = Unqual!T.stringof; 1455 } 1456 } 1457 1458 private final abstract class StringToStringSample { 1459 override string toString(); 1460 } 1461 1462 private final abstract class VoidToStringSample { 1463 void toString(scope void delegate(const(char)[]) sink); 1464 } 1465 1466 enum hasOwnStringToString(Aggregate, Super) 1467 = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString)); 1468 1469 enum hasOwnVoidToString(Aggregate, Super) 1470 = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString)); 1471 1472 public template isFromAliasThis(T, string member) 1473 { 1474 import std.meta : anySatisfy; 1475 1476 private template isFromThatAliasThis(string field) 1477 { 1478 enum bool isFromThatAliasThis = __traits(isSame, 1479 __traits(getMember, __traits(getMember, T.init, field), member), 1480 __traits(getMember, T, member)); 1481 } 1482 1483 enum bool isFromAliasThis = anySatisfy!(isFromThatAliasThis, __traits(getAliasThis, T)); 1484 } 1485 1486 @("correctly recognizes the existence of string toString() in a class") 1487 unittest 1488 { 1489 class Class1 1490 { 1491 override string toString() { return null; } 1492 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1493 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1494 } 1495 1496 class Class2 1497 { 1498 override string toString() const { return null; } 1499 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1500 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1501 } 1502 1503 class Class3 1504 { 1505 void toString(scope void delegate(const(char)[]) sink) const { } 1506 override string toString() const { return null; } 1507 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1508 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1509 } 1510 1511 class Class4 1512 { 1513 void toString(scope void delegate(const(char)[]) sink) const { } 1514 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1515 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1516 } 1517 1518 class Class5 1519 { 1520 mixin(GenerateToString); 1521 } 1522 1523 class ChildClass1 : Class1 1524 { 1525 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1526 } 1527 1528 class ChildClass2 : Class2 1529 { 1530 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1531 } 1532 1533 class ChildClass3 : Class3 1534 { 1535 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1536 } 1537 1538 class ChildClass5 : Class5 1539 { 1540 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1541 } 1542 }