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