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