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 }