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