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         static if (isNullable)
275         {
276             if (!arg.isNull)
277             {
278                 sink.sinkWrite(comma, escapeStrings, fmt, arg.get);
279             }
280             return;
281         }
282     }
283 
284     auto replaceArg(int i)()
285     if (i >= 0 && i < T.length)
286     {
287         alias PlainT = typeof(cast() args[i]);
288 
289         static if (is(PlainT == SysTime))
290         {
291             static struct SysTimeInitWrapper
292             {
293                 const typeof(args[i]) arg;
294 
295                 void toString(scope void delegate(const(char)[]) sink) const
296                 {
297                     if (this.arg is SysTime.init) // crashes on toString
298                     {
299                         sink("SysTime.init");
300                     }
301                     else
302                     {
303                         wrapFormatType(this.arg, false).toString(sink);
304                     }
305                 }
306             }
307 
308             return SysTimeInitWrapper(args[i]);
309         }
310         else
311         {
312             return wrapFormatType(args[i], escapeStrings);
313         }
314     }
315 
316     if (comma)
317     {
318         sink(", ");
319     }
320 
321     comma = true;
322 
323     mixin(`sink.formattedWrite(fmt, ` ~ T.length.iota.map!(i => format!"replaceArg!%s"(i)).join(", ") ~ `);`);
324 }
325 
326 private auto wrapFormatType(T)(T value, bool escapeStrings)
327 {
328     import std.traits : isSomeString;
329 
330     static if (__traits(compiles, customToString(value, (void delegate(const(char)[])).init)))
331     {
332         static struct CustomToStringWrapper
333         {
334             T value;
335 
336             void toString(scope void delegate(const(char)[]) sink) const
337             {
338                 customToString(this.value, sink);
339             }
340         }
341         return CustomToStringWrapper(value);
342     }
343     else static if (is(T : V[K], K, V))
344     {
345         return orderedAssociativeArray(value);
346     }
347     else static if (isSomeString!T)
348     {
349         static struct QuoteStringWrapper
350         {
351             T value;
352 
353             bool escapeStrings;
354 
355             void toString(scope void delegate(const(char)[]) sink) const
356             {
357                 import std.format : formattedWrite;
358                 import std.range : only;
359 
360                 if (escapeStrings)
361                 {
362                     sink.formattedWrite!"%(%s%)"(this.value.only);
363                 }
364                 else
365                 {
366                     sink.formattedWrite!"%s"(this.value);
367                 }
368             }
369         }
370 
371         return QuoteStringWrapper(value, escapeStrings);
372     }
373     else
374     {
375         return value;
376     }
377 }
378 
379 private auto orderedAssociativeArray(T : V[K], K, V)(T associativeArray)
380 {
381     static struct OrderedAssociativeArray
382     {
383         T associativeArray;
384 
385         public void toString(scope void delegate(const(char)[]) sink) const
386         {
387             import std.algorithm : sort;
388             sink("[");
389 
390             bool comma = false;
391 
392             foreach (key; this.associativeArray.keys.sort)
393             {
394                 sink.sinkWrite(comma, true, "%s: %s", key, this.associativeArray[key]);
395             }
396             sink("]");
397         }
398     }
399 
400     return OrderedAssociativeArray(associativeArray);
401 }
402 
403 private string quote(string text)
404 {
405     import std.string : replace;
406 
407     return `"` ~ text.replace(`\`, `\\`).replace(`"`, `\"`) ~ `"`;
408 }
409 
410 private string genFormatFunctionImpl(string text)
411 {
412     import std.algorithm : findSplit;
413     import std.exception : enforce;
414     import std.format : format;
415     import std.range : empty;
416     import std.string : join;
417 
418     string[] fragments;
419 
420     string remainder = text;
421 
422     while (true)
423     {
424         auto splitLeft = remainder.findSplit("%(");
425 
426         if (splitLeft[1].empty)
427         {
428             break;
429         }
430 
431         auto splitRight = splitLeft[2].findSplit(")");
432 
433         enforce(!splitRight[1].empty, format!"Closing paren not found in '%s'"(remainder));
434         remainder = splitRight[2];
435 
436         fragments ~= quote(splitLeft[0]);
437         fragments ~= splitRight[0];
438     }
439     fragments ~= quote(remainder);
440 
441     return `string values(T)(T arg)
442     {
443         with (arg)
444         {
445             return ` ~ fragments.join(" ~ ") ~ `;
446         }
447     }`;
448 }
449 
450 public template formatNamed(string text)
451 {
452     mixin(genFormatFunctionImpl(text));
453 }
454 
455 ///
456 @("formatNamed replaces named keys with given values")
457 unittest
458 {
459     import std.typecons : tuple;
460     import unit_threaded.should;
461 
462     formatNamed!("Hello %(second) World %(first)%(second)!")
463         .values(tuple!("first", "second")("3", "5"))
464         .shouldEqual("Hello 5 World 35!");
465 }
466 
467 public T[] reorder(T)(T[] source, size_t[] newOrder)
468 in
469 {
470     import std.algorithm : sort;
471     import std.range : array, iota;
472 
473     // newOrder must be a permutation of source indices
474     assert(newOrder.dup.sort.array == source.length.iota.array);
475 }
476 body
477 {
478     import std.algorithm : map;
479     import std.range : array;
480 
481     return newOrder.map!(i => source[i]).array;
482 }
483 
484 @("reorder returns reordered array")
485 unittest
486 {
487     import unit_threaded.should;
488 
489     [1, 2, 3].reorder([0, 2, 1]).shouldEqual([1, 3, 2]);
490 }
491 
492 // TODO replace with Nullable once pr 19037 is merged
493 public struct Optional(T)
494 {
495     import std.typecons : Nullable;
496 
497     // workaround: types in union are not destructed
498     union DontCallDestructor { SafeUnqual!T t; }
499 
500     // workaround: types in struct are memcpied in move/moveEmplace, bypassing constness
501     struct UseMemcpyMove { DontCallDestructor u; }
502 
503     private UseMemcpyMove value = UseMemcpyMove.init;
504 
505     public bool isNull = true;
506 
507     public this(T value)
508     {
509         this.value = UseMemcpyMove(DontCallDestructor(value));
510         this.isNull = false;
511     }
512 
513     // This method should only be called from Builder.value! Builder fields are semantically write-only.
514     public inout(T) _get() inout
515     in
516     {
517         assert(!this.isNull);
518     }
519     do
520     {
521         return this.value.u.t;
522     }
523 
524     public void opAssign(T value)
525     {
526         import std.algorithm : moveEmplace, move;
527 
528         auto valueCopy = UseMemcpyMove(DontCallDestructor(value));
529 
530         if (this.isNull)
531         {
532             moveEmplace(valueCopy, this.value);
533 
534             this.isNull = false;
535         }
536         else
537         {
538             move(valueCopy, this.value);
539         }
540     }
541 
542     public void opOpAssign(string op, RHS)(RHS rhs)
543     if (__traits(compiles, mixin("T.init " ~ op ~ " RHS.init")))
544     {
545         if (this.isNull)
546         {
547             this = T.init;
548         }
549         mixin("this = this._get " ~ op ~ " rhs;");
550     }
551 
552     static if (is(T: Nullable!Arg, Arg))
553     {
554         public void opAssign(Arg value)
555         {
556             this = T(value);
557         }
558     }
559 
560     static if (is(T == struct) && hasElaborateDestructor!T)
561     {
562         ~this()
563         {
564             if (!this.isNull)
565             {
566                 destroy(this.value.u.t);
567             }
568         }
569     }
570 }
571 
572 ///
573 unittest
574 {
575     Optional!(int[]) intArrayOptional;
576 
577     assert(intArrayOptional.isNull);
578 
579     intArrayOptional ~= 5;
580 
581     assert(!intArrayOptional.isNull);
582     assert(intArrayOptional._get == [5]);
583 
584     intArrayOptional ~= 6;
585 
586     assert(intArrayOptional._get == [5, 6]);
587 }
588 
589 private template SafeUnqual(T)
590 {
591     static if (__traits(compiles, (T t) { Unqual!T ut = t; }))
592     {
593         alias SafeUnqual = Unqual!T;
594     }
595     else
596     {
597         alias SafeUnqual = T;
598     }
599 }
600 
601 public string removeTrailingUnderline(string name)
602 {
603     import std.string : endsWith;
604 
605     return name.endsWith("_") ? name[0 .. $ - 1] : name;
606 }