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