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     import std.datetime : SysTime;
616     import std.typecons : Nullable;
617 
618     struct Struct
619     {
620         mixin(GenerateToString);
621     }
622 
623     struct Struct2
624     {
625         bool[Struct] hashmap;
626 
627         mixin(GenerateToString);
628     }
629 
630     const expected = `Struct2(hashmap=[])`;
631     const value = Struct2(null);
632 
633     value.to!string.shouldEqual(expected);
634 }
635 
636 @("labels nested types with fully qualified names")
637 unittest
638 {
639     import std.datetime : SysTime;
640     import std.typecons : Nullable;
641 
642     struct Struct
643     {
644         struct Struct2
645         {
646             mixin(GenerateToString);
647         }
648 
649         Struct2 struct2;
650 
651         mixin(GenerateToString);
652     }
653 
654     const expected = `Struct(Struct.Struct2())`;
655     const value = Struct(Struct.Struct2());
656 
657     value.to!string.shouldEqual(expected);
658 }
659 
660 @("supports fully qualified names with quotes")
661 unittest
662 {
663     import std.datetime : SysTime;
664     import std.typecons : Nullable;
665 
666     struct Struct(string s)
667     {
668         struct Struct2
669         {
670             mixin(GenerateToString);
671         }
672 
673         Struct2 struct2;
674 
675         mixin(GenerateToString);
676     }
677 
678     const expected = `Struct!"foo"(Struct!"foo".Struct2())`;
679     const value = Struct!"foo"(Struct!"foo".Struct2());
680 
681     value.to!string.shouldEqual(expected);
682 }
683 
684 mixin template GenerateToStringTemplate()
685 {
686 
687     // this is a separate function to reduce the
688     // "warning: unreachable code" spam that is falsely created from static foreach
689     private static generateToStringErrCheck()
690     {
691         if (!__ctfe)
692         {
693             return null;
694         }
695 
696         import boilerplate.autostring : ToString, typeName;
697         import boilerplate.util : GenNormalMemberTuple;
698         import std.string : format;
699 
700         bool udaIncludeSuper;
701         bool udaExcludeSuper;
702 
703         foreach (uda; __traits(getAttributes, typeof(this)))
704         {
705             static if (is(typeof(uda) == ToString))
706             {
707                 switch (uda)
708                 {
709                     case ToString.IncludeSuper: udaIncludeSuper = true; break;
710                     case ToString.ExcludeSuper: udaExcludeSuper = true; break;
711                     default: break;
712                 }
713             }
714         }
715 
716         if (udaIncludeSuper && udaExcludeSuper)
717         {
718             return format!(`static assert(false, ` ~
719                 `"Contradictory tags on '" ~ %(%s%) ~ "': IncludeSuper and ExcludeSuper");`)
720                 ([typeName!(typeof(this))]);
721         }
722 
723         mixin GenNormalMemberTuple!true;
724 
725         foreach (member; NormalMemberTuple)
726         {
727             enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member)));
728 
729             static if (error)
730             {
731                 return format!error(member);
732             }
733         }
734 
735         return ``;
736     }
737 
738     private static generateToStringImpl()
739     {
740         if (!__ctfe)
741         {
742             return null;
743         }
744 
745         import boilerplate.autostring : isMemberUnlabeledByDefault, ToString, typeName;
746         import boilerplate.conditions : NonEmpty;
747         import boilerplate.util : GenNormalMemberTuple, udaIndex;
748         import std.meta : Alias;
749         import std.string : endsWith, format, split, startsWith, strip;
750         import std.traits : BaseClassesTuple, getUDAs, Unqual;
751 
752         // synchronized without lock contention is basically free, so always do it
753         // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed
754         enum synchronize = false && is(typeof(this) == class);
755 
756         const constExample = typeof(this).init;
757         auto normalExample = typeof(this).init;
758 
759         enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString")
760             && is(typeof(normalExample.toString()) == string);
761         enum alreadyHaveUsableStringToString = alreadyHaveStringToString
762             && is(typeof(constExample.toString()) == string);
763 
764         enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString")
765             && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void);
766         enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString
767             && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void);
768 
769         enum isObject = is(typeof(this): Object);
770 
771         static if (isObject)
772         {
773             enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super));
774             enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super));
775         }
776         else
777         {
778             enum userDefinedStringToString = alreadyHaveStringToString;
779             enum userDefinedVoidToString = alreadyHaveVoidToString;
780         }
781 
782         static if (userDefinedStringToString && userDefinedVoidToString)
783         {
784             string result = ``; // Nothing to be done.
785         }
786         // if the user has defined their own string toString() in this aggregate:
787         else static if (userDefinedStringToString)
788         {
789             // just call it.
790             static if (alreadyHaveUsableStringToString)
791             {
792                 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~
793                     ` sink(this.toString());` ~
794                     ` }`;
795 
796                 static if (isObject
797                     && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void))
798                 {
799                     result = `override ` ~ result;
800                 }
801             }
802             else
803             {
804                 string result = `static assert(false, "toString is not const in this class.");`;
805             }
806         }
807         // if the user has defined their own void toString() in this aggregate:
808         else
809         {
810             string result = null;
811 
812             static if (!userDefinedVoidToString)
813             {
814                 bool nakedMode;
815                 bool udaIncludeSuper;
816                 bool udaExcludeSuper;
817 
818                 foreach (uda; __traits(getAttributes, typeof(this)))
819                 {
820                     static if (is(typeof(uda) == ToStringEnum))
821                     {
822                         switch (uda)
823                         {
824                             case ToString.Naked: nakedMode = true; break;
825                             case ToString.IncludeSuper: udaIncludeSuper = true; break;
826                             case ToString.ExcludeSuper: udaExcludeSuper = true; break;
827                             default: break;
828                         }
829                     }
830                 }
831 
832                 string NamePlusOpenParen = typeName!(typeof(this)) ~ "(";
833 
834                 version(AutoStringDebug)
835                 {
836                     result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString);
837                 }
838 
839                 static if (isObject && alreadyHaveVoidToString) result ~= `override `;
840 
841                 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {`
842                     ~ `import boilerplate.autostring: ToStringHandler;`
843                     ~ `import boilerplate.util: sinkWrite;`
844                     ~ `import std.traits: getUDAs;`;
845 
846                 static if (synchronize)
847                 {
848                     result ~= `synchronized (this) { `;
849                 }
850 
851                 if (!nakedMode)
852                 {
853                     result ~= format!`sink(%(%s%));`([NamePlusOpenParen]);
854                 }
855 
856                 bool includeSuper = false;
857 
858                 static if (isObject)
859                 {
860                     if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString)
861                     {
862                         includeSuper = true;
863                     }
864                 }
865 
866                 if (udaIncludeSuper)
867                 {
868                     includeSuper = true;
869                 }
870                 else if (udaExcludeSuper)
871                 {
872                     includeSuper = false;
873                 }
874 
875                 static if (isObject)
876                 {
877                     if (includeSuper)
878                     {
879                         static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString)
880                         {
881                             return `static assert(false, `
882                                 ~ `"cannot include super class in GenerateToString: `
883                                 ~ `parent class has no usable toString!");`;
884                         }
885                         else {
886                             static if (alreadyHaveUsableVoidToString)
887                             {
888                                 result ~= `super.toString(sink);`;
889                             }
890                             else
891                             {
892                                 result ~= `sink(super.toString());`;
893                             }
894                             result ~= `bool comma = true;`;
895                         }
896                     }
897                     else
898                     {
899                         result ~= `bool comma = false;`;
900                     }
901                 }
902                 else
903                 {
904                     result ~= `bool comma = false;`;
905                 }
906 
907                 result ~= `{`;
908 
909                 mixin GenNormalMemberTuple!(true);
910 
911                 foreach (member; NormalMemberTuple)
912                 {
913                     mixin("alias symbol = typeof(this)." ~ member ~ ";");
914 
915                     enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1;
916                     enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1;
917                     enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1;
918                     enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1;
919                     enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1;
920                     enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1;
921                     enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1;
922 
923                     // see std.traits.isFunction!()
924                     static if (
925                         is(symbol == function)
926                         || is(typeof(symbol) == function)
927                         || (is(typeof(&symbol) U : U*) && is(U == function)))
928                     {
929                         enum isFunction = true;
930                     }
931                     else
932                     {
933                         enum isFunction = false;
934                     }
935 
936                     enum includeOverride = udaInclude || udaOptional;
937 
938                     enum includeMember = (!isFunction || includeOverride) && !udaExclude;
939 
940                     static if (includeMember)
941                     {
942                         string memberName = member;
943 
944                         if (memberName.endsWith("_"))
945                         {
946                             memberName = memberName[0 .. $ - 1];
947                         }
948 
949                         bool labeled = true;
950 
951                         static if (udaUnlabeled)
952                         {
953                             labeled = false;
954                         }
955 
956                         if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty))
957                         {
958                             labeled = false;
959                         }
960 
961                         static if (udaLabeled)
962                         {
963                             labeled = true;
964                         }
965 
966                         string membervalue = `this.` ~ member;
967 
968                         bool escapeStrings = true;
969 
970                         static if (udaToStringHandler)
971                         {
972                             alias Handlers = getUDAs!(symbol, ToStringHandler);
973 
974                             static assert(Handlers.length == 1);
975 
976                             static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init)))
977                             {
978                                 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(`
979                                     ~ membervalue
980                                     ~ `)`;
981 
982                                 escapeStrings = false;
983                             }
984                             else
985                             {
986                                 return `static assert(false, "cannot determine how to call ToStringHandler");`;
987                             }
988                         }
989 
990                         string readMemberValue = membervalue;
991                         string conditionalWritestmt; // formatted with sink.sinkWrite(... readMemberValue ... )
992 
993                         static if (udaOptional)
994                         {
995                             import std.array : empty;
996 
997                             enum optionalIndex = udaIndex!(ToString.Optional, __traits(getAttributes, symbol));
998                             alias optionalUda = Alias!(__traits(getAttributes, symbol)[optionalIndex]);
999 
1000                             static if (is(optionalUda == struct))
1001                             {
1002                                 conditionalWritestmt = format!q{
1003                                     if (__traits(getAttributes, %s)[%s].condition(%s)) { %%s }
1004                                 } (membervalue, optionalIndex, membervalue);
1005                             }
1006                             else static if (__traits(compiles, typeof(symbol).init.isNull))
1007                             {
1008                                 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }}
1009                                     (membervalue);
1010                                 readMemberValue = membervalue ~ ".get";
1011                             }
1012                             else static if (__traits(compiles, typeof(symbol).init.empty))
1013                             {
1014                                 conditionalWritestmt = format!q{import std.array : empty; if (!%s.empty) { %%s }}
1015                                     (membervalue);
1016                             }
1017                             else static if (__traits(compiles, typeof(symbol).init !is null))
1018                             {
1019                                 conditionalWritestmt = format!q{if (%s !is null) { %%s }}
1020                                     (membervalue);
1021                             }
1022                             else static if (__traits(compiles, typeof(symbol).init != 0))
1023                             {
1024                                 conditionalWritestmt = format!q{if (%s != 0) { %%s }}
1025                                     (membervalue);
1026                             }
1027                             else static if (__traits(compiles, { if (typeof(symbol).init) { } }))
1028                             {
1029                                 conditionalWritestmt = format!q{if (%s) { %%s }}
1030                                     (membervalue);
1031                             }
1032                             else
1033                             {
1034                                 return format!(`static assert(false, `
1035                                         ~ `"don't know how to figure out whether %s is present.");`)
1036                                     (member);
1037                             }
1038                         }
1039                         else
1040                         {
1041                             conditionalWritestmt = q{ %s };
1042                         }
1043 
1044                         string writestmt;
1045 
1046                         if (labeled)
1047                         {
1048                             writestmt = format!`sink.sinkWrite(comma, %s, "%s=%%s", %s);`
1049                                 (escapeStrings, memberName, readMemberValue);
1050                         }
1051                         else
1052                         {
1053                             writestmt = format!`sink.sinkWrite(comma, %s, "%%s", %s);`
1054                                 (escapeStrings, readMemberValue);
1055                         }
1056 
1057                         result ~= format(conditionalWritestmt, writestmt);
1058                     }
1059                 }
1060 
1061                 result ~= `} `;
1062 
1063                 if (!nakedMode)
1064                 {
1065                     result ~= `sink(")");`;
1066                 }
1067 
1068                 static if (synchronize)
1069                 {
1070                     result ~= `} `;
1071                 }
1072 
1073                 result ~= `} `;
1074             }
1075 
1076             // generate fallback string toString()
1077             // that calls, specifically, *our own* toString impl.
1078             // (this is important to break cycles when a subclass implements a toString that calls super.toString)
1079             static if (isObject)
1080             {
1081                 result ~= `override `;
1082             }
1083 
1084             result ~= `public string toString() const {`
1085                 ~ `string result;`
1086                 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });`
1087                 ~ `return result;`
1088             ~ `}`;
1089         }
1090         return result;
1091     }
1092 }
1093 
1094 template checkAttributeConsistency(Attributes...)
1095 {
1096     enum checkAttributeConsistency = checkAttributeHelper();
1097 
1098     private string checkAttributeHelper()
1099     {
1100         if (!__ctfe)
1101         {
1102             return null;
1103         }
1104 
1105         import std.string : format;
1106 
1107         bool include, exclude, optional, labeled, unlabeled;
1108 
1109         foreach (uda; Attributes)
1110         {
1111             static if (is(typeof(uda) == ToStringEnum))
1112             {
1113                 switch (uda)
1114                 {
1115                     case ToString.Include: include = true; break;
1116                     case ToString.Exclude: exclude = true; break;
1117                     case ToString.Labeled: labeled = true; break;
1118                     case ToString.Unlabeled: unlabeled = true; break;
1119                     default: break;
1120                 }
1121             }
1122             else static if (is(uda == struct) && __traits(isSame, uda, ToString.Optional))
1123             {
1124                 optional = true;
1125             }
1126         }
1127 
1128         if (include && exclude)
1129         {
1130             return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`;
1131         }
1132 
1133         if (include && optional)
1134         {
1135             return `static assert(false, "Redundant tags on '%s': Optional implies Include");`;
1136         }
1137 
1138         if (exclude && optional)
1139         {
1140             return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`;
1141         }
1142 
1143         if (labeled && unlabeled)
1144         {
1145             return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`;
1146         }
1147 
1148         return null;
1149     }
1150 }
1151 
1152 struct ToStringHandler(alias Handler_)
1153 {
1154     alias Handler = Handler_;
1155 }
1156 
1157 enum ToStringEnum
1158 {
1159     // these go on the class
1160     Naked,
1161     IncludeSuper,
1162     ExcludeSuper,
1163 
1164     // these go on the field/method
1165     Unlabeled,
1166     Labeled,
1167     Exclude,
1168     Include,
1169 }
1170 
1171 struct ToString
1172 {
1173     static foreach (name; __traits(allMembers, ToStringEnum))
1174     {
1175         mixin(format!q{enum %s = ToStringEnum.%s;}(name, name));
1176     }
1177 
1178     static struct Optional(alias condition_)
1179     {
1180         alias condition = condition_;
1181     }
1182 }
1183 
1184 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty)
1185 {
1186     import std.datetime : SysTime;
1187     import std.range.primitives : ElementType, isInputRange;
1188     import std.typecons : BitFlags;
1189 
1190     field = field.toLower;
1191 
1192     static if (isInputRange!Type)
1193     {
1194         alias BaseType = ElementType!Type;
1195 
1196         if (field == BaseType.stringof.toLower.pluralize && attribNonEmpty)
1197         {
1198             return true;
1199         }
1200     }
1201     else static if (is(Type: const BitFlags!BaseType, BaseType))
1202     {
1203         if (field == BaseType.stringof.toLower.pluralize)
1204         {
1205             return true;
1206         }
1207     }
1208 
1209     return field == Type.stringof.toLower
1210         || (field == "time" && is(Type == SysTime))
1211         || (field == "id" && is(typeof(Type.toString)));
1212 }
1213 
1214 private string toLower(string text)
1215 {
1216     import std.string : stdToLower = toLower;
1217 
1218     string result = null;
1219 
1220     foreach (ub; cast(immutable(ubyte)[]) text)
1221     {
1222         if (ub >= 0x80) // utf-8, non-ascii
1223         {
1224             return text.stdToLower;
1225         }
1226         if (ub >= 'A' && ub <= 'Z')
1227         {
1228             result ~= cast(char) (ub + ('a' - 'A'));
1229         }
1230         else
1231         {
1232             result ~= cast(char) ub;
1233         }
1234     }
1235     return result;
1236 }
1237 
1238 // http://code.activestate.com/recipes/82102/
1239 private string pluralize(string label)
1240 {
1241     import std.algorithm.searching : contain = canFind;
1242 
1243     string postfix = "s";
1244     if (label.length > 2)
1245     {
1246         enum vowels = "aeiou";
1247 
1248         if (label.stringEndsWith("ch") || label.stringEndsWith("sh"))
1249         {
1250             postfix = "es";
1251         }
1252         else if (auto before = label.stringEndsWith("y"))
1253         {
1254             if (!vowels.contain(label[$ - 2]))
1255             {
1256                 postfix = "ies";
1257                 label = before;
1258             }
1259         }
1260         else if (auto before = label.stringEndsWith("is"))
1261         {
1262             postfix = "es";
1263             label = before;
1264         }
1265         else if ("sxz".contain(label[$-1]))
1266         {
1267             postfix = "es"; // glasses
1268         }
1269     }
1270     return label ~ postfix;
1271 }
1272 
1273 @("has functioning pluralize()")
1274 unittest
1275 {
1276     "dog".pluralize.shouldEqual("dogs");
1277     "ash".pluralize.shouldEqual("ashes");
1278     "day".pluralize.shouldEqual("days");
1279     "entity".pluralize.shouldEqual("entities");
1280     "thesis".pluralize.shouldEqual("theses");
1281     "glass".pluralize.shouldEqual("glasses");
1282 }
1283 
1284 private string stringEndsWith(const string text, const string suffix)
1285 {
1286     import std.range : dropBack;
1287     import std.string : endsWith;
1288 
1289     if (text.endsWith(suffix))
1290     {
1291         return text.dropBack(suffix.length);
1292     }
1293     return null;
1294 }
1295 
1296 @("has functioning stringEndsWith()")
1297 unittest
1298 {
1299     "".stringEndsWith("").shouldNotBeNull;
1300     "".stringEndsWith("x").shouldBeNull;
1301     "Hello".stringEndsWith("Hello").shouldNotBeNull;
1302     "Hello".stringEndsWith("Hello").shouldEqual("");
1303     "Hello".stringEndsWith("lo").shouldEqual("Hel");
1304 }
1305 
1306 template hasOwnFunction(Aggregate, Super, string Name, Type)
1307 {
1308     import std.meta : AliasSeq, Filter;
1309     import std.traits : Unqual;
1310     enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type);
1311 
1312     alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name));
1313     alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions);
1314     enum hasFunction = MatchingFunctions.length == 1;
1315 
1316     alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name));
1317     alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions);
1318     enum superHasFunction = SuperMatchingFunctions.length == 1;
1319 
1320     static if (hasFunction)
1321     {
1322         static if (superHasFunction)
1323         {
1324             enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]);
1325         }
1326         else
1327         {
1328             enum hasOwnFunction = true;
1329         }
1330     }
1331     else
1332     {
1333         enum hasOwnFunction = false;
1334     }
1335 }
1336 
1337 /**
1338  * Find qualified name of `T` including any containing types; not including containing functions or modules.
1339  */
1340 public template typeName(T)
1341 {
1342     static if (__traits(compiles, __traits(parent, T)))
1343     {
1344         alias parent = Alias!(__traits(parent, T));
1345         enum isSame = __traits(isSame, T, parent);
1346 
1347         static if (!isSame && (
1348             is(parent == struct) || is(parent == union) || is(parent == enum) ||
1349             is(parent == class) || is(parent == interface)))
1350         {
1351             enum typeName = typeName!parent ~ "." ~ Unqual!T.stringof;
1352         }
1353         else
1354         {
1355             enum typeName = Unqual!T.stringof;
1356         }
1357     }
1358     else
1359     {
1360         enum typeName = Unqual!T.stringof;
1361     }
1362 }
1363 
1364 private final abstract class StringToStringSample {
1365     override string toString();
1366 }
1367 
1368 private final abstract class VoidToStringSample {
1369     void toString(scope void delegate(const(char)[]) sink);
1370 }
1371 
1372 enum hasOwnStringToString(Aggregate, Super)
1373     = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString));
1374 
1375 enum hasOwnVoidToString(Aggregate, Super)
1376     = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString));
1377 
1378 @("correctly recognizes the existence of string toString() in a class")
1379 unittest
1380 {
1381     class Class1
1382     {
1383         override string toString() { return null; }
1384         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1385         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1386     }
1387 
1388     class Class2
1389     {
1390         override string toString() const { return null; }
1391         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1392         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1393     }
1394 
1395     class Class3
1396     {
1397         void toString(scope void delegate(const(char)[]) sink) const { }
1398         override string toString() const { return null; }
1399         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1400         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1401     }
1402 
1403     class Class4
1404     {
1405         void toString(scope void delegate(const(char)[]) sink) const { }
1406         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1407         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1408     }
1409 
1410     class Class5
1411     {
1412         mixin(GenerateToString);
1413     }
1414 
1415     class ChildClass1 : Class1
1416     {
1417         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1418     }
1419 
1420     class ChildClass2 : Class2
1421     {
1422         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1423     }
1424 
1425     class ChildClass3 : Class3
1426     {
1427         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1428     }
1429 
1430     class ChildClass5 : Class5
1431     {
1432         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1433     }
1434 }