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 as long as the array is NonEmpty. Otherwise, there would be no way to tell what the field contains. 342 +/ 343 @("does not label fields with the same name as the type") 344 unittest 345 { 346 import boilerplate.conditions : NonEmpty; 347 348 struct Value { mixin(GenerateToString); } 349 struct Entity { mixin(GenerateToString); } 350 struct Day { mixin(GenerateToString); } 351 352 struct Struct 353 { 354 @NonEmpty 355 Value[] values; 356 357 @NonEmpty 358 Entity[] entities; 359 360 @NonEmpty 361 Day[] days; 362 363 mixin(GenerateToString); 364 } 365 366 auto value = Struct( 367 [Value()], 368 [Entity()], 369 [Day()]); 370 371 value.to!string.shouldEqual("Struct([Value()], [Entity()], [Day()])"); 372 } 373 374 /++ 375 Fields that are not NonEmpty are always labeled. 376 This is because they can be empty, in which case you can't tell what's in them from naming. 377 +/ 378 @("does label fields that may be empty") 379 unittest 380 { 381 import boilerplate.conditions : NonEmpty; 382 383 struct Value { mixin(GenerateToString); } 384 385 struct Struct 386 { 387 Value[] values; 388 389 mixin(GenerateToString); 390 } 391 392 Struct(null).to!string.shouldEqual("Struct(values=[])"); 393 } 394 395 /++ 396 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue. 397 +/ 398 @("does not collide with accessors") 399 unittest 400 { 401 struct Struct 402 { 403 import boilerplate.accessors : GenerateFieldAccessors, ConstRead; 404 405 @ConstRead 406 private int a_; 407 408 mixin(GenerateFieldAccessors); 409 410 mixin(GenerateToString); 411 } 412 413 Struct.init.to!string.shouldEqual("Struct(a=0)"); 414 } 415 416 @("supports child classes of abstract classes") 417 unittest 418 { 419 static abstract class ParentClass 420 { 421 } 422 class ChildClass : ParentClass 423 { 424 mixin(GenerateToString); 425 } 426 } 427 428 @("supports custom toString handlers") 429 unittest 430 { 431 struct Struct 432 { 433 @ToStringHandler!(i => i ? "yes" : "no") 434 int i; 435 436 mixin(GenerateToString); 437 } 438 439 Struct.init.to!string.shouldEqual("Struct(i=no)"); 440 } 441 442 @("passes nullable unchanged to custom toString handlers") 443 unittest 444 { 445 import std.typecons : Nullable; 446 447 struct Struct 448 { 449 @ToStringHandler!(ni => ni.isNull ? "no" : "yes") 450 Nullable!int ni; 451 452 mixin(GenerateToString); 453 } 454 455 Struct.init.to!string.shouldEqual("Struct(ni=no)"); 456 } 457 458 // see unittest.config.string 459 @("supports optional BitFlags in structs") 460 unittest 461 { 462 import std.typecons : BitFlags; 463 464 enum Enum 465 { 466 A = 1, 467 B = 2, 468 } 469 470 struct Struct 471 { 472 @(ToString.Optional) 473 BitFlags!Enum field; 474 475 mixin(GenerateToString); 476 } 477 478 Struct.init.to!string.shouldEqual("Struct()"); 479 } 480 481 mixin template GenerateToStringTemplate() 482 { 483 484 // this is a separate function to reduce the 485 // "warning: unreachable code" spam that is falsely created from static foreach 486 private static generateToStringErrCheck() 487 { 488 if (!__ctfe) 489 { 490 return null; 491 } 492 493 import boilerplate.autostring : ToString; 494 import boilerplate.util : GenNormalMemberTuple; 495 import std.string : format; 496 497 bool udaIncludeSuper; 498 bool udaExcludeSuper; 499 500 foreach (uda; __traits(getAttributes, typeof(this))) 501 { 502 static if (is(typeof(uda) == ToString)) 503 { 504 switch (uda) 505 { 506 case ToString.IncludeSuper: udaIncludeSuper = true; break; 507 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 508 default: break; 509 } 510 } 511 } 512 513 if (udaIncludeSuper && udaExcludeSuper) 514 { 515 return format!`static assert(false, "Contradictory tags on '%s': IncludeSuper and ExcludeSuper");` 516 (typeof(this).stringof); 517 } 518 519 mixin GenNormalMemberTuple!true; 520 521 foreach (member; NormalMemberTuple) 522 { 523 enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member))); 524 525 static if (error) 526 { 527 return format!error(member); 528 } 529 } 530 531 return ``; 532 } 533 534 private static generateToStringImpl() 535 { 536 if (!__ctfe) 537 { 538 return null; 539 } 540 541 import std.string : endsWith, format, split, startsWith, strip; 542 import std.traits : BaseClassesTuple, Unqual, getUDAs; 543 import boilerplate.autostring : ToString, isMemberUnlabeledByDefault; 544 import boilerplate.conditions : NonEmpty; 545 import boilerplate.util : GenNormalMemberTuple, udaIndex; 546 547 // synchronized without lock contention is basically free, so always do it 548 // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 549 enum synchronize = false && is(typeof(this) == class); 550 551 const constExample = typeof(this).init; 552 auto normalExample = typeof(this).init; 553 554 enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString") 555 && is(typeof(normalExample.toString()) == string); 556 enum alreadyHaveUsableStringToString = alreadyHaveStringToString 557 && is(typeof(constExample.toString()) == string); 558 559 enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString") 560 && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void); 561 enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString 562 && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void); 563 564 enum isObject = is(typeof(this): Object); 565 566 static if (isObject) 567 { 568 enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super)); 569 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super)); 570 } 571 else 572 { 573 enum userDefinedStringToString = alreadyHaveStringToString; 574 enum userDefinedVoidToString = alreadyHaveVoidToString; 575 } 576 577 static if (userDefinedStringToString && userDefinedVoidToString) 578 { 579 string result = ``; // Nothing to be done. 580 } 581 // if the user has defined their own string toString() in this aggregate: 582 else static if (userDefinedStringToString) 583 { 584 // just call it. 585 static if (alreadyHaveUsableStringToString) 586 { 587 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~ 588 ` sink(this.toString());` ~ 589 ` }`; 590 591 static if (isObject 592 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 593 { 594 result = `override ` ~ result; 595 } 596 } 597 else 598 { 599 string result = `static assert(false, "toString is not const in this class.");`; 600 } 601 } 602 // if the user has defined their own void toString() in this aggregate: 603 else 604 { 605 string result = null; 606 607 static if (!userDefinedVoidToString) 608 { 609 bool nakedMode; 610 bool udaIncludeSuper; 611 bool udaExcludeSuper; 612 613 foreach (uda; __traits(getAttributes, typeof(this))) 614 { 615 static if (is(typeof(uda) == ToString)) 616 { 617 switch (uda) 618 { 619 case ToString.Naked: nakedMode = true; break; 620 case ToString.IncludeSuper: udaIncludeSuper = true; break; 621 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 622 default: break; 623 } 624 } 625 } 626 627 string NamePlusOpenParen = Unqual!(typeof(this)).stringof ~ "("; 628 629 version(AutoStringDebug) 630 { 631 result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString); 632 } 633 634 static if (isObject && alreadyHaveVoidToString) result ~= `override `; 635 636 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {` 637 ~ `import boilerplate.autostring: ToStringHandler;` 638 ~ `import boilerplate.util: sinkWrite;` 639 ~ `import std.traits: getUDAs;`; 640 641 static if (synchronize) 642 { 643 result ~= `synchronized (this) { `; 644 } 645 646 if (!nakedMode) 647 { 648 result ~= `sink("` ~ NamePlusOpenParen ~ `");`; 649 } 650 651 bool includeSuper = false; 652 653 static if (isObject) 654 { 655 if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString) 656 { 657 includeSuper = true; 658 } 659 } 660 661 if (udaIncludeSuper) 662 { 663 includeSuper = true; 664 } 665 else if (udaExcludeSuper) 666 { 667 includeSuper = false; 668 } 669 670 static if (isObject) 671 { 672 if (includeSuper) 673 { 674 static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString) 675 { 676 return `static assert(false, ` 677 ~ `"cannot include super class in GenerateToString: ` 678 ~ `parent class has no usable toString!");`; 679 } 680 else { 681 static if (alreadyHaveUsableVoidToString) 682 { 683 result ~= `super.toString(sink);`; 684 } 685 else 686 { 687 result ~= `sink(super.toString());`; 688 } 689 result ~= `bool comma = true;`; 690 } 691 } 692 else 693 { 694 result ~= `bool comma = false;`; 695 } 696 } 697 else 698 { 699 result ~= `bool comma = false;`; 700 } 701 702 result ~= `{`; 703 704 mixin GenNormalMemberTuple!(true); 705 706 foreach (member; NormalMemberTuple) 707 { 708 mixin("alias symbol = typeof(this)." ~ member ~ ";"); 709 710 enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1; 711 enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1; 712 enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1; 713 enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1; 714 enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1; 715 enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1; 716 enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1; 717 718 // see std.traits.isFunction!() 719 static if (is(symbol == function) || is(typeof(symbol) == function) 720 || is(typeof(&symbol) U : U*) && is(U == function)) 721 { 722 enum isFunction = true; 723 } 724 else 725 { 726 enum isFunction = false; 727 } 728 729 enum includeOverride = udaInclude || udaOptional; 730 731 enum includeMember = (!isFunction || includeOverride) && !udaExclude; 732 733 static if (includeMember) 734 { 735 string memberName = member; 736 737 if (memberName.endsWith("_")) 738 { 739 memberName = memberName[0 .. $ - 1]; 740 } 741 742 bool labeled = true; 743 744 static if (udaUnlabeled) 745 { 746 labeled = false; 747 } 748 749 if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty)) 750 { 751 labeled = false; 752 } 753 754 static if (udaLabeled) 755 { 756 labeled = true; 757 } 758 759 string membervalue = `this.` ~ member; 760 761 static if (udaToStringHandler) 762 { 763 alias Handlers = getUDAs!(symbol, ToStringHandler); 764 765 static assert(Handlers.length == 1); 766 767 static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init))) 768 { 769 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(` 770 ~ membervalue 771 ~ `)`; 772 } 773 else 774 { 775 return `static assert(false, "cannot determine how to call ToStringHandler");`; 776 } 777 } 778 779 string writestmt; 780 781 if (labeled) 782 { 783 writestmt = format!`sink.sinkWrite(comma, "%s=%%s", %s);` 784 (memberName, membervalue); 785 } 786 else 787 { 788 writestmt = format!`sink.sinkWrite(comma, "%%s", %s);`(membervalue); 789 } 790 791 static if (udaOptional) 792 { 793 import std.array : empty; 794 795 static if (__traits(compiles, typeof(symbol).init.empty)) 796 { 797 result ~= format!`import std.array : empty; if (!%s.empty) { %s }` 798 (membervalue, writestmt); 799 } 800 else static if (__traits(compiles, typeof(symbol).init !is null)) 801 { 802 result ~= format!`if (%s !is null) { %s }` 803 (membervalue, writestmt); 804 } 805 else static if (__traits(compiles, typeof(symbol).init != 0)) 806 { 807 result ~= format!`if (%s != 0) { %s }` 808 (membervalue, writestmt); 809 } 810 else static if (__traits(compiles, { if (typeof(symbol).init) { } })) 811 { 812 result ~= format!`if (%s) { %s }` 813 (membervalue, writestmt); 814 } 815 else 816 { 817 return format!(`static assert(false, ` 818 ~ `"don't know how to figure out whether %s is present.");`) 819 (member); 820 } 821 } 822 else 823 { 824 result ~= writestmt; 825 } 826 } 827 } 828 829 result ~= `} `; 830 831 if (!nakedMode) 832 { 833 result ~= `sink(")");`; 834 } 835 836 static if (synchronize) 837 { 838 result ~= `} `; 839 } 840 841 result ~= `} `; 842 } 843 844 // generate fallback string toString() 845 // that calls, specifically, *our own* toString impl. 846 // (this is important to break cycles when a subclass implements a toString that calls super.toString) 847 static if (isObject) 848 { 849 result ~= `override `; 850 } 851 852 result ~= `public string toString() const {` 853 ~ `string result;` 854 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });` 855 ~ `return result;` 856 ~ `}`; 857 } 858 return result; 859 } 860 } 861 862 template checkAttributeConsistency(Attributes...) 863 { 864 enum checkAttributeConsistency = checkAttributeHelper(); 865 866 private string checkAttributeHelper() 867 { 868 if (!__ctfe) 869 { 870 return null; 871 } 872 873 import std.string : format; 874 875 bool include, exclude, optional, labeled, unlabeled; 876 877 foreach (uda; Attributes) 878 { 879 static if (is(typeof(uda) == ToString)) 880 { 881 switch (uda) 882 { 883 case ToString.Include: include = true; break; 884 case ToString.Exclude: exclude = true; break; 885 case ToString.Optional: optional = true; break; 886 case ToString.Labeled: labeled = true; break; 887 case ToString.Unlabeled: unlabeled = true; break; 888 default: break; 889 } 890 } 891 } 892 893 if (include && exclude) 894 { 895 return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`; 896 } 897 898 if (include && optional) 899 { 900 return `static assert(false, "Redundant tags on '%s': Optional implies Include");`; 901 } 902 903 if (exclude && optional) 904 { 905 return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`; 906 } 907 908 if (labeled && unlabeled) 909 { 910 return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`; 911 } 912 913 return null; 914 } 915 } 916 917 struct ToStringHandler(alias Handler_) 918 { 919 alias Handler = Handler_; 920 } 921 922 enum ToString 923 { 924 // these go on the class 925 Naked, 926 IncludeSuper, 927 ExcludeSuper, 928 929 // these go on the field/method 930 Unlabeled, 931 Labeled, 932 Exclude, 933 Include, 934 Optional, 935 } 936 937 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty) 938 { 939 import std.string : toLower; 940 import std.range.primitives : ElementType, isInputRange; 941 942 static if (isInputRange!Type) 943 { 944 alias BaseType = ElementType!Type; 945 946 if (field.toLower == BaseType.stringof.toLower.pluralize && attribNonEmpty) 947 { 948 return true; 949 } 950 } 951 952 return field.toLower == Type.stringof.toLower 953 || field.toLower == "time" && Type.stringof == "SysTime" 954 || field.toLower == "id" && is(typeof(Type.toString)); 955 } 956 957 // http://code.activestate.com/recipes/82102/ 958 private string pluralize(string label) 959 { 960 import std.algorithm.searching : contain = canFind; 961 962 string postfix = "s"; 963 if (label.length > 2) 964 { 965 enum vowels = "aeiou"; 966 967 if (label.stringEndsWith("ch") || label.stringEndsWith("sh")) 968 { 969 postfix = "es"; 970 } 971 else if (auto before = label.stringEndsWith("y")) 972 { 973 if (!vowels.contain(label[$ - 2])) 974 { 975 postfix = "ies"; 976 label = before; 977 } 978 } 979 else if (auto before = label.stringEndsWith("is")) 980 { 981 postfix = "es"; 982 label = before; 983 } 984 else if ("sxz".contain(label[$-1])) 985 { 986 postfix = "es"; // glasses 987 } 988 } 989 return label ~ postfix; 990 } 991 992 @("has functioning pluralize()") 993 unittest 994 { 995 "dog".pluralize.shouldEqual("dogs"); 996 "ash".pluralize.shouldEqual("ashes"); 997 "day".pluralize.shouldEqual("days"); 998 "entity".pluralize.shouldEqual("entities"); 999 "thesis".pluralize.shouldEqual("theses"); 1000 "glass".pluralize.shouldEqual("glasses"); 1001 } 1002 1003 private string stringEndsWith(const string text, const string suffix) 1004 { 1005 import std.range : dropBack; 1006 import std.string : endsWith; 1007 1008 if (text.endsWith(suffix)) 1009 { 1010 return text.dropBack(suffix.length); 1011 } 1012 return null; 1013 } 1014 1015 @("has functioning stringEndsWith()") 1016 unittest 1017 { 1018 "".stringEndsWith("").shouldNotBeNull; 1019 "".stringEndsWith("x").shouldBeNull; 1020 "Hello".stringEndsWith("Hello").shouldNotBeNull; 1021 "Hello".stringEndsWith("Hello").shouldEqual(""); 1022 "Hello".stringEndsWith("lo").shouldEqual("Hel"); 1023 } 1024 1025 template hasOwnFunction(Aggregate, Super, string Name, Type) 1026 { 1027 import std.meta : AliasSeq, Filter; 1028 import std.traits : Unqual; 1029 enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1030 1031 alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name)); 1032 alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions); 1033 enum hasFunction = MatchingFunctions.length == 1; 1034 1035 alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name)); 1036 alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions); 1037 enum superHasFunction = SuperMatchingFunctions.length == 1; 1038 1039 static if (hasFunction) 1040 { 1041 static if (superHasFunction) 1042 { 1043 enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]); 1044 } 1045 else 1046 { 1047 enum hasOwnFunction = true; 1048 } 1049 } 1050 else 1051 { 1052 enum hasOwnFunction = false; 1053 } 1054 } 1055 1056 private final abstract class StringToStringSample { 1057 override string toString(); 1058 } 1059 1060 private final abstract class VoidToStringSample { 1061 void toString(scope void delegate(const(char)[]) sink); 1062 } 1063 1064 enum hasOwnStringToString(Aggregate, Super) 1065 = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString)); 1066 1067 enum hasOwnVoidToString(Aggregate, Super) 1068 = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString)); 1069 1070 @("correctly recognizes the existence of string toString() in a class") 1071 unittest 1072 { 1073 class Class1 1074 { 1075 override string toString() { return null; } 1076 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1077 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1078 } 1079 1080 class Class2 1081 { 1082 override string toString() const { return null; } 1083 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1084 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1085 } 1086 1087 class Class3 1088 { 1089 void toString(scope void delegate(const(char)[]) sink) const { } 1090 override string toString() const { return null; } 1091 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1092 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1093 } 1094 1095 class Class4 1096 { 1097 void toString(scope void delegate(const(char)[]) sink) const { } 1098 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1099 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1100 } 1101 1102 class Class5 1103 { 1104 mixin(GenerateToString); 1105 } 1106 1107 class ChildClass1 : Class1 1108 { 1109 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1110 } 1111 1112 class ChildClass2 : Class2 1113 { 1114 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1115 } 1116 1117 class ChildClass3 : Class3 1118 { 1119 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1120 } 1121 1122 class ChildClass5 : Class5 1123 { 1124 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1125 } 1126 }