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