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