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