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