1 module boilerplate.autostring; 2 3 version(unittest) 4 { 5 import std.conv : to; 6 import std.datetime : SysTime; 7 import unit_threaded.should; 8 } 9 10 /++ 11 GenerateToString is a mixin string that automatically generates toString functions, 12 both sink-based and classic, customizable with UDA annotations on classes, members and functions. 13 +/ 14 public enum string GenerateToString = ` 15 import boilerplate.autostring : GenerateToStringTemplate; 16 mixin GenerateToStringTemplate; 17 mixin(typeof(this).generateToStringErrCheck()); 18 mixin(typeof(this).generateToStringImpl()); 19 `; 20 21 /++ 22 When used with objects, toString methods of type string toString() are also created. 23 +/ 24 @("generates legacy toString on objects") 25 unittest 26 { 27 class Class 28 { 29 mixin(GenerateToString); 30 } 31 32 (new Class).to!string.shouldEqual("Class()"); 33 (new Class).toString.shouldEqual("Class()"); 34 } 35 36 /++ 37 A trailing underline in member names is removed when labeling. 38 +/ 39 @("removes trailing underline") 40 unittest 41 { 42 struct Struct 43 { 44 int a_; 45 mixin(GenerateToString); 46 } 47 48 Struct.init.to!string.shouldEqual("Struct(a=0)"); 49 } 50 51 /++ 52 The `@(ToString.Exclude)` tag can be used to exclude a member. 53 +/ 54 @("can exclude a member") 55 unittest 56 { 57 struct Struct 58 { 59 @(ToString.Exclude) 60 int a; 61 mixin(GenerateToString); 62 } 63 64 Struct.init.to!string.shouldEqual("Struct()"); 65 } 66 67 /++ 68 The `@(ToString.Optional)` tag can be used to include a member only if it's in some form "present". 69 This means non-empty for arrays, non-null for objects, non-zero for ints. 70 +/ 71 @("can optionally exclude member") 72 unittest 73 { 74 class Class 75 { 76 mixin(GenerateToString); 77 } 78 79 struct Struct 80 { 81 @(ToString.Optional) 82 int a; 83 @(ToString.Optional) 84 string s; 85 @(ToString.Optional) 86 Class obj; 87 mixin(GenerateToString); 88 } 89 90 Struct.init.to!string.shouldEqual("Struct()"); 91 Struct(2, "hi", new Class).to!string.shouldEqual("Struct(a=2, s=hi, obj=Class())"); 92 Struct(0, "", null).to!string.shouldEqual("Struct()"); 93 } 94 95 /++ 96 The `@(ToString.Include)` tag can be used to explicitly include a member. 97 This is intended to be used on property methods. 98 +/ 99 @("can include a method") 100 unittest 101 { 102 struct Struct 103 { 104 @(ToString.Include) 105 int foo() const { return 5; } 106 mixin(GenerateToString); 107 } 108 109 Struct.init.to!string.shouldEqual("Struct(foo=5)"); 110 } 111 112 /++ 113 The `@(ToString.Unlabeled)` tag will omit a field's name. 114 +/ 115 @("can omit names") 116 unittest 117 { 118 struct Struct 119 { 120 @(ToString.Unlabeled) 121 int a; 122 mixin(GenerateToString); 123 } 124 125 Struct.init.to!string.shouldEqual("Struct(0)"); 126 } 127 128 /++ 129 Parent class `toString()` methods are included automatically as the first entry, except if the parent class is `Object`. 130 +/ 131 @("can be used in both parent and child class") 132 unittest 133 { 134 class ParentClass { mixin(GenerateToString); } 135 136 class ChildClass : ParentClass { mixin(GenerateToString); } 137 138 (new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())"); 139 } 140 141 @("invokes manually implemented parent toString") 142 unittest 143 { 144 class ParentClass 145 { 146 override string toString() const 147 { 148 return "Some string"; 149 } 150 } 151 152 class ChildClass : ParentClass { mixin(GenerateToString); } 153 154 (new ChildClass).to!string.shouldEqual("ChildClass(Some string)"); 155 } 156 157 @("can partially override toString in child class") 158 unittest 159 { 160 class ParentClass 161 { 162 mixin(GenerateToString); 163 } 164 165 class ChildClass : ParentClass 166 { 167 override string toString() const 168 { 169 return "Some string"; 170 } 171 172 mixin(GenerateToString); 173 } 174 175 (new ChildClass).to!string.shouldEqual("Some string"); 176 } 177 178 @("invokes manually implemented string toString in same class") 179 unittest 180 { 181 class Class 182 { 183 override string toString() const 184 { 185 return "Some string"; 186 } 187 188 mixin(GenerateToString); 189 } 190 191 (new Class).to!string.shouldEqual("Some string"); 192 } 193 194 @("invokes manually implemented void toString in same class") 195 unittest 196 { 197 class Class 198 { 199 void toString(scope void delegate(const(char)[]) sink) const 200 { 201 sink("Some string"); 202 } 203 204 mixin(GenerateToString); 205 } 206 207 (new Class).to!string.shouldEqual("Some string"); 208 } 209 210 /++ 211 Inclusion of parent class `toString()` can be prevented using `@(ToString.ExcludeSuper)`. 212 +/ 213 @("can suppress parent class toString()") 214 unittest 215 { 216 class ParentClass { } 217 218 @(ToString.ExcludeSuper) 219 class ChildClass : ParentClass { mixin(GenerateToString); } 220 221 (new ChildClass).to!string.shouldEqual("ChildClass()"); 222 } 223 224 /++ 225 The `@(ToString.Naked)` tag will omit the name of the type and parentheses. 226 +/ 227 @("can omit the type name") 228 unittest 229 { 230 @(ToString.Naked) 231 struct Struct 232 { 233 int a; 234 mixin(GenerateToString); 235 } 236 237 Struct.init.to!string.shouldEqual("a=0"); 238 } 239 240 /++ 241 Fields with the same name (ignoring capitalization) as their type, are unlabeled by default. 242 +/ 243 @("does not label fields with the same name as the type") 244 unittest 245 { 246 struct Struct1 { mixin(GenerateToString); } 247 248 struct Struct2 249 { 250 Struct1 struct1; 251 mixin(GenerateToString); 252 } 253 254 Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 255 } 256 257 @("does not label fields with the same name as the type, even if they're const") 258 unittest 259 { 260 struct Struct1 { mixin(GenerateToString); } 261 262 struct Struct2 263 { 264 const Struct1 struct1; 265 mixin(GenerateToString); 266 } 267 268 Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 269 } 270 271 /++ 272 This behavior can be prevented by explicitly tagging the field with `@(ToString.Labeled)`. 273 +/ 274 @("does label fields tagged as labeled") 275 unittest 276 { 277 struct Struct1 { mixin(GenerateToString); } 278 279 struct Struct2 280 { 281 @(ToString.Labeled) 282 Struct1 struct1; 283 mixin(GenerateToString); 284 } 285 286 Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())"); 287 } 288 289 /++ 290 Fields of type 'SysTime' and name 'time' are unlabeled by default. 291 +/ 292 @("does not label SysTime time field correctly") 293 unittest 294 { 295 struct Struct { SysTime time; mixin(GenerateToString); } 296 297 Struct strct; 298 strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z"); 299 300 // see unittest/config/string.d 301 strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)"); 302 } 303 304 /++ 305 Fields named 'id' are unlabeled only if they define their own toString(). 306 +/ 307 @("does not label id fields with toString()") 308 unittest 309 { 310 struct IdType 311 { 312 string toString() const { return "ID"; } 313 } 314 315 struct Struct 316 { 317 IdType id; 318 mixin(GenerateToString); 319 } 320 321 Struct.init.to!string.shouldEqual("Struct(ID)"); 322 } 323 324 /++ 325 Otherwise, they are labeled as normal. 326 +/ 327 @("labels id fields without toString") 328 unittest 329 { 330 struct Struct 331 { 332 int id; 333 mixin(GenerateToString); 334 } 335 336 Struct.init.to!string.shouldEqual("Struct(id=0)"); 337 } 338 339 /++ 340 Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default. 341 +/ 342 @("does not label fields with the same name as the type") 343 unittest 344 { 345 struct SomeValue { mixin(GenerateToString); } 346 struct Entity { mixin(GenerateToString); } 347 struct Day { mixin(GenerateToString); } 348 349 struct Struct 350 { 351 SomeValue[] someValues; 352 Entity[] entities; 353 Day[] days; 354 mixin(GenerateToString); 355 } 356 357 Struct.init.to!string.shouldEqual("Struct([], [], [])"); 358 } 359 360 /++ 361 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue. 362 +/ 363 @("does not collide with accessors") 364 unittest 365 { 366 struct Struct 367 { 368 import boilerplate.accessors : GenerateFieldAccessors, ConstRead; 369 370 @ConstRead 371 private int a_; 372 373 mixin(GenerateFieldAccessors); 374 375 mixin(GenerateToString); 376 } 377 378 Struct.init.to!string.shouldEqual("Struct(a=0)"); 379 } 380 381 @("supports child classes of abstract classes") 382 unittest 383 { 384 static abstract class ParentClass 385 { 386 } 387 class ChildClass : ParentClass 388 { 389 mixin(GenerateToString); 390 } 391 } 392 393 @("supports custom toString handlers") 394 unittest 395 { 396 struct Struct 397 { 398 @ToStringHandler!(i => i ? "yes" : "no") 399 int i; 400 401 mixin(GenerateToString); 402 } 403 404 Struct.init.to!string.shouldEqual("Struct(i=no)"); 405 } 406 407 @("passes nullable unchanged to custom toString handlers") 408 unittest 409 { 410 import std.typecons : Nullable; 411 412 struct Struct 413 { 414 @ToStringHandler!(ni => ni.isNull ? "no" : "yes") 415 Nullable!int ni; 416 417 mixin(GenerateToString); 418 } 419 420 Struct.init.to!string.shouldEqual("Struct(ni=no)"); 421 } 422 423 mixin template GenerateToStringTemplate() 424 { 425 426 // this is a separate function to reduce the 427 // "warning: unreachable code" spam that is falsely created from static foreach 428 private static generateToStringErrCheck() 429 { 430 if (!__ctfe) 431 { 432 return null; 433 } 434 435 import boilerplate.autostring : ToString; 436 import boilerplate.util : GenNormalMemberTuple; 437 import std.string : format; 438 439 bool udaIncludeSuper; 440 bool udaExcludeSuper; 441 442 foreach (uda; __traits(getAttributes, typeof(this))) 443 { 444 static if (is(typeof(uda) == ToString)) 445 { 446 switch (uda) 447 { 448 case ToString.IncludeSuper: udaIncludeSuper = true; break; 449 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 450 default: break; 451 } 452 } 453 } 454 455 if (udaIncludeSuper && udaExcludeSuper) 456 { 457 return format!`static assert(false, "Contradictory tags on '%s': IncludeSuper and ExcludeSuper");` 458 (typeof(this).stringof); 459 } 460 461 mixin GenNormalMemberTuple!true; 462 463 foreach (member; NormalMemberTuple) 464 { 465 enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member))); 466 467 static if (error) 468 { 469 return format!error(member); 470 } 471 } 472 473 return ``; 474 } 475 476 private static generateToStringImpl() 477 { 478 if (!__ctfe) 479 { 480 return null; 481 } 482 483 import std.string : endsWith, format, split, startsWith, strip; 484 import std.traits : BaseClassesTuple, Unqual, getUDAs; 485 import boilerplate.autostring : ToString, isMemberUnlabeledByDefault; 486 import boilerplate.util : GenNormalMemberTuple, udaIndex; 487 488 // synchronized without lock contention is basically free, so always do it 489 // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 490 enum synchronize = false && is(typeof(this) == class); 491 492 const constExample = typeof(this).init; 493 auto normalExample = typeof(this).init; 494 495 enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString") 496 && is(typeof(normalExample.toString()) == string); 497 enum alreadyHaveUsableStringToString = alreadyHaveStringToString 498 && is(typeof(constExample.toString()) == string); 499 500 enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString") 501 && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void); 502 enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString 503 && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void); 504 505 enum isObject = is(typeof(this): Object); 506 507 static if (isObject) 508 { 509 enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super)); 510 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super)); 511 } 512 else 513 { 514 enum userDefinedStringToString = alreadyHaveStringToString; 515 enum userDefinedVoidToString = alreadyHaveVoidToString; 516 } 517 518 static if (userDefinedStringToString && userDefinedVoidToString) 519 { 520 string result = ``; // Nothing to be done. 521 } 522 // if the user has defined their own string toString() in this aggregate: 523 else static if (userDefinedStringToString) 524 { 525 // just call it. 526 static if (alreadyHaveUsableStringToString) 527 { 528 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~ 529 ` sink(this.toString());` ~ 530 ` }`; 531 532 static if (isObject 533 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 534 { 535 result = `override ` ~ result; 536 } 537 } 538 else 539 { 540 string result = `static assert(false, "toString is not const in this class.");`; 541 } 542 } 543 // if the user has defined their own void toString() in this aggregate: 544 else 545 { 546 string result = null; 547 548 static if (!userDefinedVoidToString) 549 { 550 bool nakedMode; 551 bool udaIncludeSuper; 552 bool udaExcludeSuper; 553 554 foreach (uda; __traits(getAttributes, typeof(this))) 555 { 556 static if (is(typeof(uda) == ToString)) 557 { 558 switch (uda) 559 { 560 case ToString.Naked: nakedMode = true; break; 561 case ToString.IncludeSuper: udaIncludeSuper = true; break; 562 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 563 default: break; 564 } 565 } 566 } 567 568 string NamePlusOpenParen = Unqual!(typeof(this)).stringof ~ "("; 569 570 version(AutoStringDebug) 571 { 572 result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString); 573 } 574 575 static if (isObject && alreadyHaveVoidToString) result ~= `override `; 576 577 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {` 578 ~ `import boilerplate.autostring: ToStringHandler;` 579 ~ `import boilerplate.util: sinkWrite;` 580 ~ `import std.traits: getUDAs;`; 581 582 static if (synchronize) 583 { 584 result ~= `synchronized (this) { `; 585 } 586 587 if (!nakedMode) 588 { 589 result ~= `sink("` ~ NamePlusOpenParen ~ `");`; 590 } 591 592 bool includeSuper = false; 593 594 static if (isObject) 595 { 596 if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString) 597 { 598 includeSuper = true; 599 } 600 } 601 602 if (udaIncludeSuper) 603 { 604 includeSuper = true; 605 } 606 else if (udaExcludeSuper) 607 { 608 includeSuper = false; 609 } 610 611 static if (isObject) 612 { 613 if (includeSuper) 614 { 615 static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString) 616 { 617 return `static assert(false, ` 618 ~ `"cannot include super class in GenerateToString: ` 619 ~ `parent class has no usable toString!");`; 620 } 621 else { 622 static if (alreadyHaveUsableVoidToString) 623 { 624 result ~= `super.toString(sink);`; 625 } 626 else 627 { 628 result ~= `sink(super.toString());`; 629 } 630 result ~= `bool comma = true;`; 631 } 632 } 633 else 634 { 635 result ~= `bool comma = false;`; 636 } 637 } 638 else 639 { 640 result ~= `bool comma = false;`; 641 } 642 643 result ~= `{`; 644 645 mixin GenNormalMemberTuple!(true); 646 647 foreach (member; NormalMemberTuple) 648 { 649 mixin("alias symbol = this." ~ member ~ ";"); 650 651 enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1; 652 enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1; 653 enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1; 654 enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1; 655 enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1; 656 enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1; 657 658 // see std.traits.isFunction!() 659 static if (is(symbol == function) || is(typeof(symbol) == function) 660 || is(typeof(&symbol) U : U*) && is(U == function)) 661 { 662 enum isFunction = true; 663 } 664 else 665 { 666 enum isFunction = false; 667 } 668 669 enum includeOverride = udaInclude || udaOptional; 670 671 enum includeMember = (!isFunction || includeOverride) && !udaExclude; 672 673 static if (includeMember) 674 { 675 string memberName = member; 676 677 if (memberName.endsWith("_")) 678 { 679 memberName = memberName[0 .. $ - 1]; 680 } 681 682 bool labeled = true; 683 684 static if (udaUnlabeled) 685 { 686 labeled = false; 687 } 688 689 if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName)) 690 { 691 labeled = false; 692 } 693 694 static if (udaLabeled) 695 { 696 labeled = true; 697 } 698 699 string membervalue = `this.` ~ member; 700 701 static if (udaToStringHandler) 702 { 703 alias Handlers = getUDAs!(symbol, ToStringHandler); 704 705 static assert(Handlers.length == 1); 706 707 static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init))) 708 { 709 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(` 710 ~ membervalue 711 ~ `)`; 712 } 713 else 714 { 715 return `static assert(false, "cannot determine how to call ToStringHandler");`; 716 } 717 } 718 719 string writestmt; 720 721 if (labeled) 722 { 723 writestmt = format!`sink.sinkWrite(comma, "%s=%%s", %s);` 724 (memberName, membervalue); 725 } 726 else 727 { 728 writestmt = format!`sink.sinkWrite(comma, "%%s", %s);`(membervalue); 729 } 730 731 static if (udaOptional) 732 { 733 import std.array : empty; 734 735 static if (__traits(compiles, typeof(symbol).init.empty)) 736 { 737 result ~= format!`import std.array : empty; if (!%s.empty) { %s }` 738 (membervalue, writestmt); 739 } 740 else static if (__traits(compiles, typeof(symbol).init !is null)) 741 { 742 result ~= format!`if (%s !is null) { %s }` 743 (membervalue, writestmt); 744 } 745 else static if (__traits(compiles, typeof(symbol).init != 0)) 746 { 747 result ~= format!`if (%s != 0) { %s }` 748 (membervalue, writestmt); 749 } 750 else 751 { 752 return format!(`static assert(false, ` 753 ~ `"don't know how to figure out whether %s is present.");`) 754 (member); 755 } 756 } 757 else 758 { 759 result ~= writestmt; 760 } 761 } 762 } 763 764 result ~= `} `; 765 766 if (!nakedMode) 767 { 768 result ~= `sink(")");`; 769 } 770 771 static if (synchronize) 772 { 773 result ~= `} `; 774 } 775 776 result ~= `} `; 777 } 778 779 // generate fallback string toString() 780 // that calls, specifically, *our own* toString impl. 781 // (this is important to break cycles when a subclass implements a toString that calls super.toString) 782 static if (isObject) 783 { 784 result ~= `override `; 785 } 786 787 result ~= `public string toString() const {` 788 ~ `string result;` 789 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });` 790 ~ `return result;` 791 ~ `}`; 792 } 793 return result; 794 } 795 } 796 797 template checkAttributeConsistency(Attributes...) 798 { 799 enum checkAttributeConsistency = checkAttributeHelper(); 800 801 private string checkAttributeHelper() 802 { 803 if (!__ctfe) 804 { 805 return null; 806 } 807 808 import std.string : format; 809 810 bool include, exclude, optional, labeled, unlabeled; 811 812 foreach (uda; Attributes) 813 { 814 static if (is(typeof(uda) == ToString)) 815 { 816 switch (uda) 817 { 818 case ToString.Include: include = true; break; 819 case ToString.Exclude: exclude = true; break; 820 case ToString.Optional: optional = true; break; 821 case ToString.Labeled: labeled = true; break; 822 case ToString.Unlabeled: unlabeled = true; break; 823 default: break; 824 } 825 } 826 } 827 828 if (include && exclude) 829 { 830 return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`; 831 } 832 833 if (include && optional) 834 { 835 return `static assert(false, "Redundant tags on '%s': Optional implies Include");`; 836 } 837 838 if (exclude && optional) 839 { 840 return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`; 841 } 842 843 if (labeled && unlabeled) 844 { 845 return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`; 846 } 847 848 return null; 849 } 850 } 851 852 struct ToStringHandler(alias Handler_) 853 { 854 alias Handler = Handler_; 855 } 856 857 enum ToString 858 { 859 // these go on the class 860 Naked, 861 IncludeSuper, 862 ExcludeSuper, 863 864 // these go on the field/method 865 Unlabeled, 866 Labeled, 867 Exclude, 868 Include, 869 Optional, 870 } 871 872 public bool isMemberUnlabeledByDefault(Type)(string field) 873 { 874 import std.string : toLower; 875 import std.range.primitives : ElementType, isInputRange; 876 877 static if (isInputRange!Type) 878 { 879 alias BaseType = ElementType!Type; 880 881 if (field.toLower == BaseType.stringof.toLower.pluralize) 882 { 883 return true; 884 } 885 } 886 887 return field.toLower == Type.stringof.toLower 888 || field.toLower == "time" && Type.stringof == "SysTime" 889 || field.toLower == "id" && is(typeof(Type.toString)); 890 } 891 892 // http://code.activestate.com/recipes/82102/ 893 private string pluralize(string label) 894 { 895 import std.algorithm.searching : contain = canFind; 896 897 string postfix = "s"; 898 if (label.length > 2) 899 { 900 enum vowels = "aeiou"; 901 902 if (label.stringEndsWith("ch") || label.stringEndsWith("sh")) 903 { 904 postfix = "es"; 905 } 906 else if (auto before = label.stringEndsWith("y")) 907 { 908 if (!vowels.contain(label[$ - 2])) 909 { 910 postfix = "ies"; 911 label = before; 912 } 913 } 914 else if (auto before = label.stringEndsWith("is")) 915 { 916 postfix = "es"; 917 label = before; 918 } 919 else if ("sxz".contain(label[$-1])) 920 { 921 postfix = "es"; // glasses 922 } 923 } 924 return label ~ postfix; 925 } 926 927 @("has functioning pluralize()") 928 unittest 929 { 930 "dog".pluralize.shouldEqual("dogs"); 931 "ash".pluralize.shouldEqual("ashes"); 932 "day".pluralize.shouldEqual("days"); 933 "entity".pluralize.shouldEqual("entities"); 934 "thesis".pluralize.shouldEqual("theses"); 935 "glass".pluralize.shouldEqual("glasses"); 936 } 937 938 private string stringEndsWith(const string text, const string suffix) 939 { 940 import std.range : dropBack; 941 import std.string : endsWith; 942 943 if (text.endsWith(suffix)) 944 { 945 return text.dropBack(suffix.length); 946 } 947 return null; 948 } 949 950 @("has functioning stringEndsWith()") 951 unittest 952 { 953 "".stringEndsWith("").shouldNotBeNull; 954 "".stringEndsWith("x").shouldBeNull; 955 "Hello".stringEndsWith("Hello").shouldNotBeNull; 956 "Hello".stringEndsWith("Hello").shouldEqual(""); 957 "Hello".stringEndsWith("lo").shouldEqual("Hel"); 958 } 959 960 template hasOwnFunction(Aggregate, Super, string Name, Type) 961 { 962 import std.meta : AliasSeq, Filter; 963 import std.traits : Unqual; 964 enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 965 966 alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name)); 967 alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions); 968 enum hasFunction = MatchingFunctions.length == 1; 969 970 alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name)); 971 alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions); 972 enum superHasFunction = SuperMatchingFunctions.length == 1; 973 974 static if (hasFunction) 975 { 976 static if (superHasFunction) 977 { 978 enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]); 979 } 980 else 981 { 982 enum hasOwnFunction = true; 983 } 984 } 985 else 986 { 987 enum hasOwnFunction = false; 988 } 989 } 990 991 private final abstract class StringToStringSample { 992 override string toString(); 993 } 994 995 private final abstract class VoidToStringSample { 996 void toString(scope void delegate(const(char)[]) sink); 997 } 998 999 enum hasOwnStringToString(Aggregate, Super) 1000 = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString)); 1001 1002 enum hasOwnVoidToString(Aggregate, Super) 1003 = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString)); 1004 1005 @("correctly recognizes the existence of string toString() in a class") 1006 unittest 1007 { 1008 class Class1 1009 { 1010 override string toString() { return null; } 1011 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1012 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1013 } 1014 1015 class Class2 1016 { 1017 override string toString() const { return null; } 1018 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1019 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1020 } 1021 1022 class Class3 1023 { 1024 void toString(scope void delegate(const(char)[]) sink) const { } 1025 override string toString() const { return null; } 1026 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1027 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1028 } 1029 1030 class Class4 1031 { 1032 void toString(scope void delegate(const(char)[]) sink) const { } 1033 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1034 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1035 } 1036 1037 class Class5 1038 { 1039 mixin(GenerateToString); 1040 } 1041 1042 class ChildClass1 : Class1 1043 { 1044 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1045 } 1046 1047 class ChildClass2 : Class2 1048 { 1049 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1050 } 1051 1052 class ChildClass3 : Class3 1053 { 1054 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1055 } 1056 1057 class ChildClass5 : Class5 1058 { 1059 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1060 } 1061 }