1 module boilerplate.util;
2 
3 import std.algorithm : map, sort;
4 import std.format;
5 import std.meta;
6 import std.range : array, iota;
7 import std.string : join;
8 import std.traits;
9 
10 static if (__traits(compiles, { import config.string : toString; }))
11 {
12     import config.string : customToString = toString;
13 }
14 else
15 {
16     private void customToString(T)()
17     if (false)
18     {
19     }
20 }
21 
22 enum needToDup(T) = (isArray!T || isAssociativeArray!T) && !DeepConst!T;
23 
24 enum DeepConst(T) = __traits(compiles, (const T x) { T y = x; });
25 
26 @("needToDup correctly handles common types")
27 @nogc nothrow pure @safe unittest
28 {
29     int integerField;
30     int[] integerArrayField;
31 
32     static assert(!needToDup!(typeof(integerField)));
33     static assert(needToDup!(typeof(integerArrayField)));
34 }
35 
36 @("needToDup correctly handles const types")
37 @nogc nothrow pure @safe unittest
38 {
39     const(int)[] constIntegerArrayField;
40     string stringField;
41 
42     static assert(!needToDup!(typeof(constIntegerArrayField)));
43     static assert(!needToDup!(typeof(stringField)));
44 }
45 
46 @("doesn't add write-only properties to NormalMembers")
47 unittest
48 {
49     struct Test
50     {
51         @property void foo(int i) { }
52         mixin GenNormalMemberTuple;
53         static assert(is(NormalMemberTuple == AliasSeq!()),
54             "write-only properties should not appear in NormalMembers because they have no type"
55         );
56     }
57 }
58 
59 @("doesn't add read properties to NormalMembers if includeFunctions is false")
60 unittest
61 {
62     struct Test
63     {
64         @property int foo() { return 0; }
65         int bar() { return 0; }
66         mixin GenNormalMemberTuple;
67         static assert(is(NormalMemberTuple == AliasSeq!()),
68             "read properties should not appear in NormalMembers if includeFunctions is false"
69         );
70     }
71 }
72 
73 /**
74  * Generate AliasSeq of "normal" members - ie. no templates, no alias, no enum, only fields
75  * (and functions if includeFunctions is true).
76  */
77 mixin template GenNormalMemberTuple(bool includeFunctions = false)
78 {
79     import boilerplate.util : GenNormalMembersCheck, GenNormalMembersImpl;
80     import std.meta : AliasSeq;
81 
82     mixin(`alias NormalMemberTuple = ` ~ GenNormalMembersImpl([__traits(derivedMembers, typeof(this))],
83         mixin(GenNormalMembersCheck([__traits(derivedMembers, typeof(this))], includeFunctions))) ~ `;`);
84 }
85 
86 string GenNormalMembersCheck(string[] members, bool includeFunctions)
87 {
88     import std.format : format;
89     import std.string : join;
90 
91     string code = "[";
92     foreach (i, member; members)
93     {
94         if (i > 0)
95         {
96             code ~= ", "; // don't .map.join because this is compile performance critical code
97         }
98 
99         if (member != "this")
100         {
101             string check = `__traits(compiles, &typeof(this)[].init[0].` ~ member ~ `)`
102                 ~ ` && __traits(compiles, typeof(typeof(this).init.` ~ member ~ `))`;
103 
104             if (!includeFunctions)
105             {
106                 check ~= ` && !is(typeof(typeof(this).` ~ member ~ `) == function)`
107                     ~ ` && !is(typeof(&typeof(this).init.` ~ member ~ `) == delegate)`;
108             }
109 
110             code ~= check;
111         }
112         else
113         {
114             code ~= `false`;
115         }
116     }
117     code ~= "]";
118 
119     return code;
120 }
121 
122 string GenNormalMembersImpl(string[] members, bool[] compiles)
123 {
124     import std.string : join;
125 
126     string[] names;
127 
128     foreach (i, member; members)
129     {
130         if (member != "this" && compiles[i])
131         {
132             names ~= "\"" ~ member ~ "\"";
133         }
134     }
135 
136     return "AliasSeq!(" ~ names.join(", ") ~ ")";
137 }
138 
139 template getOverloadLike(Aggregate, string Name, Type)
140 {
141     alias Overloads = AliasSeq!(__traits(getOverloads, Aggregate, Name));
142     enum FunctionMatchesType(alias Fun) = is(typeof(Fun) == Type);
143     alias MatchingOverloads = Filter!(FunctionMatchesType, Overloads);
144 
145     static assert(MatchingOverloads.length == 1);
146 
147     alias getOverloadLike = MatchingOverloads[0];
148 }
149 
150 template udaIndex(alias attr, attributes...)
151 {
152     enum udaIndex = helper();
153 
154     ptrdiff_t helper()
155     {
156         if (!__ctfe)
157         {
158             return 0;
159         }
160         static if (attributes.length)
161         {
162             foreach (i, attrib; attributes)
163             {
164                 enum lastAttrib = i + 1 == attributes.length;
165 
166                 static if (__traits(isTemplate, attr))
167                 {
168                     static if (__traits(isSame, attrib, attr))
169                     {
170                         return i;
171                     }
172                     else static if (is(attrib: attr!Args, Args...))
173                     {
174                         return i;
175                     }
176                     else static if (lastAttrib)
177                     {
178                         return -1;
179                     }
180                 }
181                 else static if (__traits(compiles, is(typeof(attrib) == typeof(attr)) && attrib == attr))
182                 {
183                     static if (is(typeof(attrib) == typeof(attr)) && attrib == attr)
184                     {
185                         return i;
186                     }
187                     else static if (lastAttrib)
188                     {
189                         return -1;
190                     }
191                 }
192                 else static if (__traits(compiles, typeof(attrib)) && __traits(compiles, is(typeof(attrib) == attr)))
193                 {
194                     static if (is(typeof(attrib) == attr))
195                     {
196                         return i;
197                     }
198                     else static if (lastAttrib)
199                     {
200                         return -1;
201                     }
202                 }
203                 else static if (__traits(compiles, is(attrib == attr)))
204                 {
205                     static if (is(attrib == attr))
206                     {
207                         return i;
208                     }
209                     else static if (lastAttrib)
210                     {
211                         return -1;
212                     }
213                 }
214                 else static if (lastAttrib)
215                 {
216                     return -1;
217                 }
218             }
219         }
220         else
221         {
222             return -1;
223         }
224     }
225 }
226 
227 string isStatic(string field)
228 {
229     return `__traits(getOverloads, typeof(this), "` ~ field ~ `").length == 0`
230       ~ ` && __traits(compiles, &this.` ~ field ~ `)`;
231 }
232 
233 string isUnsafe(string field)
234 {
235     return isStatic(field) ~ ` && !__traits(compiles, () @safe { return this.` ~ field ~ `; })`;
236 }
237 
238 // a stable, simple O(n) sort optimal for a small number of sort keys
239 T[] bucketSort(T)(T[] inputArray, size_t delegate(T) rankfn)
240 {
241     import std.algorithm : joiner;
242     import std.range : array;
243 
244     T[][] buckets;
245 
246     foreach (element; inputArray)
247     {
248         auto rank = rankfn(element);
249 
250         if (rank >= buckets.length)
251         {
252             buckets.length = rank + 1;
253         }
254 
255         buckets[rank] ~= element;
256     }
257 
258     return buckets.joiner.array;
259 }
260 
261 void sinkWrite(T...)(scope void delegate(const(char)[]) sink, ref bool comma, bool escapeStrings, string fmt, T args)
262 {
263     import std.datetime : SysTime;
264     import std.format : formattedWrite;
265     import std.typecons : Nullable;
266 
267     static if (T.length == 1) // check for optional field: single Nullable
268     {
269         const arg = args[0];
270 
271         alias PlainT = typeof(cast() arg);
272 
273         enum isNullable = is(PlainT: Nullable!Arg, Arg);
274     }
275     else
276     {
277         enum isNullable = false;
278     }
279 
280     static if (isNullable)
281     {
282         if (!arg.isNull)
283         {
284             sink.sinkWrite(comma, escapeStrings, fmt, arg.get);
285         }
286         else
287         {
288             sink.sinkWrite(comma, false, fmt, "Nullable.null");
289         }
290         return;
291     }
292     else
293     {
294         auto replaceArg(int i)()
295         if (i >= 0 && i < T.length)
296         {
297             alias PlainT = typeof(cast() args[i]);
298 
299             static if (is(PlainT == SysTime))
300             {
301                 static struct SysTimeInitWrapper
302                 {
303                     const typeof(args[i]) arg;
304 
305                     void toString(scope void delegate(const(char)[]) sink) const
306                     {
307                         if (this.arg is SysTime.init) // crashes on toString
308                         {
309                             sink("SysTime.init");
310                         }
311                         else
312                         {
313                             wrapFormatType(this.arg, false).toString(sink);
314                         }
315                     }
316                 }
317 
318                 return SysTimeInitWrapper(args[i]);
319             }
320             else
321             {
322                 return wrapFormatType(args[i], escapeStrings);
323             }
324         }
325 
326         if (comma)
327         {
328             sink(", ");
329         }
330 
331         comma = true;
332 
333         mixin(`sink.formattedWrite(fmt, ` ~ replaceArgHelper!(T.length) ~ `);`);
334     }
335 }
336 
337 private enum replaceArgHelper(size_t length) = length.iota.map!(i => format!"replaceArg!%s"(i)).join(", ");
338 
339 private auto wrapFormatType(T)(T value, bool escapeStrings)
340 {
341     import std.traits : isSomeString;
342     import std.typecons : Nullable;
343 
344     // for Nullable types, we cannot distinguish between a custom handler that takes Nullable!Arg
345     // and one that takes Arg via alias get this. So handlers that take Nullable are impossible, since we
346     // need to handle it here to avoid crashes.
347     static if (is(T: Nullable!Arg, Arg))
348     {
349         static struct NullableWrapper
350         {
351             T value;
352 
353             bool escapeStrings;
354 
355             void toString(scope void delegate(const(char)[]) sink) const
356             {
357                 if (this.value.isNull)
358                 {
359                     sink("null");
360                 }
361                 else
362                 {
363                     wrapFormatType(this.value.get, escapeStrings).toString(sink);
364                 }
365             }
366         }
367         return NullableWrapper(value, escapeStrings);
368     }
369     else static if (__traits(compiles, customToString(value, (void delegate(const(char)[])).init)))
370     {
371         static struct CustomToStringWrapper
372         {
373             T value;
374 
375             void toString(scope void delegate(const(char)[]) sink) const
376             {
377                 customToString(this.value, sink);
378             }
379         }
380         return CustomToStringWrapper(value);
381     }
382     else static if (is(T : V[K], K, V))
383     {
384         static if (isOrderingComparable!K)
385         {
386             return orderedAssociativeArray(value);
387         }
388         else
389         {
390             import std.traits : fullyQualifiedName;
391 
392             // ansi escape codes. 0: reset, 1: bold, 93: bright yellow
393             pragma(msg, "\x1b[1;93mWarning\x1b[0m: Consistent ordering of type \x1b[1m" ~ T.stringof ~ "\x1b[0m " ~
394                 "on output cannot be guaranteed.");
395             pragma(msg, "         Please implement opCmp for \x1b[1m" ~ fullyQualifiedName!K ~ "\x1b[0m.");
396 
397             return value;
398         }
399     }
400     else static if (isSomeString!T)
401     {
402         static struct QuoteStringWrapper
403         {
404             T value;
405 
406             bool escapeStrings;
407 
408             void toString(scope void delegate(const(char)[]) sink) const
409             {
410                 import std.format : formattedWrite;
411                 import std.range : only;
412 
413                 if (escapeStrings)
414                 {
415                     sink.formattedWrite!"%(%s%)"(this.value.only);
416                 }
417                 else
418                 {
419                     sink.formattedWrite!"%s"(this.value);
420                 }
421             }
422         }
423 
424         return QuoteStringWrapper(value, escapeStrings);
425     }
426     else
427     {
428         return value;
429     }
430 }
431 
432 private auto orderedAssociativeArray(T : V[K], K, V)(T associativeArray)
433 {
434     static struct OrderedAssociativeArray
435     {
436         T associativeArray;
437 
438         public void toString(scope void delegate(const(char)[]) sink) const
439         {
440             import std.algorithm : sort;
441             sink("[");
442 
443             bool comma = false;
444 
445             foreach (key; this.associativeArray.keys.sort)
446             {
447                 sink.sinkWrite(comma, true, "%s: %s", key, this.associativeArray[key]);
448             }
449             sink("]");
450         }
451     }
452 
453     return OrderedAssociativeArray(associativeArray);
454 }
455 
456 private string quote(string text)
457 {
458     import std.string : replace;
459 
460     return `"` ~ text.replace(`\`, `\\`).replace(`"`, `\"`) ~ `"`;
461 }
462 
463 private string genFormatFunctionImpl(string text)
464 {
465     import std.algorithm : findSplit;
466     import std.exception : enforce;
467     import std.format : format;
468     import std.range : empty;
469     import std.string : join;
470 
471     string[] fragments;
472 
473     string remainder = text;
474 
475     while (true)
476     {
477         auto splitLeft = remainder.findSplit("%(");
478 
479         if (splitLeft[1].empty)
480         {
481             break;
482         }
483 
484         auto splitRight = splitLeft[2].findSplit(")");
485 
486         enforce(!splitRight[1].empty, format!"Closing paren not found in '%s'"(remainder));
487         remainder = splitRight[2];
488 
489         fragments ~= quote(splitLeft[0]);
490         fragments ~= splitRight[0];
491     }
492     fragments ~= quote(remainder);
493 
494     return `string values(T)(T arg)
495     {
496         with (arg)
497         {
498             return ` ~ fragments.join(" ~ ") ~ `;
499         }
500     }`;
501 }
502 
503 public template formatNamed(string text)
504 {
505     mixin(genFormatFunctionImpl(text));
506 }
507 
508 ///
509 @("formatNamed replaces named keys with given values")
510 unittest
511 {
512     import std.typecons : tuple;
513     import unit_threaded.should : shouldEqual;
514 
515     formatNamed!("Hello %(second) World %(first)%(second)!")
516         .values(tuple!("first", "second")("3", "5"))
517         .shouldEqual("Hello 5 World 35!");
518 }
519 
520 public T[] reorder(T)(T[] source, size_t[] newOrder)
521 // newOrder must be a permutation of source indices
522 in (newOrder.dup.sort.array == source.length.iota.array)
523 {
524     import std.algorithm : map;
525     import std.range : array;
526 
527     return newOrder.map!(i => source[i]).array;
528 }
529 
530 @("reorder returns reordered array")
531 unittest
532 {
533     import unit_threaded.should : shouldEqual;
534 
535     [1, 2, 3].reorder([0, 2, 1]).shouldEqual([1, 3, 2]);
536 }
537 
538 public struct Optional(T)
539 {
540     import std.typecons : Nullable;
541 
542     // workaround: types in union are not destructed
543     union DontCallDestructor { SafeUnqual!T t; }
544 
545     // workaround: types in struct are memcpied in move/moveEmplace, bypassing constness
546     struct UseMemcpyMove { DontCallDestructor u; }
547 
548     private UseMemcpyMove value = UseMemcpyMove.init;
549 
550     public bool isNull = true;
551 
552     public this(T value)
553     {
554         this.value = UseMemcpyMove(DontCallDestructor(value));
555         this.isNull = false;
556     }
557 
558     // This method should only be called from Builder.value! Builder fields are semantically write-only.
559     public inout(T) _get() inout
560     in
561     {
562         assert(!this.isNull);
563     }
564     do
565     {
566         return this.value.u.t;
567     }
568 
569     public U opAssign(U)(U value)
570     {
571         static if (is(U : Nullable!Arg, Arg))
572         {
573             import std.traits : Unqual;
574 
575             // fixup Nullable!(const T) -> Nullable!T
576             // Nullable!(const T) is a type that should not ever have been allowed to exist.
577             static if (is(T == Nullable!(Unqual!Arg)))
578             {
579                 static assert(is(Arg: Unqual!Arg), "Cannot assign Nullable!" ~ Arg.stringof ~ " to " ~ T.stringof);
580                 if (value.isNull)
581                 {
582                     _assign(T());
583                 }
584                 else
585                 {
586                     _assign(T(value.get));
587                 }
588             }
589             else
590             {
591                 // force-bypass the drug-fuelled `alias get this` idiocy by manually converting
592                 // value to the strictly (const) correct type for the assign() call
593                 T implConvertedValue = value;
594 
595                 _assign(implConvertedValue);
596             }
597         }
598         else
599         {
600             _assign(value);
601         }
602         return value;
603     }
604 
605     private void _assign(T value)
606     {
607         import std.algorithm : move, moveEmplace;
608 
609         auto valueCopy = UseMemcpyMove(DontCallDestructor(value));
610 
611         if (this.isNull)
612         {
613             moveEmplace(valueCopy, this.value);
614 
615             this.isNull = false;
616         }
617         else
618         {
619             move(valueCopy, this.value);
620         }
621     }
622 
623     public void opOpAssign(string op, RHS)(RHS rhs)
624     if (__traits(compiles, mixin("T.init " ~ op ~ " RHS.init")))
625     {
626         if (this.isNull)
627         {
628             this = T.init;
629         }
630         mixin("this = this._get " ~ op ~ " rhs;");
631     }
632 
633     static if (is(T: Nullable!Arg, Arg))
634     {
635         private void _assign(Arg value)
636         {
637             this = T(value);
638         }
639     }
640 
641     static if (is(T == struct) && hasElaborateDestructor!T)
642     {
643         ~this()
644         {
645             if (!this.isNull)
646             {
647                 destroy(this.value.u.t);
648             }
649         }
650     }
651 }
652 
653 ///
654 unittest
655 {
656     Optional!(int[]) intArrayOptional;
657 
658     assert(intArrayOptional.isNull);
659 
660     intArrayOptional ~= 5;
661 
662     assert(!intArrayOptional.isNull);
663     assert(intArrayOptional._get == [5]);
664 
665     intArrayOptional ~= 6;
666 
667     assert(intArrayOptional._get == [5, 6]);
668 }
669 
670 ///
671 @("optional correctly supports nullable assignment to const nullable of array")
672 unittest
673 {
674     import std.typecons : Nullable;
675 
676     Optional!(const(Nullable!int)) nullableArrayOptional;
677 
678     nullableArrayOptional = Nullable!int();
679 }
680 
681 private template SafeUnqual(T)
682 {
683     static if (__traits(compiles, (T t) { Unqual!T ut = t; }))
684     {
685         alias SafeUnqual = Unqual!T;
686     }
687     else
688     {
689         alias SafeUnqual = T;
690     }
691 }
692 
693 public string removeTrailingUnderline(string name)
694 {
695     import std.string : endsWith;
696 
697     return name.endsWith("_") ? name[0 .. $ - 1] : name;
698 }
699 
700 // Remove trailing underline iff the result would not be a reserved identifier
701 public enum optionallyRemoveTrailingUnderline(string name) =
702     isReservedIdentifier!(name.removeTrailingUnderline) ? name : name.removeTrailingUnderline;
703 
704 private enum isReservedIdentifier(string identifier) = !__traits(compiles, mixin(format!q{({ int %s; })}(identifier)));
705 
706 static assert(isReservedIdentifier!"version");
707 static assert(!isReservedIdentifier!"bla");
708 
709 /**
710  * manually reimplement `move`, `moveEmplace` because the phobos implementation of
711  * `moveEmplace` is **really, really bad!** it forces a recursive template
712  * instantiation over every primitive field in a struct, causing template overflows
713  * during compilation.
714  *
715  * See:
716  *      Phobos bug https://issues.dlang.org/show_bug.cgi?id=19689
717  *      Phobos fix https://github.com/dlang/phobos/pull/6873
718  */
719 public void moveEmplace(T)(ref T source, ref T dest) @trusted
720 in (&dest !is &source)
721 {
722     import core.stdc.string : memcpy, memset;
723 
724     memcpy(&dest, &source, T.sizeof);
725     memset(&source, 0, T.sizeof);
726 }
727 
728 ///
729 public void move(T)(ref T source, ref T dest) @trusted
730 in (&dest !is &source)
731 {
732     import std.traits : hasElaborateDestructor;
733 
734     static if (hasElaborateDestructor!T)
735     {
736         dest.__xdtor();
737     }
738     moveEmplace(source, dest);
739 }