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