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