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 with the same name as the type")
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 /++
375 Fields that are not NonEmpty are always labeled.
376 This is because they can be empty, in which case you can't tell what's in them from naming.
377 +/
378 @("does label fields that may be empty")
379 unittest
380 {
381     import boilerplate.conditions : NonEmpty;
382 
383     struct Value { mixin(GenerateToString); }
384 
385     struct Struct
386     {
387         Value[] values;
388 
389         mixin(GenerateToString);
390     }
391 
392     Struct(null).to!string.shouldEqual("Struct(values=[])");
393 }
394 
395 /++
396 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue.
397 +/
398 @("does not collide with accessors")
399 unittest
400 {
401     struct Struct
402     {
403         import boilerplate.accessors : GenerateFieldAccessors, ConstRead;
404 
405         @ConstRead
406         private int a_;
407 
408         mixin(GenerateFieldAccessors);
409 
410         mixin(GenerateToString);
411     }
412 
413     Struct.init.to!string.shouldEqual("Struct(a=0)");
414 }
415 
416 @("supports child classes of abstract classes")
417 unittest
418 {
419     static abstract class ParentClass
420     {
421     }
422     class ChildClass : ParentClass
423     {
424         mixin(GenerateToString);
425     }
426 }
427 
428 @("supports custom toString handlers")
429 unittest
430 {
431     struct Struct
432     {
433         @ToStringHandler!(i => i ? "yes" : "no")
434         int i;
435 
436         mixin(GenerateToString);
437     }
438 
439     Struct.init.to!string.shouldEqual("Struct(i=no)");
440 }
441 
442 @("passes nullable unchanged to custom toString handlers")
443 unittest
444 {
445     import std.typecons : Nullable;
446 
447     struct Struct
448     {
449         @ToStringHandler!(ni => ni.isNull ? "no" : "yes")
450         Nullable!int ni;
451 
452         mixin(GenerateToString);
453     }
454 
455     Struct.init.to!string.shouldEqual("Struct(ni=no)");
456 }
457 
458 // see unittest.config.string
459 @("supports optional BitFlags in structs")
460 unittest
461 {
462     import std.typecons : BitFlags;
463 
464     enum Enum
465     {
466         A = 1,
467         B = 2,
468     }
469 
470     struct Struct
471     {
472         @(ToString.Optional)
473         BitFlags!Enum field;
474 
475         mixin(GenerateToString);
476     }
477 
478     Struct.init.to!string.shouldEqual("Struct()");
479 }
480 
481 mixin template GenerateToStringTemplate()
482 {
483 
484     // this is a separate function to reduce the
485     // "warning: unreachable code" spam that is falsely created from static foreach
486     private static generateToStringErrCheck()
487     {
488         if (!__ctfe)
489         {
490             return null;
491         }
492 
493         import boilerplate.autostring : ToString;
494         import boilerplate.util : GenNormalMemberTuple;
495         import std.string : format;
496 
497         bool udaIncludeSuper;
498         bool udaExcludeSuper;
499 
500         foreach (uda; __traits(getAttributes, typeof(this)))
501         {
502             static if (is(typeof(uda) == ToString))
503             {
504                 switch (uda)
505                 {
506                     case ToString.IncludeSuper: udaIncludeSuper = true; break;
507                     case ToString.ExcludeSuper: udaExcludeSuper = true; break;
508                     default: break;
509                 }
510             }
511         }
512 
513         if (udaIncludeSuper && udaExcludeSuper)
514         {
515             return format!`static assert(false, "Contradictory tags on '%s': IncludeSuper and ExcludeSuper");`
516                 (typeof(this).stringof);
517         }
518 
519         mixin GenNormalMemberTuple!true;
520 
521         foreach (member; NormalMemberTuple)
522         {
523             enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member)));
524 
525             static if (error)
526             {
527                 return format!error(member);
528             }
529         }
530 
531         return ``;
532     }
533 
534     private static generateToStringImpl()
535     {
536         if (!__ctfe)
537         {
538             return null;
539         }
540 
541         import std.string : endsWith, format, split, startsWith, strip;
542         import std.traits : BaseClassesTuple, Unqual, getUDAs;
543         import boilerplate.autostring : ToString, isMemberUnlabeledByDefault;
544         import boilerplate.conditions : NonEmpty;
545         import boilerplate.util : GenNormalMemberTuple, udaIndex;
546 
547         // synchronized without lock contention is basically free, so always do it
548         // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed
549         enum synchronize = false && is(typeof(this) == class);
550 
551         const constExample = typeof(this).init;
552         auto normalExample = typeof(this).init;
553 
554         enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString")
555             && is(typeof(normalExample.toString()) == string);
556         enum alreadyHaveUsableStringToString = alreadyHaveStringToString
557             && is(typeof(constExample.toString()) == string);
558 
559         enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString")
560             && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void);
561         enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString
562             && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void);
563 
564         enum isObject = is(typeof(this): Object);
565 
566         static if (isObject)
567         {
568             enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super));
569             enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super));
570         }
571         else
572         {
573             enum userDefinedStringToString = alreadyHaveStringToString;
574             enum userDefinedVoidToString = alreadyHaveVoidToString;
575         }
576 
577         static if (userDefinedStringToString && userDefinedVoidToString)
578         {
579             string result = ``; // Nothing to be done.
580         }
581         // if the user has defined their own string toString() in this aggregate:
582         else static if (userDefinedStringToString)
583         {
584             // just call it.
585             static if (alreadyHaveUsableStringToString)
586             {
587                 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~
588                     ` sink(this.toString());` ~
589                     ` }`;
590 
591                 static if (isObject
592                     && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void))
593                 {
594                     result = `override ` ~ result;
595                 }
596             }
597             else
598             {
599                 string result = `static assert(false, "toString is not const in this class.");`;
600             }
601         }
602         // if the user has defined their own void toString() in this aggregate:
603         else
604         {
605             string result = null;
606 
607             static if (!userDefinedVoidToString)
608             {
609                 bool nakedMode;
610                 bool udaIncludeSuper;
611                 bool udaExcludeSuper;
612 
613                 foreach (uda; __traits(getAttributes, typeof(this)))
614                 {
615                     static if (is(typeof(uda) == ToString))
616                     {
617                         switch (uda)
618                         {
619                             case ToString.Naked: nakedMode = true; break;
620                             case ToString.IncludeSuper: udaIncludeSuper = true; break;
621                             case ToString.ExcludeSuper: udaExcludeSuper = true; break;
622                             default: break;
623                         }
624                     }
625                 }
626 
627                 string NamePlusOpenParen = Unqual!(typeof(this)).stringof ~ "(";
628 
629                 version(AutoStringDebug)
630                 {
631                     result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString);
632                 }
633 
634                 static if (isObject && alreadyHaveVoidToString) result ~= `override `;
635 
636                 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {`
637                     ~ `import boilerplate.autostring: ToStringHandler;`
638                     ~ `import boilerplate.util: sinkWrite;`
639                     ~ `import std.traits: getUDAs;`;
640 
641                 static if (synchronize)
642                 {
643                     result ~= `synchronized (this) { `;
644                 }
645 
646                 if (!nakedMode)
647                 {
648                     result ~= `sink("` ~ NamePlusOpenParen ~ `");`;
649                 }
650 
651                 bool includeSuper = false;
652 
653                 static if (isObject)
654                 {
655                     if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString)
656                     {
657                         includeSuper = true;
658                     }
659                 }
660 
661                 if (udaIncludeSuper)
662                 {
663                     includeSuper = true;
664                 }
665                 else if (udaExcludeSuper)
666                 {
667                     includeSuper = false;
668                 }
669 
670                 static if (isObject)
671                 {
672                     if (includeSuper)
673                     {
674                         static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString)
675                         {
676                             return `static assert(false, `
677                                 ~ `"cannot include super class in GenerateToString: `
678                                 ~ `parent class has no usable toString!");`;
679                         }
680                         else {
681                             static if (alreadyHaveUsableVoidToString)
682                             {
683                                 result ~= `super.toString(sink);`;
684                             }
685                             else
686                             {
687                                 result ~= `sink(super.toString());`;
688                             }
689                             result ~= `bool comma = true;`;
690                         }
691                     }
692                     else
693                     {
694                         result ~= `bool comma = false;`;
695                     }
696                 }
697                 else
698                 {
699                     result ~= `bool comma = false;`;
700                 }
701 
702                 result ~= `{`;
703 
704                 mixin GenNormalMemberTuple!(true);
705 
706                 foreach (member; NormalMemberTuple)
707                 {
708                     mixin("alias symbol = typeof(this)." ~ member ~ ";");
709 
710                     enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1;
711                     enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1;
712                     enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1;
713                     enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1;
714                     enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1;
715                     enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1;
716                     enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1;
717 
718                     // see std.traits.isFunction!()
719                     static if (is(symbol == function) || is(typeof(symbol) == function)
720                         || is(typeof(&symbol) U : U*) && is(U == function))
721                     {
722                         enum isFunction = true;
723                     }
724                     else
725                     {
726                         enum isFunction = false;
727                     }
728 
729                     enum includeOverride = udaInclude || udaOptional;
730 
731                     enum includeMember = (!isFunction || includeOverride) && !udaExclude;
732 
733                     static if (includeMember)
734                     {
735                         string memberName = member;
736 
737                         if (memberName.endsWith("_"))
738                         {
739                             memberName = memberName[0 .. $ - 1];
740                         }
741 
742                         bool labeled = true;
743 
744                         static if (udaUnlabeled)
745                         {
746                             labeled = false;
747                         }
748 
749                         if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty))
750                         {
751                             labeled = false;
752                         }
753 
754                         static if (udaLabeled)
755                         {
756                             labeled = true;
757                         }
758 
759                         string membervalue = `this.` ~ member;
760 
761                         static if (udaToStringHandler)
762                         {
763                             alias Handlers = getUDAs!(symbol, ToStringHandler);
764 
765                             static assert(Handlers.length == 1);
766 
767                             static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init)))
768                             {
769                                 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(`
770                                     ~ membervalue
771                                     ~ `)`;
772                             }
773                             else
774                             {
775                                 return `static assert(false, "cannot determine how to call ToStringHandler");`;
776                             }
777                         }
778 
779                         string writestmt;
780 
781                         if (labeled)
782                         {
783                             writestmt = format!`sink.sinkWrite(comma, "%s=%%s", %s);`
784                                 (memberName, membervalue);
785                         }
786                         else
787                         {
788                             writestmt = format!`sink.sinkWrite(comma, "%%s", %s);`(membervalue);
789                         }
790 
791                         static if (udaOptional)
792                         {
793                             import std.array : empty;
794 
795                             static if (__traits(compiles, typeof(symbol).init.empty))
796                             {
797                                 result ~= format!`import std.array : empty; if (!%s.empty) { %s }`
798                                     (membervalue, writestmt);
799                             }
800                             else static if (__traits(compiles, typeof(symbol).init !is null))
801                             {
802                                 result ~= format!`if (%s !is null) { %s }`
803                                     (membervalue, writestmt);
804                             }
805                             else static if (__traits(compiles, typeof(symbol).init != 0))
806                             {
807                                 result ~= format!`if (%s != 0) { %s }`
808                                     (membervalue, writestmt);
809                             }
810                             else static if (__traits(compiles, { if (typeof(symbol).init) { } }))
811                             {
812                                 result ~= format!`if (%s) { %s }`
813                                     (membervalue, writestmt);
814                             }
815                             else
816                             {
817                                 return format!(`static assert(false, `
818                                         ~ `"don't know how to figure out whether %s is present.");`)
819                                     (member);
820                             }
821                         }
822                         else
823                         {
824                             result ~= writestmt;
825                         }
826                     }
827                 }
828 
829                 result ~= `} `;
830 
831                 if (!nakedMode)
832                 {
833                     result ~= `sink(")");`;
834                 }
835 
836                 static if (synchronize)
837                 {
838                     result ~= `} `;
839                 }
840 
841                 result ~= `} `;
842             }
843 
844             // generate fallback string toString()
845             // that calls, specifically, *our own* toString impl.
846             // (this is important to break cycles when a subclass implements a toString that calls super.toString)
847             static if (isObject)
848             {
849                 result ~= `override `;
850             }
851 
852             result ~= `public string toString() const {`
853                 ~ `string result;`
854                 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });`
855                 ~ `return result;`
856             ~ `}`;
857         }
858         return result;
859     }
860 }
861 
862 template checkAttributeConsistency(Attributes...)
863 {
864     enum checkAttributeConsistency = checkAttributeHelper();
865 
866     private string checkAttributeHelper()
867     {
868         if (!__ctfe)
869         {
870             return null;
871         }
872 
873         import std.string : format;
874 
875         bool include, exclude, optional, labeled, unlabeled;
876 
877         foreach (uda; Attributes)
878         {
879             static if (is(typeof(uda) == ToString))
880             {
881                 switch (uda)
882                 {
883                     case ToString.Include: include = true; break;
884                     case ToString.Exclude: exclude = true; break;
885                     case ToString.Optional: optional = true; break;
886                     case ToString.Labeled: labeled = true; break;
887                     case ToString.Unlabeled: unlabeled = true; break;
888                     default: break;
889                 }
890             }
891         }
892 
893         if (include && exclude)
894         {
895             return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`;
896         }
897 
898         if (include && optional)
899         {
900             return `static assert(false, "Redundant tags on '%s': Optional implies Include");`;
901         }
902 
903         if (exclude && optional)
904         {
905             return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`;
906         }
907 
908         if (labeled && unlabeled)
909         {
910             return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`;
911         }
912 
913         return null;
914     }
915 }
916 
917 struct ToStringHandler(alias Handler_)
918 {
919     alias Handler = Handler_;
920 }
921 
922 enum ToString
923 {
924     // these go on the class
925     Naked,
926     IncludeSuper,
927     ExcludeSuper,
928 
929     // these go on the field/method
930     Unlabeled,
931     Labeled,
932     Exclude,
933     Include,
934     Optional,
935 }
936 
937 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty)
938 {
939     import std.string : toLower;
940     import std.range.primitives : ElementType, isInputRange;
941 
942     static if (isInputRange!Type)
943     {
944         alias BaseType = ElementType!Type;
945 
946         if (field.toLower == BaseType.stringof.toLower.pluralize && attribNonEmpty)
947         {
948             return true;
949         }
950     }
951 
952     return field.toLower == Type.stringof.toLower
953         || field.toLower == "time" && Type.stringof == "SysTime"
954         || field.toLower == "id" && is(typeof(Type.toString));
955 }
956 
957 // http://code.activestate.com/recipes/82102/
958 private string pluralize(string label)
959 {
960     import std.algorithm.searching : contain = canFind;
961 
962     string postfix = "s";
963     if (label.length > 2)
964     {
965         enum vowels = "aeiou";
966 
967         if (label.stringEndsWith("ch") || label.stringEndsWith("sh"))
968         {
969             postfix = "es";
970         }
971         else if (auto before = label.stringEndsWith("y"))
972         {
973             if (!vowels.contain(label[$ - 2]))
974             {
975                 postfix = "ies";
976                 label = before;
977             }
978         }
979         else if (auto before = label.stringEndsWith("is"))
980         {
981             postfix = "es";
982             label = before;
983         }
984         else if ("sxz".contain(label[$-1]))
985         {
986             postfix = "es"; // glasses
987         }
988     }
989     return label ~ postfix;
990 }
991 
992 @("has functioning pluralize()")
993 unittest
994 {
995     "dog".pluralize.shouldEqual("dogs");
996     "ash".pluralize.shouldEqual("ashes");
997     "day".pluralize.shouldEqual("days");
998     "entity".pluralize.shouldEqual("entities");
999     "thesis".pluralize.shouldEqual("theses");
1000     "glass".pluralize.shouldEqual("glasses");
1001 }
1002 
1003 private string stringEndsWith(const string text, const string suffix)
1004 {
1005     import std.range : dropBack;
1006     import std.string : endsWith;
1007 
1008     if (text.endsWith(suffix))
1009     {
1010         return text.dropBack(suffix.length);
1011     }
1012     return null;
1013 }
1014 
1015 @("has functioning stringEndsWith()")
1016 unittest
1017 {
1018     "".stringEndsWith("").shouldNotBeNull;
1019     "".stringEndsWith("x").shouldBeNull;
1020     "Hello".stringEndsWith("Hello").shouldNotBeNull;
1021     "Hello".stringEndsWith("Hello").shouldEqual("");
1022     "Hello".stringEndsWith("lo").shouldEqual("Hel");
1023 }
1024 
1025 template hasOwnFunction(Aggregate, Super, string Name, Type)
1026 {
1027     import std.meta : AliasSeq, Filter;
1028     import std.traits : Unqual;
1029     enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type);
1030 
1031     alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name));
1032     alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions);
1033     enum hasFunction = MatchingFunctions.length == 1;
1034 
1035     alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name));
1036     alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions);
1037     enum superHasFunction = SuperMatchingFunctions.length == 1;
1038 
1039     static if (hasFunction)
1040     {
1041         static if (superHasFunction)
1042         {
1043             enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]);
1044         }
1045         else
1046         {
1047             enum hasOwnFunction = true;
1048         }
1049     }
1050     else
1051     {
1052         enum hasOwnFunction = false;
1053     }
1054 }
1055 
1056 private final abstract class StringToStringSample {
1057     override string toString();
1058 }
1059 
1060 private final abstract class VoidToStringSample {
1061     void toString(scope void delegate(const(char)[]) sink);
1062 }
1063 
1064 enum hasOwnStringToString(Aggregate, Super)
1065     = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString));
1066 
1067 enum hasOwnVoidToString(Aggregate, Super)
1068     = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString));
1069 
1070 @("correctly recognizes the existence of string toString() in a class")
1071 unittest
1072 {
1073     class Class1
1074     {
1075         override string toString() { return null; }
1076         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1077         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1078     }
1079 
1080     class Class2
1081     {
1082         override string toString() const { return null; }
1083         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1084         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1085     }
1086 
1087     class Class3
1088     {
1089         void toString(scope void delegate(const(char)[]) sink) const { }
1090         override string toString() const { return null; }
1091         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1092         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1093     }
1094 
1095     class Class4
1096     {
1097         void toString(scope void delegate(const(char)[]) sink) const { }
1098         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1099         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1100     }
1101 
1102     class Class5
1103     {
1104         mixin(GenerateToString);
1105     }
1106 
1107     class ChildClass1 : Class1
1108     {
1109         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1110     }
1111 
1112     class ChildClass2 : Class2
1113     {
1114         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1115     }
1116 
1117     class ChildClass3 : Class3
1118     {
1119         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1120     }
1121 
1122     class ChildClass5 : Class5
1123     {
1124         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1125     }
1126 }