├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dub.json ├── dub.selections.json ├── src └── boilerplate │ ├── accessors.d │ ├── autostring.d │ ├── builder.d │ ├── conditions.d │ ├── constructor.d │ ├── package.d │ ├── toString │ └── util.d └── unittest └── config └── string.d /.gitignore: -------------------------------------------------------------------------------- 1 | *.[oa] 2 | /.dub 3 | /boilerplate-test-library 4 | /boilerplate 5 | *.lst 6 | /build/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | os: 4 | - linux 5 | 6 | language: d 7 | 8 | d: 9 | - dmd-2.090.1 10 | - dmd-2.094.2 11 | 12 | env: 13 | matrix: 14 | - ARCH=x86_64 15 | 16 | script: 17 | - dub test --build=unittest --arch=$ARCH 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repo disabled! 2 | 3 | We've moved to [funkwerk-mobility/boilerplate](https://github.com/funkwerk-mobility/boilerplate)! 4 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate", 3 | "description": "D boilerplate code generator", 4 | "license": "BSL-1.0", 5 | "authors": [ 6 | "Mathis Beer" 7 | ], 8 | 9 | "targetType": "library", 10 | 11 | "configurations": [ 12 | { 13 | "name": "library" 14 | }, 15 | { 16 | "name": "unittest", 17 | "targetType": "executable", 18 | "preBuildCommands": ["$DUB run --compiler=$$DC unit-threaded -c gen_ut_main -- -f build/ut.d"], 19 | "mainSourceFile": "build/ut.d", 20 | "dependencies": { 21 | "unit-threaded": "*" 22 | }, 23 | "sourcePaths": ["src", "unittest"], 24 | "importPaths": ["src", "unittest"] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "unit-threaded": "1.0.11" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/boilerplate/accessors.d: -------------------------------------------------------------------------------- 1 | module boilerplate.accessors; 2 | 3 | import boilerplate.conditions : generateChecksForAttributes, IsConditionAttribute; 4 | import boilerplate.util: DeepConst, isStatic; 5 | import std.meta : StdMetaFilter = Filter; 6 | import std.traits; 7 | import std.typecons: Nullable; 8 | 9 | struct Read 10 | { 11 | string visibility = "public"; 12 | } 13 | 14 | // Deprecated! See below. 15 | // RefRead can not check invariants on change, so there's no point. 16 | // ref property functions where the value being returned is a field of the class 17 | // are entirely equivalent to public fields. 18 | struct RefRead 19 | { 20 | string visibility = "public"; 21 | } 22 | 23 | struct ConstRead 24 | { 25 | string visibility = "public"; 26 | } 27 | 28 | struct Write 29 | { 30 | string visibility = "public"; 31 | } 32 | 33 | immutable string GenerateFieldAccessors = ` 34 | import boilerplate.accessors : GenerateFieldAccessorMethods; 35 | mixin GenerateFieldAccessorMethods; 36 | mixin(GenerateFieldAccessorMethodsImpl); 37 | `; 38 | 39 | public static string GenerateFieldDecls_(FieldType, Attributes...) 40 | (string name, bool synchronize, bool fieldIsStatic, bool fieldIsUnsafe) 41 | { 42 | if (!__ctfe) 43 | { 44 | return null; 45 | } 46 | 47 | string result; 48 | 49 | import boilerplate.accessors : 50 | ConstRead, 51 | GenerateConstReader, GenerateReader, GenerateRefReader, GenerateWriter, 52 | Read, RefRead, Write; 53 | import boilerplate.util : udaIndex; 54 | 55 | static if (udaIndex!(Read, Attributes) != -1) 56 | { 57 | string readerDecl = GenerateReader!(FieldType, Attributes)( 58 | name, fieldIsStatic, fieldIsUnsafe, synchronize); 59 | 60 | debug (accessors) pragma(msg, readerDecl); 61 | result ~= readerDecl; 62 | } 63 | 64 | static if (udaIndex!(RefRead, Attributes) != -1) 65 | { 66 | result ~= `pragma(msg, "Deprecation! RefRead on " ~ typeof(this).stringof ~ ".` ~ name 67 | ~ ` makes a private field effectively public, defeating the point.");`; 68 | 69 | string refReaderDecl = GenerateRefReader!(FieldType)(name, fieldIsStatic); 70 | 71 | debug (accessors) pragma(msg, refReaderDecl); 72 | result ~= refReaderDecl; 73 | } 74 | 75 | static if (udaIndex!(ConstRead, Attributes) != -1) 76 | { 77 | string constReaderDecl = GenerateConstReader!(const(FieldType), Attributes) 78 | (name, fieldIsStatic, fieldIsUnsafe, synchronize); 79 | 80 | debug (accessors) pragma(msg, constReaderDecl); 81 | result ~= constReaderDecl; 82 | } 83 | 84 | static if (udaIndex!(Write, Attributes) != -1) 85 | { 86 | string writerDecl = GenerateWriter!(FieldType, Attributes) 87 | (name, `this.` ~ name, fieldIsStatic, fieldIsUnsafe, synchronize); 88 | 89 | debug (accessors) pragma(msg, writerDecl); 90 | result ~= writerDecl; 91 | } 92 | 93 | return result; 94 | } 95 | 96 | mixin template GenerateFieldAccessorMethods() 97 | { 98 | private static string GenerateFieldAccessorMethodsImpl() 99 | { 100 | if (!__ctfe) 101 | { 102 | return null; 103 | } 104 | 105 | import boilerplate.accessors : GenerateFieldDecls_; 106 | import boilerplate.util : GenNormalMemberTuple, isStatic, isUnsafe; 107 | 108 | string result = ""; 109 | 110 | mixin GenNormalMemberTuple; 111 | 112 | foreach (name; NormalMemberTuple) 113 | { 114 | // synchronized without lock contention is basically free, so always do it 115 | // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 116 | // enum synchronize = is(typeof(__traits(getMember, typeof(this), name)) == class); 117 | enum fieldIsStatic = mixin(name.isStatic); 118 | enum fieldIsUnsafe = mixin(name.isUnsafe); 119 | 120 | result ~= GenerateFieldDecls_!( 121 | typeof(__traits(getMember, typeof(this), name)), 122 | __traits(getAttributes, __traits(getMember, typeof(this), name)) 123 | ) (name, /*synchronize*/false, fieldIsStatic, fieldIsUnsafe); 124 | } 125 | 126 | return result; 127 | } 128 | } 129 | 130 | string getModifiers(bool isStatic) 131 | { 132 | return isStatic ? " static" : ""; 133 | } 134 | 135 | uint filterAttributes(T)(bool isStatic, bool isUnsafe, FilterMode mode) 136 | { 137 | import boilerplate.util : needToDup; 138 | 139 | uint attributes = uint.max; 140 | 141 | if (needToDup!T) 142 | { 143 | attributes &= ~FunctionAttribute.nogc; 144 | static if (isAssociativeArray!T) 145 | { 146 | // int[int].dup can throw apparently 147 | attributes &= ~FunctionAttribute.nothrow_; 148 | } 149 | } 150 | // Nullable.opAssign is not nogc 151 | if (mode == FilterMode.Writer && isInstanceOf!(Nullable, T)) 152 | { 153 | attributes &= ~FunctionAttribute.nogc; 154 | } 155 | // TODO remove once synchronized (this) is nothrow 156 | // see https://github.com/dlang/druntime/pull/2105 , https://github.com/dlang/dmd/pull/7942 157 | if (is(T == class)) 158 | { 159 | attributes &= ~FunctionAttribute.nothrow_; 160 | } 161 | if (isStatic) 162 | { 163 | attributes &= ~FunctionAttribute.pure_; 164 | } 165 | if (isUnsafe) 166 | { 167 | attributes &= ~FunctionAttribute.safe; 168 | } 169 | return attributes; 170 | } 171 | 172 | enum FilterMode 173 | { 174 | Reader, 175 | Writer, 176 | } 177 | 178 | string GenerateReader(T, Attributes...)(string name, bool fieldIsStatic, bool fieldIsUnsafe, bool synchronize) 179 | { 180 | import boilerplate.util : needToDup; 181 | import std.string : format; 182 | 183 | auto example = T.init; 184 | auto accessorName = accessor(name); 185 | enum visibility = getVisibility!(Read, __traits(getAttributes, example)); 186 | enum needToDupField = needToDup!T; 187 | 188 | static if (isAssociativeArray!T) 189 | { 190 | fieldIsUnsafe = true; 191 | } 192 | 193 | uint attributes = inferAttributes!(T, "__postblit") & 194 | filterAttributes!T(fieldIsStatic, fieldIsUnsafe, FilterMode.Reader); 195 | 196 | string attributesString = generateAttributeString(attributes); 197 | string accessorBody; 198 | string type; 199 | 200 | if (fieldIsStatic) 201 | { 202 | type = format!`typeof(this.%s)`(name); 203 | } 204 | else 205 | { 206 | type = format!`inout(typeof(this.%s))`(name); 207 | } 208 | 209 | // for types like string where the contents are already const or value, 210 | // so we can safely reassign to a non-const type 211 | static if (needToDupField) 212 | { 213 | static if (isArray!T) 214 | { 215 | accessorBody = format!`return typeof(this.%s).init ~ this.%s;`(name, name); 216 | } 217 | else static if (isAssociativeArray!T) 218 | { 219 | accessorBody = format!`return cast(typeof(this.%s)) this.%s.dup;`(name, name); 220 | } 221 | else 222 | { 223 | static assert(false, "logic error: need to dup but don't know how"); 224 | } 225 | } 226 | else static if (DeepConst!(Unqual!T) && !is(Unqual!T == T)) 227 | { 228 | // necessitated by DMD bug https://issues.dlang.org/show_bug.cgi?id=18545 229 | accessorBody = format!`typeof(cast() this.%s) var = this.%s; return var;`(name, name); 230 | type = format!`typeof(cast() this.%s)`(name); 231 | } 232 | else 233 | { 234 | accessorBody = format!`return this.%s;`(name); 235 | } 236 | 237 | if (synchronize) 238 | { 239 | accessorBody = format!`synchronized (this) { %s} `(accessorBody); 240 | } 241 | 242 | auto modifiers = getModifiers(fieldIsStatic); 243 | 244 | if (!fieldIsStatic) 245 | { 246 | attributesString ~= " inout"; 247 | } 248 | 249 | string outCondition = ""; 250 | 251 | if (fieldIsStatic) 252 | { 253 | if (auto checks = generateChecksForAttributes!(T, StdMetaFilter!(IsConditionAttribute, Attributes)) 254 | ("result", " in postcondition of @Read")) 255 | { 256 | outCondition = format!` out(result) { %s } do`(checks); 257 | } 258 | } 259 | 260 | return format!("%s%s final @property %s %s()%s%s { %s }") 261 | (visibility, modifiers, type, accessorName, attributesString, outCondition, accessorBody); 262 | } 263 | 264 | @("generates readers as expected") 265 | @nogc nothrow pure @safe unittest 266 | { 267 | int integerValue; 268 | string stringValue; 269 | int[] intArrayValue; 270 | const string constStringValue; 271 | 272 | static assert(GenerateReader!int("foo", true, false, false) == 273 | "public static final @property typeof(this.foo) foo() " ~ 274 | "@nogc nothrow @safe { return this.foo; }"); 275 | static assert(GenerateReader!string("foo", true, false, false) == 276 | "public static final @property typeof(this.foo) foo() " ~ 277 | "@nogc nothrow @safe { return this.foo; }"); 278 | static assert(GenerateReader!(int[])("foo", true, false, false) == 279 | "public static final @property typeof(this.foo) foo() nothrow @safe " 280 | ~ "{ return typeof(this.foo).init ~ this.foo; }"); 281 | static assert(GenerateReader!(const string)("foo", true, false, false) == 282 | "public static final @property typeof(cast() this.foo) foo() @nogc nothrow @safe " 283 | ~ "{ typeof(cast() this.foo) var = this.foo; return var; }"); 284 | } 285 | 286 | string GenerateRefReader(T)(string name, bool isStatic) 287 | { 288 | import std.string : format; 289 | 290 | auto example = T.init; 291 | auto accessorName = accessor(name); 292 | enum visibility = getVisibility!(RefRead, __traits(getAttributes, example)); 293 | 294 | string attributesString; 295 | if (isStatic) 296 | { 297 | attributesString = "@nogc nothrow @safe "; 298 | } 299 | else 300 | { 301 | attributesString = "@nogc nothrow pure @safe "; 302 | } 303 | 304 | auto modifiers = getModifiers(isStatic); 305 | 306 | // no need to synchronize a reference read 307 | return format("%s%s final @property ref typeof(this.%s) %s() " ~ 308 | "%s{ return this.%s; }", 309 | visibility, modifiers, name, accessorName, attributesString, name); 310 | } 311 | 312 | @("generates ref readers as expected") 313 | @nogc nothrow pure @safe unittest 314 | { 315 | static assert(GenerateRefReader!int("foo", true) == 316 | "public static final @property ref typeof(this.foo) foo() " ~ 317 | "@nogc nothrow @safe { return this.foo; }"); 318 | static assert(GenerateRefReader!string("foo", true) == 319 | "public static final @property ref typeof(this.foo) foo() " ~ 320 | "@nogc nothrow @safe { return this.foo; }"); 321 | static assert(GenerateRefReader!(int[])("foo", true) == 322 | "public static final @property ref typeof(this.foo) foo() " ~ 323 | "@nogc nothrow @safe { return this.foo; }"); 324 | } 325 | 326 | string GenerateConstReader(T, Attributes...)(string name, bool isStatic, bool isUnsafe, bool synchronize) 327 | { 328 | import std.string : format; 329 | 330 | auto example = T.init; 331 | auto accessorName = accessor(name); 332 | enum visibility = getVisibility!(ConstRead, __traits(getAttributes, example)); 333 | 334 | uint attributes = inferAttributes!(T, "__postblit") & 335 | filterAttributes!T(isStatic, isUnsafe, FilterMode.Reader); 336 | 337 | string attributesString = generateAttributeString(attributes); 338 | 339 | static if (DeepConst!(Unqual!T) && !is(Unqual!T == T)) 340 | { 341 | // necessitated by DMD bug https://issues.dlang.org/show_bug.cgi?id=18545 342 | string accessorBody = format!`typeof(cast() this.%s) var = this.%s; return var;`(name, name); 343 | string type = format!`typeof(cast() const(typeof(this)).init.%s)`(name); 344 | } 345 | else 346 | { 347 | string accessorBody = format!`return this.%s; `(name); 348 | string type = format!`const(typeof(this.%s))`(name); 349 | } 350 | 351 | if (synchronize) 352 | { 353 | accessorBody = format!`synchronized (this) { %s} `(accessorBody); 354 | } 355 | 356 | auto modifiers = getModifiers(isStatic); 357 | 358 | if (isStatic) 359 | { 360 | string outCondition = ""; 361 | 362 | if (auto checks = generateChecksForAttributes!(T, StdMetaFilter!(IsConditionAttribute, Attributes)) 363 | ("result", " in postcondition of @ConstRead")) 364 | { 365 | outCondition = format!` out(result) { %s } do`(checks); 366 | } 367 | 368 | return format("%s%s final @property %s %s()%s%s { %s}", 369 | visibility, modifiers, type, accessorName, attributesString, outCondition, accessorBody); 370 | } 371 | 372 | return format("%s%s final @property %s %s() const%s { %s}", 373 | visibility, modifiers, type, accessorName, attributesString, accessorBody); 374 | } 375 | 376 | string GenerateWriter(T, Attributes...)(string name, string fieldCode, bool isStatic, bool isUnsafe, bool synchronize) 377 | { 378 | import boilerplate.util : needToDup; 379 | import std.algorithm : canFind; 380 | import std.string : format; 381 | 382 | auto example = T.init; 383 | auto accessorName = accessor(name); 384 | auto inputName = accessorName; 385 | enum needToDupField = needToDup!T; 386 | enum visibility = getVisibility!(Write, __traits(getAttributes, example)); 387 | 388 | uint attributes = defaultFunctionAttributes & 389 | filterAttributes!T(isStatic, isUnsafe, FilterMode.Writer) & 390 | inferAssignAttributes!T & 391 | inferAttributes!(T, "__postblit") & 392 | inferAttributes!(T, "__dtor"); 393 | 394 | string precondition = ``; 395 | 396 | if (auto checks = generateChecksForAttributes!(T, StdMetaFilter!(IsConditionAttribute, Attributes)) 397 | (inputName, " in precondition of @Write")) 398 | { 399 | precondition = format!` in { import std.format : format; import std.array : empty; %s } do`(checks); 400 | attributes &= ~FunctionAttribute.nogc; 401 | attributes &= ~FunctionAttribute.nothrow_; 402 | // format() is neither pure nor safe 403 | if (checks.canFind("format")) 404 | { 405 | attributes &= ~FunctionAttribute.pure_; 406 | attributes &= ~FunctionAttribute.safe; 407 | } 408 | } 409 | 410 | auto attributesString = generateAttributeString(attributes); 411 | auto modifiers = getModifiers(isStatic); 412 | 413 | string accessorBody = format!`this.%s = %s%s; `(name, inputName, needToDupField ? ".dup" : ""); 414 | 415 | if (synchronize) 416 | { 417 | accessorBody = format!`synchronized (this) { %s} `(accessorBody); 418 | } 419 | 420 | string result = format("%s%s final @property void %s(typeof(%s) %s)%s%s { %s}", 421 | visibility, modifiers, accessorName, fieldCode, inputName, 422 | attributesString, precondition, accessorBody); 423 | 424 | static if (is(T : Nullable!Arg, Arg)) 425 | { 426 | result ~= format("%s%s final @property void %s(typeof(%s.get) %s)%s%s { %s}", 427 | visibility, modifiers, accessorName, fieldCode, inputName, 428 | attributesString, precondition, accessorBody); 429 | } 430 | return result; 431 | } 432 | 433 | @("generates writers as expected") 434 | @nogc nothrow pure @safe unittest 435 | { 436 | static assert(GenerateWriter!int("foo", "integerValue", true, false, false) == 437 | "public static final @property void foo(typeof(integerValue) foo) " ~ 438 | "@nogc nothrow @safe { this.foo = foo; }"); 439 | static assert(GenerateWriter!string("foo", "stringValue", true, false, false) == 440 | "public static final @property void foo(typeof(stringValue) foo) " ~ 441 | "@nogc nothrow @safe { this.foo = foo; }"); 442 | static assert(GenerateWriter!(int[])("foo", "intArrayValue", true, false, false) == 443 | "public static final @property void foo(typeof(intArrayValue) foo) " ~ 444 | "nothrow @safe { this.foo = foo.dup; }"); 445 | } 446 | 447 | @("generates same-type writer for Nullable") 448 | pure @safe unittest 449 | { 450 | import std.typecons : Nullable, nullable; 451 | import unit_threaded.should : shouldEqual; 452 | 453 | struct Struct 454 | { 455 | @Write 456 | Nullable!int optional_; 457 | 458 | mixin(GenerateFieldAccessors); 459 | } 460 | 461 | Struct value; 462 | 463 | value.optional = 5; 464 | 465 | value.optional_.shouldEqual(5.nullable); 466 | } 467 | 468 | private enum uint defaultFunctionAttributes = 469 | FunctionAttribute.nogc | 470 | FunctionAttribute.safe | 471 | FunctionAttribute.nothrow_ | 472 | FunctionAttribute.pure_; 473 | 474 | private template inferAttributes(T, string M) 475 | { 476 | uint inferAttributes() 477 | { 478 | uint attributes = defaultFunctionAttributes; 479 | 480 | static if (is(T == struct)) 481 | { 482 | static if (hasMember!(T, M)) 483 | { 484 | attributes &= functionAttributes!(__traits(getMember, T, M)); 485 | } 486 | else 487 | { 488 | foreach (field; Fields!T) 489 | { 490 | attributes &= inferAttributes!(field, M); 491 | } 492 | } 493 | } 494 | return attributes; 495 | } 496 | } 497 | 498 | private template inferAssignAttributes(T) 499 | { 500 | uint inferAssignAttributes() 501 | { 502 | uint attributes = defaultFunctionAttributes; 503 | 504 | static if (is(T == struct)) 505 | { 506 | static if (hasMember!(T, "opAssign")) 507 | { 508 | foreach (o; __traits(getOverloads, T, "opAssign")) 509 | { 510 | alias params = Parameters!o; 511 | static if (params.length == 1 && is(params[0] == T)) 512 | { 513 | attributes &= functionAttributes!o; 514 | } 515 | } 516 | } 517 | else 518 | { 519 | foreach (field; Fields!T) 520 | { 521 | attributes &= inferAssignAttributes!field; 522 | } 523 | } 524 | } 525 | return attributes; 526 | } 527 | } 528 | 529 | private string generateAttributeString(uint attributes) 530 | { 531 | string attributesString; 532 | 533 | if (attributes & FunctionAttribute.nogc) 534 | { 535 | attributesString ~= " @nogc"; 536 | } 537 | if (attributes & FunctionAttribute.nothrow_) 538 | { 539 | attributesString ~= " nothrow"; 540 | } 541 | if (attributes & FunctionAttribute.pure_) 542 | { 543 | attributesString ~= " pure"; 544 | } 545 | if (attributes & FunctionAttribute.safe) 546 | { 547 | attributesString ~= " @safe"; 548 | } 549 | 550 | return attributesString; 551 | } 552 | 553 | private string accessor(string name) @nogc nothrow pure @safe 554 | { 555 | import std.string : chomp, chompPrefix; 556 | 557 | return name.chomp("_").chompPrefix("_"); 558 | } 559 | 560 | @("removes underlines from names") 561 | @nogc nothrow pure @safe unittest 562 | { 563 | assert(accessor("foo_") == "foo"); 564 | assert(accessor("_foo") == "foo"); 565 | } 566 | 567 | /** 568 | * Returns a string with the value of the field "visibility" if the attributes 569 | * include an UDA of type A. The default visibility is "public". 570 | */ 571 | template getVisibility(A, attributes...) 572 | { 573 | import std.string : format; 574 | 575 | enum getVisibility = helper; 576 | 577 | private static helper() 578 | { 579 | static if (!attributes.length) 580 | { 581 | return A.init.visibility; 582 | } 583 | else 584 | { 585 | foreach (i, uda; attributes) 586 | { 587 | static if (is(typeof(uda) == A)) 588 | { 589 | return uda.visibility; 590 | } 591 | else static if (is(uda == A)) 592 | { 593 | return A.init.visibility; 594 | } 595 | else static if (i + 1 == attributes.length) 596 | { 597 | return A.init.visibility; 598 | } 599 | } 600 | } 601 | } 602 | } 603 | 604 | @("applies visibility from the uda parameter") 605 | @nogc nothrow pure @safe unittest 606 | { 607 | @Read("public") int publicInt; 608 | @Read("package") int packageInt; 609 | @Read("protected") int protectedInt; 610 | @Read("private") int privateInt; 611 | @Read int defaultVisibleInt; 612 | @Read @Write("protected") int publicReadableProtectedWritableInt; 613 | 614 | static assert(getVisibility!(Read, __traits(getAttributes, publicInt)) == "public"); 615 | static assert(getVisibility!(Read, __traits(getAttributes, packageInt)) == "package"); 616 | static assert(getVisibility!(Read, __traits(getAttributes, protectedInt)) == "protected"); 617 | static assert(getVisibility!(Read, __traits(getAttributes, privateInt)) == "private"); 618 | static assert(getVisibility!(Read, __traits(getAttributes, defaultVisibleInt)) == "public"); 619 | static assert(getVisibility!(Read, __traits(getAttributes, publicReadableProtectedWritableInt)) == "public"); 620 | static assert(getVisibility!(Write, __traits(getAttributes, publicReadableProtectedWritableInt)) == "protected"); 621 | } 622 | 623 | @("creates accessors for flags") 624 | nothrow pure @safe unittest 625 | { 626 | import std.typecons : Flag, No, Yes; 627 | 628 | class Test 629 | { 630 | @Read 631 | @Write 632 | public Flag!"someFlag" test_ = Yes.someFlag; 633 | 634 | mixin(GenerateFieldAccessors); 635 | } 636 | 637 | with (new Test) 638 | { 639 | assert(test == Yes.someFlag); 640 | 641 | test = No.someFlag; 642 | 643 | assert(test == No.someFlag); 644 | 645 | static assert(is(typeof(test) == Flag!"someFlag")); 646 | } 647 | } 648 | 649 | @("creates accessors for nullables") 650 | nothrow pure @safe unittest 651 | { 652 | import std.typecons : Nullable; 653 | 654 | class Test 655 | { 656 | @Read @Write 657 | public Nullable!string test_ = Nullable!string("X"); 658 | 659 | mixin(GenerateFieldAccessors); 660 | } 661 | 662 | with (new Test) 663 | { 664 | assert(!test.isNull); 665 | assert(test.get == "X"); 666 | 667 | static assert(is(typeof(test) == Nullable!string)); 668 | } 669 | } 670 | 671 | @("does not break with const Nullable accessor") 672 | nothrow pure @safe unittest 673 | { 674 | import std.typecons : Nullable; 675 | 676 | class Test 677 | { 678 | @Read 679 | private const Nullable!string test_; 680 | 681 | mixin(GenerateFieldAccessors); 682 | } 683 | 684 | with (new Test) 685 | { 686 | assert(test.isNull); 687 | } 688 | } 689 | 690 | @("creates non-const reader") 691 | nothrow pure @safe unittest 692 | { 693 | class Test 694 | { 695 | @Read 696 | int i_; 697 | 698 | mixin(GenerateFieldAccessors); 699 | } 700 | 701 | auto mutableObject = new Test; 702 | const constObject = mutableObject; 703 | 704 | mutableObject.i_ = 42; 705 | 706 | assert(mutableObject.i == 42); 707 | 708 | static assert(is(typeof(mutableObject.i) == int)); 709 | static assert(is(typeof(constObject.i) == const(int))); 710 | } 711 | 712 | @("creates ref reader") 713 | nothrow pure @safe unittest 714 | { 715 | class Test 716 | { 717 | @RefRead 718 | int i_; 719 | 720 | mixin(GenerateFieldAccessors); 721 | } 722 | 723 | auto mutableTestObject = new Test; 724 | 725 | mutableTestObject.i = 42; 726 | 727 | assert(mutableTestObject.i == 42); 728 | static assert(is(typeof(mutableTestObject.i) == int)); 729 | } 730 | 731 | @("creates writer") 732 | nothrow pure @safe unittest 733 | { 734 | class Test 735 | { 736 | @Read @Write 737 | private int i_; 738 | 739 | mixin(GenerateFieldAccessors); 740 | } 741 | 742 | auto mutableTestObject = new Test; 743 | mutableTestObject.i = 42; 744 | 745 | assert(mutableTestObject.i == 42); 746 | static assert(!__traits(compiles, mutableTestObject.i += 1)); 747 | static assert(is(typeof(mutableTestObject.i) == int)); 748 | } 749 | 750 | @("checks whether hasUDA can be used for each member") 751 | nothrow pure @safe unittest 752 | { 753 | class Test 754 | { 755 | alias Z = int; 756 | 757 | @Read @Write 758 | private int i_; 759 | 760 | mixin(GenerateFieldAccessors); 761 | } 762 | 763 | auto mutableTestObject = new Test; 764 | mutableTestObject.i = 42; 765 | 766 | assert(mutableTestObject.i == 42); 767 | static assert(!__traits(compiles, mutableTestObject.i += 1)); 768 | } 769 | 770 | @("returns non const for PODs and structs.") 771 | nothrow pure @safe unittest 772 | { 773 | import std.algorithm : map, sort; 774 | import std.array : array; 775 | 776 | class C 777 | { 778 | @Read 779 | string s_; 780 | 781 | mixin(GenerateFieldAccessors); 782 | } 783 | 784 | C[] a = null; 785 | 786 | static assert(__traits(compiles, a.map!(c => c.s).array.sort())); 787 | } 788 | 789 | @("functions with strings") 790 | nothrow pure @safe unittest 791 | { 792 | class C 793 | { 794 | @Read @Write 795 | string s_; 796 | 797 | mixin(GenerateFieldAccessors); 798 | } 799 | 800 | with (new C) 801 | { 802 | s = "foo"; 803 | assert(s == "foo"); 804 | static assert(is(typeof(s) == string)); 805 | } 806 | } 807 | 808 | @("supports user-defined accessors") 809 | nothrow pure @safe unittest 810 | { 811 | class C 812 | { 813 | this() 814 | { 815 | str_ = "foo"; 816 | } 817 | 818 | @RefRead 819 | private string str_; 820 | 821 | public @property const(string) str() const 822 | { 823 | return this.str_.dup; 824 | } 825 | 826 | mixin(GenerateFieldAccessors); 827 | } 828 | 829 | with (new C) 830 | { 831 | str = "bar"; 832 | } 833 | } 834 | 835 | @("creates accessor for locally defined types") 836 | @system unittest 837 | { 838 | class X 839 | { 840 | } 841 | 842 | class Test 843 | { 844 | @Read 845 | public X x_; 846 | 847 | mixin(GenerateFieldAccessors); 848 | } 849 | 850 | with (new Test) 851 | { 852 | x_ = new X; 853 | 854 | assert(x == x_); 855 | static assert(is(typeof(x) == X)); 856 | } 857 | } 858 | 859 | @("creates const reader for simple structs") 860 | nothrow pure @safe unittest 861 | { 862 | class Test 863 | { 864 | struct S 865 | { 866 | int i; 867 | } 868 | 869 | @Read 870 | S s_; 871 | 872 | mixin(GenerateFieldAccessors); 873 | } 874 | 875 | auto mutableObject = new Test; 876 | const constObject = mutableObject; 877 | 878 | mutableObject.s_.i = 42; 879 | 880 | assert(constObject.s.i == 42); 881 | 882 | static assert(is(typeof(mutableObject.s) == Test.S)); 883 | static assert(is(typeof(constObject.s) == const(Test.S))); 884 | } 885 | 886 | @("returns copies when reading structs") 887 | nothrow pure @safe unittest 888 | { 889 | class Test 890 | { 891 | struct S 892 | { 893 | int i; 894 | } 895 | 896 | @Read 897 | S s_; 898 | 899 | mixin(GenerateFieldAccessors); 900 | } 901 | 902 | auto mutableObject = new Test; 903 | 904 | mutableObject.s.i = 42; 905 | 906 | assert(mutableObject.s.i == int.init); 907 | } 908 | 909 | @("works with const arrays") 910 | nothrow pure @safe unittest 911 | { 912 | class X 913 | { 914 | } 915 | 916 | class C 917 | { 918 | @Read 919 | private const(X)[] foo_; 920 | 921 | mixin(GenerateFieldAccessors); 922 | } 923 | 924 | auto x = new X; 925 | 926 | with (new C) 927 | { 928 | foo_ = [x]; 929 | 930 | auto y = foo; 931 | 932 | static assert(is(typeof(y) == const(X)[])); 933 | static assert(is(typeof(foo) == const(X)[])); 934 | } 935 | } 936 | 937 | @("has correct type of int") 938 | nothrow pure @safe unittest 939 | { 940 | class C 941 | { 942 | @Read 943 | private int foo_; 944 | 945 | mixin(GenerateFieldAccessors); 946 | } 947 | 948 | with (new C) 949 | { 950 | static assert(is(typeof(foo) == int)); 951 | } 952 | } 953 | 954 | @("works under inheritance (https://github.com/funkwerk/accessors/issues/5)") 955 | @nogc nothrow pure @safe unittest 956 | { 957 | class A 958 | { 959 | @Read 960 | string foo_; 961 | 962 | mixin(GenerateFieldAccessors); 963 | } 964 | 965 | class B : A 966 | { 967 | @Read 968 | string bar_; 969 | 970 | mixin(GenerateFieldAccessors); 971 | } 972 | } 973 | 974 | @("transfers struct attributes") 975 | @nogc nothrow pure @safe unittest 976 | { 977 | struct S 978 | { 979 | this(this) 980 | { 981 | } 982 | 983 | void opAssign(S s) 984 | { 985 | } 986 | } 987 | 988 | class A 989 | { 990 | @Read 991 | S[] foo_; 992 | 993 | @ConstRead 994 | S bar_; 995 | 996 | @Write 997 | S baz_; 998 | 999 | mixin(GenerateFieldAccessors); 1000 | } 1001 | } 1002 | 1003 | @("returns array with mutable elements when reading") 1004 | nothrow pure @safe unittest 1005 | { 1006 | struct Field 1007 | { 1008 | } 1009 | 1010 | struct S 1011 | { 1012 | @Read 1013 | Field[] foo_; 1014 | 1015 | mixin(GenerateFieldAccessors); 1016 | } 1017 | 1018 | with (S()) 1019 | { 1020 | Field[] arr = foo; 1021 | } 1022 | } 1023 | 1024 | @("ConstRead with tailconst type") 1025 | nothrow pure @safe unittest 1026 | { 1027 | struct S 1028 | { 1029 | @ConstRead 1030 | string foo_; 1031 | 1032 | mixin(GenerateFieldAccessors); 1033 | } 1034 | 1035 | auto var = S().foo; 1036 | 1037 | static assert(is(typeof(var) == string)); 1038 | } 1039 | 1040 | @("generates safe static properties for static members") 1041 | @safe unittest 1042 | { 1043 | class MyStaticTest 1044 | { 1045 | @Read 1046 | static int stuff_ = 8; 1047 | 1048 | mixin(GenerateFieldAccessors); 1049 | } 1050 | 1051 | assert(MyStaticTest.stuff == 8); 1052 | } 1053 | 1054 | @safe unittest 1055 | { 1056 | struct S 1057 | { 1058 | @Read @Write 1059 | static int foo_ = 8; 1060 | 1061 | @RefRead 1062 | static int bar_ = 6; 1063 | 1064 | mixin(GenerateFieldAccessors); 1065 | } 1066 | 1067 | assert(S.foo == 8); 1068 | static assert(is(typeof({ S.foo = 8; }))); 1069 | assert(S.bar == 6); 1070 | } 1071 | 1072 | @("does not set @safe on accessors for static __gshared members") 1073 | unittest 1074 | { 1075 | class Test 1076 | { 1077 | @Read 1078 | __gshared int stuff_ = 8; 1079 | 1080 | mixin(GenerateFieldAccessors); 1081 | } 1082 | 1083 | assert(Test.stuff == 8); 1084 | } 1085 | 1086 | @("does not set inout on accessors for static fields") 1087 | unittest 1088 | { 1089 | class Test 1090 | { 1091 | @Read 1092 | __gshared Object[] stuff_; 1093 | 1094 | mixin(GenerateFieldAccessors); 1095 | } 1096 | } 1097 | 1098 | unittest 1099 | { 1100 | struct Thing 1101 | { 1102 | @Read 1103 | private int[] content_; 1104 | 1105 | mixin(GenerateFieldAccessors); 1106 | } 1107 | 1108 | class User 1109 | { 1110 | void helper(const int[]) 1111 | { 1112 | } 1113 | 1114 | void doer(const Thing thing) 1115 | { 1116 | helper(thing.content); 1117 | } 1118 | } 1119 | } 1120 | 1121 | @("dups nullable arrays") 1122 | unittest 1123 | { 1124 | class Class 1125 | { 1126 | } 1127 | 1128 | struct Thing 1129 | { 1130 | @Read 1131 | private Class[] classes_; 1132 | 1133 | mixin(GenerateFieldAccessors); 1134 | } 1135 | 1136 | const Thing thing; 1137 | 1138 | assert(thing.classes.length == 0); 1139 | } 1140 | 1141 | @("dups associative arrays") 1142 | unittest 1143 | { 1144 | struct Thing 1145 | { 1146 | @Read 1147 | @Write 1148 | private int[int] value_; 1149 | 1150 | mixin(GenerateFieldAccessors); 1151 | } 1152 | 1153 | auto thing = Thing([2: 3]); 1154 | auto value = [2: 3]; 1155 | 1156 | thing.value = value; 1157 | 1158 | // overwrite return value of @Read 1159 | thing.value[2] = 4; 1160 | // overwrite source value of @Write 1161 | value[2] = 4; 1162 | 1163 | // member value is still unchanged. 1164 | assert(thing.value == [2: 3]); 1165 | } 1166 | 1167 | @("generates invariant checks via precondition for writers") 1168 | unittest 1169 | { 1170 | import boilerplate.conditions : AllNonNull; 1171 | import core.exception : AssertError; 1172 | import std.algorithm : canFind; 1173 | import std.conv : to; 1174 | import unit_threaded.should : shouldThrow; 1175 | 1176 | class Thing 1177 | { 1178 | @Write @AllNonNull 1179 | Object[] objects_; 1180 | 1181 | this(Object[] objects) 1182 | { 1183 | this.objects_ = objects.dup; 1184 | } 1185 | 1186 | mixin(GenerateFieldAccessors); 1187 | } 1188 | 1189 | auto thing = new Thing([new Object]); 1190 | 1191 | auto error = ({ thing.objects = [null]; })().shouldThrow!AssertError; 1192 | 1193 | assert(error.to!string.canFind("in precondition")); 1194 | } 1195 | 1196 | @("generates out conditions for invariant tags on static accessors") 1197 | unittest 1198 | { 1199 | import boilerplate.conditions : NonInit; 1200 | import core.exception : AssertError; 1201 | import unit_threaded.should : shouldThrow; 1202 | 1203 | struct Struct 1204 | { 1205 | @Read 1206 | @NonInit 1207 | static int test_; 1208 | 1209 | @ConstRead 1210 | @NonInit 1211 | static int test2_; 1212 | 1213 | mixin(GenerateFieldAccessors); 1214 | } 1215 | 1216 | Struct.test.shouldThrow!AssertError; 1217 | Struct.test2.shouldThrow!AssertError; 1218 | } 1219 | -------------------------------------------------------------------------------- /src/boilerplate/autostring.d: -------------------------------------------------------------------------------- 1 | module boilerplate.autostring; 2 | 3 | import std.format : format; 4 | import std.meta : Alias; 5 | import std.traits : Unqual; 6 | 7 | version(unittest) 8 | { 9 | import std.conv : to; 10 | import std.datetime : SysTime; 11 | import unit_threaded.should; 12 | } 13 | 14 | /++ 15 | GenerateToString is a mixin string that automatically generates toString functions, 16 | both sink-based and classic, customizable with UDA annotations on classes, members and functions. 17 | +/ 18 | public enum string GenerateToString = ` 19 | import boilerplate.autostring : GenerateToStringTemplate; 20 | mixin GenerateToStringTemplate; 21 | mixin(typeof(this).generateToStringErrCheck()); 22 | mixin(typeof(this).generateToStringImpl()); 23 | `; 24 | 25 | /++ 26 | When used with objects, toString methods of type string toString() are also created. 27 | +/ 28 | @("generates legacy toString on objects") 29 | unittest 30 | { 31 | class Class 32 | { 33 | mixin(GenerateToString); 34 | } 35 | 36 | (new Class).to!string.shouldEqual("Class()"); 37 | (new Class).toString.shouldEqual("Class()"); 38 | } 39 | 40 | /++ 41 | A trailing underline in member names is removed when labeling. 42 | +/ 43 | @("removes trailing underline") 44 | unittest 45 | { 46 | struct Struct 47 | { 48 | int a_; 49 | mixin(GenerateToString); 50 | } 51 | 52 | Struct.init.to!string.shouldEqual("Struct(a=0)"); 53 | } 54 | 55 | /++ 56 | The `@(ToString.Exclude)` tag can be used to exclude a member. 57 | +/ 58 | @("can exclude a member") 59 | unittest 60 | { 61 | struct Struct 62 | { 63 | @(ToString.Exclude) 64 | int a; 65 | mixin(GenerateToString); 66 | } 67 | 68 | Struct.init.to!string.shouldEqual("Struct()"); 69 | } 70 | 71 | /++ 72 | The `@(ToString.Optional)` tag can be used to include a member only if it's in some form "present". 73 | This means non-empty for arrays, non-null for objects, non-zero for ints. 74 | +/ 75 | @("can optionally exclude member") 76 | unittest 77 | { 78 | import std.typecons : Nullable, nullable; 79 | 80 | class Class 81 | { 82 | mixin(GenerateToString); 83 | } 84 | 85 | struct Test // some type that is not comparable to null or 0 86 | { 87 | mixin(GenerateToString); 88 | } 89 | 90 | struct Struct 91 | { 92 | @(ToString.Optional) 93 | int a; 94 | 95 | @(ToString.Optional) 96 | string s; 97 | 98 | @(ToString.Optional) 99 | Class obj; 100 | 101 | @(ToString.Optional) 102 | Nullable!Test nullable; 103 | 104 | mixin(GenerateToString); 105 | } 106 | 107 | Struct.init.to!string.shouldEqual("Struct()"); 108 | Struct(2, "hi", new Class, Test().nullable).to!string 109 | .shouldEqual(`Struct(a=2, s="hi", obj=Class(), nullable=Test())`); 110 | Struct(0, "", null, Nullable!Test()).to!string.shouldEqual("Struct()"); 111 | } 112 | 113 | /++ 114 | The `@(ToString.Optional)` tag can be used with a condition parameter 115 | indicating when the type is to be _included._ 116 | +/ 117 | @("can pass exclusion condition to Optional") 118 | unittest 119 | { 120 | struct Struct 121 | { 122 | @(ToString.Optional!(a => a > 3)) 123 | int i; 124 | 125 | mixin(GenerateToString); 126 | } 127 | 128 | Struct.init.to!string.shouldEqual("Struct()"); 129 | Struct(3).to!string.shouldEqual("Struct()"); 130 | Struct(5).to!string.shouldEqual("Struct(i=5)"); 131 | } 132 | 133 | /++ 134 | The `@(ToString.Optional)` condition predicate 135 | can also take the whole data type. 136 | +/ 137 | @("can pass exclusion condition to Optional") 138 | unittest 139 | { 140 | struct Struct 141 | { 142 | @(ToString.Optional!(self => self.include)) 143 | int i; 144 | 145 | @(ToString.Exclude) 146 | bool include; 147 | 148 | mixin(GenerateToString); 149 | } 150 | 151 | Struct(5, false).to!string.shouldEqual("Struct()"); 152 | Struct(5, true).to!string.shouldEqual("Struct(i=5)"); 153 | } 154 | 155 | /++ 156 | The `@(ToString.Include)` tag can be used to explicitly include a member. 157 | This is intended to be used on property methods. 158 | +/ 159 | @("can include a method") 160 | unittest 161 | { 162 | struct Struct 163 | { 164 | @(ToString.Include) 165 | int foo() const { return 5; } 166 | mixin(GenerateToString); 167 | } 168 | 169 | Struct.init.to!string.shouldEqual("Struct(foo=5)"); 170 | } 171 | 172 | /++ 173 | The `@(ToString.Unlabeled)` tag will omit a field's name. 174 | +/ 175 | @("can omit names") 176 | unittest 177 | { 178 | struct Struct 179 | { 180 | @(ToString.Unlabeled) 181 | int a; 182 | mixin(GenerateToString); 183 | } 184 | 185 | Struct.init.to!string.shouldEqual("Struct(0)"); 186 | } 187 | 188 | /++ 189 | Parent class `toString()` methods are included automatically as the first entry, except if the parent class is `Object`. 190 | +/ 191 | @("can be used in both parent and child class") 192 | unittest 193 | { 194 | class ParentClass { mixin(GenerateToString); } 195 | 196 | class ChildClass : ParentClass { mixin(GenerateToString); } 197 | 198 | (new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())"); 199 | } 200 | 201 | @("invokes manually implemented parent toString") 202 | unittest 203 | { 204 | class ParentClass 205 | { 206 | override string toString() const 207 | { 208 | return "Some string"; 209 | } 210 | } 211 | 212 | class ChildClass : ParentClass { mixin(GenerateToString); } 213 | 214 | (new ChildClass).to!string.shouldEqual("ChildClass(Some string)"); 215 | } 216 | 217 | @("can partially override toString in child class") 218 | unittest 219 | { 220 | class ParentClass 221 | { 222 | mixin(GenerateToString); 223 | } 224 | 225 | class ChildClass : ParentClass 226 | { 227 | override string toString() const 228 | { 229 | return "Some string"; 230 | } 231 | 232 | mixin(GenerateToString); 233 | } 234 | 235 | (new ChildClass).to!string.shouldEqual("Some string"); 236 | } 237 | 238 | @("invokes manually implemented string toString in same class") 239 | unittest 240 | { 241 | class Class 242 | { 243 | override string toString() const 244 | { 245 | return "Some string"; 246 | } 247 | 248 | mixin(GenerateToString); 249 | } 250 | 251 | (new Class).to!string.shouldEqual("Some string"); 252 | } 253 | 254 | @("invokes manually implemented void toString in same class") 255 | unittest 256 | { 257 | class Class 258 | { 259 | void toString(scope void delegate(const(char)[]) sink) const 260 | { 261 | sink("Some string"); 262 | } 263 | 264 | mixin(GenerateToString); 265 | } 266 | 267 | (new Class).to!string.shouldEqual("Some string"); 268 | } 269 | 270 | /++ 271 | Inclusion of parent class `toString()` can be prevented using `@(ToString.ExcludeSuper)`. 272 | +/ 273 | @("can suppress parent class toString()") 274 | unittest 275 | { 276 | class ParentClass { } 277 | 278 | @(ToString.ExcludeSuper) 279 | class ChildClass : ParentClass { mixin(GenerateToString); } 280 | 281 | (new ChildClass).to!string.shouldEqual("ChildClass()"); 282 | } 283 | 284 | /++ 285 | The `@(ToString.Naked)` tag will omit the name of the type and parentheses. 286 | +/ 287 | @("can omit the type name") 288 | unittest 289 | { 290 | @(ToString.Naked) 291 | struct Struct 292 | { 293 | int a; 294 | mixin(GenerateToString); 295 | } 296 | 297 | Struct.init.to!string.shouldEqual("a=0"); 298 | } 299 | 300 | /++ 301 | Fields with the same name (ignoring capitalization) as their type, are unlabeled by default. 302 | +/ 303 | @("does not label fields with the same name as the type") 304 | unittest 305 | { 306 | struct Struct1 { mixin(GenerateToString); } 307 | 308 | struct Struct2 309 | { 310 | Struct1 struct1; 311 | mixin(GenerateToString); 312 | } 313 | 314 | Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 315 | } 316 | 317 | @("does not label fields with the same name as the type, even if they're const") 318 | unittest 319 | { 320 | struct Struct1 { mixin(GenerateToString); } 321 | 322 | struct Struct2 323 | { 324 | const Struct1 struct1; 325 | mixin(GenerateToString); 326 | } 327 | 328 | Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 329 | } 330 | 331 | @("does not label fields with the same name as the type, even if they're nullable") 332 | unittest 333 | { 334 | import std.typecons : Nullable; 335 | 336 | struct Struct1 { mixin(GenerateToString); } 337 | 338 | struct Struct2 339 | { 340 | const Nullable!Struct1 struct1; 341 | mixin(GenerateToString); 342 | } 343 | 344 | Struct2(Nullable!Struct1(Struct1())).to!string.shouldEqual("Struct2(Struct1())"); 345 | } 346 | 347 | /++ 348 | This behavior can be prevented by explicitly tagging the field with `@(ToString.Labeled)`. 349 | +/ 350 | @("does label fields tagged as labeled") 351 | unittest 352 | { 353 | struct Struct1 { mixin(GenerateToString); } 354 | 355 | struct Struct2 356 | { 357 | @(ToString.Labeled) 358 | Struct1 struct1; 359 | mixin(GenerateToString); 360 | } 361 | 362 | Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())"); 363 | } 364 | 365 | /++ 366 | Fields of type 'SysTime' and name 'time' are unlabeled by default. 367 | +/ 368 | @("does not label SysTime time field correctly") 369 | unittest 370 | { 371 | struct Struct { SysTime time; mixin(GenerateToString); } 372 | 373 | Struct strct; 374 | strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z"); 375 | 376 | // see unittest/config/string.d 377 | strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)"); 378 | } 379 | 380 | /++ 381 | Fields named 'id' are unlabeled only if they define their own toString(). 382 | +/ 383 | @("does not label id fields with toString()") 384 | unittest 385 | { 386 | struct IdType 387 | { 388 | string toString() const { return "ID"; } 389 | } 390 | 391 | struct Struct 392 | { 393 | IdType id; 394 | mixin(GenerateToString); 395 | } 396 | 397 | Struct.init.to!string.shouldEqual("Struct(ID)"); 398 | } 399 | 400 | /++ 401 | Otherwise, they are labeled as normal. 402 | +/ 403 | @("labels id fields without toString") 404 | unittest 405 | { 406 | struct Struct 407 | { 408 | int id; 409 | mixin(GenerateToString); 410 | } 411 | 412 | Struct.init.to!string.shouldEqual("Struct(id=0)"); 413 | } 414 | 415 | /++ 416 | Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default, 417 | as long as the array is NonEmpty. Otherwise, there would be no way to tell what the field contains. 418 | +/ 419 | @("does not label fields named a plural of the basetype, if the type is an array") 420 | unittest 421 | { 422 | import boilerplate.conditions : NonEmpty; 423 | 424 | struct Value { mixin(GenerateToString); } 425 | struct Entity { mixin(GenerateToString); } 426 | struct Day { mixin(GenerateToString); } 427 | 428 | struct Struct 429 | { 430 | @NonEmpty 431 | Value[] values; 432 | 433 | @NonEmpty 434 | Entity[] entities; 435 | 436 | @NonEmpty 437 | Day[] days; 438 | 439 | mixin(GenerateToString); 440 | } 441 | 442 | auto value = Struct( 443 | [Value()], 444 | [Entity()], 445 | [Day()]); 446 | 447 | value.to!string.shouldEqual("Struct([Value()], [Entity()], [Day()])"); 448 | } 449 | 450 | @("does not label fields named a plural of the basetype, if the type is a BitFlags") 451 | unittest 452 | { 453 | import std.typecons : BitFlags; 454 | 455 | enum Flag 456 | { 457 | A = 1 << 0, 458 | B = 1 << 1, 459 | } 460 | 461 | struct Struct 462 | { 463 | BitFlags!Flag flags; 464 | 465 | mixin(GenerateToString); 466 | } 467 | 468 | auto value = Struct(BitFlags!Flag(Flag.A, Flag.B)); 469 | 470 | value.to!string.shouldEqual("Struct(Flag(A, B))"); 471 | } 472 | 473 | /++ 474 | Fields that are not NonEmpty are always labeled. 475 | This is because they can be empty, in which case you can't tell what's in them from naming. 476 | +/ 477 | @("does label fields that may be empty") 478 | unittest 479 | { 480 | import boilerplate.conditions : NonEmpty; 481 | 482 | struct Value { mixin(GenerateToString); } 483 | 484 | struct Struct 485 | { 486 | Value[] values; 487 | 488 | mixin(GenerateToString); 489 | } 490 | 491 | Struct(null).to!string.shouldEqual("Struct(values=[])"); 492 | } 493 | 494 | /++ 495 | `GenerateToString` can be combined with `GenerateFieldAccessors` without issue. 496 | +/ 497 | @("does not collide with accessors") 498 | unittest 499 | { 500 | struct Struct 501 | { 502 | import boilerplate.accessors : ConstRead, GenerateFieldAccessors; 503 | 504 | @ConstRead 505 | private int a_; 506 | 507 | mixin(GenerateFieldAccessors); 508 | 509 | mixin(GenerateToString); 510 | } 511 | 512 | Struct.init.to!string.shouldEqual("Struct(a=0)"); 513 | } 514 | 515 | @("supports child classes of abstract classes") 516 | unittest 517 | { 518 | static abstract class ParentClass 519 | { 520 | } 521 | class ChildClass : ParentClass 522 | { 523 | mixin(GenerateToString); 524 | } 525 | } 526 | 527 | @("supports custom toString handlers") 528 | unittest 529 | { 530 | struct Struct 531 | { 532 | @ToStringHandler!(i => i ? "yes" : "no") 533 | int i; 534 | 535 | mixin(GenerateToString); 536 | } 537 | 538 | Struct.init.to!string.shouldEqual("Struct(i=no)"); 539 | } 540 | 541 | @("passes nullable unchanged to custom toString handlers") 542 | unittest 543 | { 544 | import std.typecons : Nullable; 545 | 546 | struct Struct 547 | { 548 | @ToStringHandler!(ni => ni.isNull ? "no" : "yes") 549 | Nullable!int ni; 550 | 551 | mixin(GenerateToString); 552 | } 553 | 554 | Struct.init.to!string.shouldEqual("Struct(ni=no)"); 555 | } 556 | 557 | // see unittest.config.string 558 | @("supports optional BitFlags in structs") 559 | unittest 560 | { 561 | import std.typecons : BitFlags; 562 | 563 | enum Enum 564 | { 565 | A = 1, 566 | B = 2, 567 | } 568 | 569 | struct Struct 570 | { 571 | @(ToString.Optional) 572 | BitFlags!Enum field; 573 | 574 | mixin(GenerateToString); 575 | } 576 | 577 | Struct.init.to!string.shouldEqual("Struct()"); 578 | } 579 | 580 | @("prints hashmaps in deterministic order") 581 | unittest 582 | { 583 | struct Struct 584 | { 585 | string[string] map; 586 | 587 | mixin(GenerateToString); 588 | } 589 | 590 | bool foundCollision = false; 591 | 592 | foreach (key1; ["opstop", "opsto"]) 593 | { 594 | enum key2 = "foo"; // collide 595 | 596 | const first = Struct([key1: null, key2: null]); 597 | string[string] backwardsHashmap; 598 | 599 | backwardsHashmap[key2] = null; 600 | backwardsHashmap[key1] = null; 601 | 602 | const second = Struct(backwardsHashmap); 603 | 604 | if (first.map.keys != second.map.keys) 605 | { 606 | foundCollision = true; 607 | first.to!string.shouldEqual(second.to!string); 608 | } 609 | } 610 | assert(foundCollision, "none of the listed keys caused a hash collision"); 611 | } 612 | 613 | @("applies custom formatters to types in hashmaps") 614 | unittest 615 | { 616 | import std.datetime : SysTime; 617 | 618 | struct Struct 619 | { 620 | SysTime[string] map; 621 | 622 | mixin(GenerateToString); 623 | } 624 | 625 | const expected = "2003-02-01T11:55:00Z"; 626 | const value = Struct(["foo": SysTime.fromISOExtString(expected)]); 627 | 628 | value.to!string.shouldEqual(`Struct(map=["foo": ` ~ expected ~ `])`); 629 | } 630 | 631 | @("can format associative array of Nullable SysTime") 632 | unittest 633 | { 634 | import std.datetime : SysTime; 635 | import std.typecons : Nullable; 636 | 637 | struct Struct 638 | { 639 | Nullable!SysTime[string] map; 640 | 641 | mixin(GenerateToString); 642 | } 643 | 644 | const expected = `Struct(map=["foo": null])`; 645 | const value = Struct(["foo": Nullable!SysTime()]); 646 | 647 | value.to!string.shouldEqual(expected); 648 | } 649 | 650 | @("can format associative array of type that cannot be sorted") 651 | unittest 652 | { 653 | struct Struct 654 | { 655 | mixin(GenerateToString); 656 | } 657 | 658 | struct Struct2 659 | { 660 | bool[Struct] hashmap; 661 | 662 | mixin(GenerateToString); 663 | } 664 | 665 | const expected = `Struct2(hashmap=[])`; 666 | const value = Struct2(null); 667 | 668 | value.to!string.shouldEqual(expected); 669 | } 670 | 671 | @("labels nested types with fully qualified names") 672 | unittest 673 | { 674 | import std.datetime : SysTime; 675 | import std.typecons : Nullable; 676 | 677 | struct Struct 678 | { 679 | struct Struct2 680 | { 681 | mixin(GenerateToString); 682 | } 683 | 684 | Struct2 struct2; 685 | 686 | mixin(GenerateToString); 687 | } 688 | 689 | const expected = `Struct(Struct.Struct2())`; 690 | const value = Struct(Struct.Struct2()); 691 | 692 | value.to!string.shouldEqual(expected); 693 | } 694 | 695 | @("supports fully qualified names with quotes") 696 | unittest 697 | { 698 | struct Struct(string s) 699 | { 700 | struct Struct2 701 | { 702 | mixin(GenerateToString); 703 | } 704 | 705 | Struct2 struct2; 706 | 707 | mixin(GenerateToString); 708 | } 709 | 710 | const expected = `Struct!"foo"(Struct!"foo".Struct2())`; 711 | const value = Struct!"foo"(Struct!"foo".Struct2()); 712 | 713 | value.to!string.shouldEqual(expected); 714 | } 715 | 716 | @("optional-always null Nullable") 717 | unittest 718 | { 719 | import std.typecons : Nullable; 720 | 721 | struct Struct 722 | { 723 | @(ToString.Optional!(a => true)) 724 | Nullable!int i; 725 | 726 | mixin(GenerateToString); 727 | } 728 | 729 | Struct().to!string.shouldEqual("Struct(i=Nullable.null)"); 730 | } 731 | 732 | @("force-included null Nullable") 733 | unittest 734 | { 735 | import std.typecons : Nullable; 736 | 737 | struct Struct 738 | { 739 | @(ToString.Include) 740 | Nullable!int i; 741 | 742 | mixin(GenerateToString); 743 | } 744 | 745 | Struct().to!string.shouldEqual("Struct(i=Nullable.null)"); 746 | } 747 | 748 | // test for clean detection of Nullable 749 | @("struct with isNull") 750 | unittest 751 | { 752 | struct Inner 753 | { 754 | bool isNull() const { return false; } 755 | 756 | mixin(GenerateToString); 757 | } 758 | 759 | struct Outer 760 | { 761 | Inner inner; 762 | 763 | mixin(GenerateToString); 764 | } 765 | 766 | Outer().to!string.shouldEqual("Outer(Inner())"); 767 | } 768 | 769 | // regression 770 | @("mutable struct with alias this of sink toString") 771 | unittest 772 | { 773 | struct Inner 774 | { 775 | public void toString(scope void delegate(const(char)[]) sink) const 776 | { 777 | sink("Inner()"); 778 | } 779 | } 780 | 781 | struct Outer 782 | { 783 | Inner inner; 784 | 785 | alias inner this; 786 | 787 | mixin(GenerateToString); 788 | } 789 | } 790 | 791 | @("immutable struct with alias this of const toString") 792 | unittest 793 | { 794 | struct Inner 795 | { 796 | string toString() const { return "Inner()"; } 797 | } 798 | 799 | immutable struct Outer 800 | { 801 | Inner inner; 802 | 803 | alias inner this; 804 | 805 | mixin(GenerateToString); 806 | } 807 | 808 | Outer().to!string.shouldEqual("Outer(Inner())"); 809 | } 810 | 811 | mixin template GenerateToStringTemplate() 812 | { 813 | // this is a separate function to reduce the 814 | // "warning: unreachable code" spam that is falsely created from static foreach 815 | private static generateToStringErrCheck() 816 | { 817 | if (!__ctfe) 818 | { 819 | return null; 820 | } 821 | 822 | import boilerplate.autostring : ToString, typeName; 823 | import boilerplate.util : GenNormalMemberTuple; 824 | import std.string : format; 825 | 826 | bool udaIncludeSuper; 827 | bool udaExcludeSuper; 828 | 829 | foreach (uda; __traits(getAttributes, typeof(this))) 830 | { 831 | static if (is(typeof(uda) == ToString)) 832 | { 833 | switch (uda) 834 | { 835 | case ToString.IncludeSuper: udaIncludeSuper = true; break; 836 | case ToString.ExcludeSuper: udaExcludeSuper = true; break; 837 | default: break; 838 | } 839 | } 840 | } 841 | 842 | if (udaIncludeSuper && udaExcludeSuper) 843 | { 844 | return format!(`static assert(false, ` ~ 845 | `"Contradictory tags on '" ~ %(%s%) ~ "': IncludeSuper and ExcludeSuper");`) 846 | ([typeName!(typeof(this))]); 847 | } 848 | 849 | mixin GenNormalMemberTuple!true; 850 | 851 | foreach (member; NormalMemberTuple) 852 | { 853 | enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member))); 854 | 855 | static if (error) 856 | { 857 | return format!error(member); 858 | } 859 | } 860 | 861 | return ``; 862 | } 863 | 864 | private static generateToStringImpl() 865 | { 866 | if (!__ctfe) 867 | { 868 | return null; 869 | } 870 | 871 | import boilerplate.autostring : 872 | hasOwnStringToString, hasOwnVoidToString, isMemberUnlabeledByDefault, ToString, typeName; 873 | import boilerplate.conditions : NonEmpty; 874 | import boilerplate.util : GenNormalMemberTuple, udaIndex; 875 | import std.meta : Alias; 876 | import std.string : endsWith, format, split, startsWith, strip; 877 | import std.traits : BaseClassesTuple, getUDAs, Unqual; 878 | import std.typecons : Nullable; 879 | 880 | // synchronized without lock contention is basically free, so always do it 881 | // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 882 | enum synchronize = false && is(typeof(this) == class); 883 | 884 | const constExample = typeof(this).init; 885 | auto normalExample = typeof(this).init; 886 | 887 | enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString") 888 | && is(typeof(normalExample.toString()) == string); 889 | enum alreadyHaveUsableStringToString = alreadyHaveStringToString 890 | && is(typeof(constExample.toString()) == string); 891 | 892 | enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString") 893 | && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void); 894 | enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString 895 | && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void); 896 | 897 | enum isObject = is(typeof(this): Object); 898 | 899 | static if (isObject) 900 | { 901 | enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super)); 902 | enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super)); 903 | } 904 | else 905 | { 906 | enum userDefinedStringToString = hasOwnStringToString!(typeof(this)); 907 | enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this)); 908 | } 909 | 910 | static if (userDefinedStringToString && userDefinedVoidToString) 911 | { 912 | string result = ``; // Nothing to be done. 913 | } 914 | // if the user has defined their own string toString() in this aggregate: 915 | else static if (userDefinedStringToString) 916 | { 917 | // just call it. 918 | static if (alreadyHaveUsableStringToString) 919 | { 920 | string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~ 921 | ` sink(this.toString());` ~ 922 | ` }`; 923 | 924 | static if (isObject 925 | && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 926 | { 927 | result = `override ` ~ result; 928 | } 929 | } 930 | else 931 | { 932 | string result = `static assert(false, "toString is not const in this class.");`; 933 | } 934 | } 935 | // if the user has defined their own void toString() in this aggregate: 936 | else 937 | { 938 | string result = null; 939 | 940 | static if (!userDefinedVoidToString) 941 | { 942 | bool nakedMode; 943 | bool udaIncludeSuper; 944 | bool udaExcludeSuper; 945 | 946 | foreach (uda; __traits(getAttributes, typeof(this))) 947 | { 948 | static if (is(typeof(uda) == ToStringEnum)) 949 | { 950 | switch (uda) 951 | { 952 | case ToString.Naked: nakedMode = true; break; 953 | case ToString.IncludeSuper: udaIncludeSuper = true; break; 954 | case ToString.ExcludeSuper: udaExcludeSuper = true; break; 955 | default: break; 956 | } 957 | } 958 | } 959 | 960 | string NamePlusOpenParen = typeName!(typeof(this)) ~ "("; 961 | 962 | version(AutoStringDebug) 963 | { 964 | result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString); 965 | } 966 | 967 | static if (isObject 968 | && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 969 | { 970 | result ~= `override `; 971 | } 972 | 973 | result ~= `public void toString(scope void delegate(const(char)[]) sink) const {` 974 | ~ `import boilerplate.autostring: ToStringHandler;` 975 | ~ `import boilerplate.util: sinkWrite;` 976 | ~ `import std.traits: getUDAs;`; 977 | 978 | static if (synchronize) 979 | { 980 | result ~= `synchronized (this) { `; 981 | } 982 | 983 | if (!nakedMode) 984 | { 985 | result ~= format!`sink(%(%s%));`([NamePlusOpenParen]); 986 | } 987 | 988 | bool includeSuper = false; 989 | 990 | static if (isObject) 991 | { 992 | if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString) 993 | { 994 | includeSuper = true; 995 | } 996 | } 997 | 998 | if (udaIncludeSuper) 999 | { 1000 | includeSuper = true; 1001 | } 1002 | else if (udaExcludeSuper) 1003 | { 1004 | includeSuper = false; 1005 | } 1006 | 1007 | static if (isObject) 1008 | { 1009 | if (includeSuper) 1010 | { 1011 | static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString) 1012 | { 1013 | return `static assert(false, ` 1014 | ~ `"cannot include super class in GenerateToString: ` 1015 | ~ `parent class has no usable toString!");`; 1016 | } 1017 | else { 1018 | static if (alreadyHaveUsableVoidToString) 1019 | { 1020 | result ~= `super.toString(sink);`; 1021 | } 1022 | else 1023 | { 1024 | result ~= `sink(super.toString());`; 1025 | } 1026 | result ~= `bool comma = true;`; 1027 | } 1028 | } 1029 | else 1030 | { 1031 | result ~= `bool comma = false;`; 1032 | } 1033 | } 1034 | else 1035 | { 1036 | result ~= `bool comma = false;`; 1037 | } 1038 | 1039 | result ~= `{`; 1040 | 1041 | mixin GenNormalMemberTuple!(true); 1042 | 1043 | foreach (member; NormalMemberTuple) 1044 | { 1045 | mixin("alias symbol = typeof(this)." ~ member ~ ";"); 1046 | 1047 | enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1; 1048 | enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1; 1049 | enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1; 1050 | enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1; 1051 | enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1; 1052 | enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1; 1053 | enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1; 1054 | 1055 | // see std.traits.isFunction!() 1056 | static if ( 1057 | is(symbol == function) 1058 | || is(typeof(symbol) == function) 1059 | || (is(typeof(&symbol) U : U*) && is(U == function))) 1060 | { 1061 | enum isFunction = true; 1062 | } 1063 | else 1064 | { 1065 | enum isFunction = false; 1066 | } 1067 | 1068 | enum includeOverride = udaInclude || udaOptional; 1069 | 1070 | enum includeMember = (!isFunction || includeOverride) && !udaExclude; 1071 | 1072 | static if (includeMember) 1073 | { 1074 | string memberName = member; 1075 | 1076 | if (memberName.endsWith("_")) 1077 | { 1078 | memberName = memberName[0 .. $ - 1]; 1079 | } 1080 | 1081 | bool labeled = true; 1082 | 1083 | static if (udaUnlabeled) 1084 | { 1085 | labeled = false; 1086 | } 1087 | 1088 | if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty)) 1089 | { 1090 | labeled = false; 1091 | } 1092 | 1093 | static if (udaLabeled) 1094 | { 1095 | labeled = true; 1096 | } 1097 | 1098 | string membervalue = `this.` ~ member; 1099 | 1100 | bool escapeStrings = true; 1101 | 1102 | static if (udaToStringHandler) 1103 | { 1104 | alias Handlers = getUDAs!(symbol, ToStringHandler); 1105 | 1106 | static assert(Handlers.length == 1); 1107 | 1108 | static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init))) 1109 | { 1110 | membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(` 1111 | ~ membervalue 1112 | ~ `)`; 1113 | 1114 | escapeStrings = false; 1115 | } 1116 | else 1117 | { 1118 | return `static assert(false, "cannot determine how to call ToStringHandler");`; 1119 | } 1120 | } 1121 | 1122 | string readMemberValue = membervalue; 1123 | string conditionalWritestmt; // formatted with sink.sinkWrite(... readMemberValue ... ) 1124 | 1125 | static if (udaOptional) 1126 | { 1127 | import std.array : empty; 1128 | 1129 | enum optionalIndex = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)); 1130 | alias optionalUda = Alias!(__traits(getAttributes, symbol)[optionalIndex]); 1131 | 1132 | static if (is(optionalUda == struct)) 1133 | { 1134 | alias pred = Alias!(__traits(getAttributes, symbol)[optionalIndex]).condition; 1135 | static if (__traits(compiles, pred(typeof(this).init))) 1136 | { 1137 | conditionalWritestmt = format!q{ 1138 | if (__traits(getAttributes, %s)[%s].condition(this)) { %%s } 1139 | } (membervalue, optionalIndex); 1140 | } 1141 | else 1142 | { 1143 | conditionalWritestmt = format!q{ 1144 | if (__traits(getAttributes, %s)[%s].condition(%s)) { %%s } 1145 | } (membervalue, optionalIndex, membervalue); 1146 | } 1147 | } 1148 | else static if (__traits(compiles, typeof(symbol).init.isNull)) 1149 | { 1150 | conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1151 | (membervalue); 1152 | 1153 | static if (is(typeof(symbol) : Nullable!T, T)) 1154 | { 1155 | readMemberValue = membervalue ~ ".get"; 1156 | } 1157 | } 1158 | else static if (__traits(compiles, typeof(symbol).init.empty)) 1159 | { 1160 | conditionalWritestmt = format!q{import std.array : empty; if (!%s.empty) { %%s }} 1161 | (membervalue); 1162 | } 1163 | else static if (__traits(compiles, typeof(symbol).init !is null)) 1164 | { 1165 | conditionalWritestmt = format!q{if (%s !is null) { %%s }} 1166 | (membervalue); 1167 | } 1168 | else static if (__traits(compiles, typeof(symbol).init != 0)) 1169 | { 1170 | conditionalWritestmt = format!q{if (%s != 0) { %%s }} 1171 | (membervalue); 1172 | } 1173 | else static if (__traits(compiles, { if (typeof(symbol).init) { } })) 1174 | { 1175 | conditionalWritestmt = format!q{if (%s) { %%s }} 1176 | (membervalue); 1177 | } 1178 | else 1179 | { 1180 | return format!(`static assert(false, ` 1181 | ~ `"don't know how to figure out whether %s is present.");`) 1182 | (member); 1183 | } 1184 | } 1185 | else 1186 | { 1187 | // Nullables (without handler, that aren't force-included) fall back to optional 1188 | static if (!udaToStringHandler && !udaInclude && 1189 | __traits(compiles, typeof(symbol).init.isNull)) 1190 | { 1191 | conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1192 | (membervalue); 1193 | 1194 | static if (is(typeof(symbol) : Nullable!T, T)) 1195 | { 1196 | readMemberValue = membervalue ~ ".get"; 1197 | } 1198 | } 1199 | else 1200 | { 1201 | conditionalWritestmt = q{ %s }; 1202 | } 1203 | } 1204 | 1205 | string writestmt; 1206 | 1207 | if (labeled) 1208 | { 1209 | writestmt = format!`sink.sinkWrite(comma, %s, "%s=%%s", %s);` 1210 | (escapeStrings, memberName, readMemberValue); 1211 | } 1212 | else 1213 | { 1214 | writestmt = format!`sink.sinkWrite(comma, %s, "%%s", %s);` 1215 | (escapeStrings, readMemberValue); 1216 | } 1217 | 1218 | result ~= format(conditionalWritestmt, writestmt); 1219 | } 1220 | } 1221 | 1222 | result ~= `} `; 1223 | 1224 | if (!nakedMode) 1225 | { 1226 | result ~= `sink(")");`; 1227 | } 1228 | 1229 | static if (synchronize) 1230 | { 1231 | result ~= `} `; 1232 | } 1233 | 1234 | result ~= `} `; 1235 | } 1236 | 1237 | // generate fallback string toString() 1238 | // that calls, specifically, *our own* toString impl. 1239 | // (this is important to break cycles when a subclass implements a toString that calls super.toString) 1240 | static if (isObject) 1241 | { 1242 | result ~= `override `; 1243 | } 1244 | 1245 | result ~= `public string toString() const {` 1246 | ~ `string result;` 1247 | ~ `typeof(this).toString((const(char)[] part) { result ~= part; });` 1248 | ~ `return result;` 1249 | ~ `}`; 1250 | } 1251 | return result; 1252 | } 1253 | } 1254 | 1255 | template checkAttributeConsistency(Attributes...) 1256 | { 1257 | enum checkAttributeConsistency = checkAttributeHelper(); 1258 | 1259 | private string checkAttributeHelper() 1260 | { 1261 | if (!__ctfe) 1262 | { 1263 | return null; 1264 | } 1265 | 1266 | import std.string : format; 1267 | 1268 | bool include, exclude, optional, labeled, unlabeled; 1269 | 1270 | foreach (uda; Attributes) 1271 | { 1272 | static if (is(typeof(uda) == ToStringEnum)) 1273 | { 1274 | switch (uda) 1275 | { 1276 | case ToString.Include: include = true; break; 1277 | case ToString.Exclude: exclude = true; break; 1278 | case ToString.Labeled: labeled = true; break; 1279 | case ToString.Unlabeled: unlabeled = true; break; 1280 | default: break; 1281 | } 1282 | } 1283 | else static if (is(uda == struct) && __traits(isSame, uda, ToString.Optional)) 1284 | { 1285 | optional = true; 1286 | } 1287 | } 1288 | 1289 | if (include && exclude) 1290 | { 1291 | return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`; 1292 | } 1293 | 1294 | if (include && optional) 1295 | { 1296 | return `static assert(false, "Redundant tags on '%s': Optional implies Include");`; 1297 | } 1298 | 1299 | if (exclude && optional) 1300 | { 1301 | return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`; 1302 | } 1303 | 1304 | if (labeled && unlabeled) 1305 | { 1306 | return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`; 1307 | } 1308 | 1309 | return null; 1310 | } 1311 | } 1312 | 1313 | struct ToStringHandler(alias Handler_) 1314 | { 1315 | alias Handler = Handler_; 1316 | } 1317 | 1318 | enum ToStringEnum 1319 | { 1320 | // these go on the class 1321 | Naked, 1322 | IncludeSuper, 1323 | ExcludeSuper, 1324 | 1325 | // these go on the field/method 1326 | Unlabeled, 1327 | Labeled, 1328 | Exclude, 1329 | Include, 1330 | } 1331 | 1332 | struct ToString 1333 | { 1334 | static foreach (name; __traits(allMembers, ToStringEnum)) 1335 | { 1336 | mixin(format!q{enum %s = ToStringEnum.%s;}(name, name)); 1337 | } 1338 | 1339 | static struct Optional(alias condition_) 1340 | { 1341 | alias condition = condition_; 1342 | } 1343 | } 1344 | 1345 | public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty) 1346 | { 1347 | import std.datetime : SysTime; 1348 | import std.range.primitives : ElementType, isInputRange; 1349 | // Types whose toString starts with the contained type 1350 | import std.typecons : BitFlags, Nullable; 1351 | 1352 | field = field.toLower; 1353 | 1354 | static if (is(Type: const Nullable!BaseType, BaseType)) 1355 | { 1356 | if (field == BaseType.stringof.toLower) 1357 | { 1358 | return true; 1359 | } 1360 | } 1361 | else static if (isInputRange!Type) 1362 | { 1363 | alias BaseType = ElementType!Type; 1364 | 1365 | if (field == BaseType.stringof.toLower.pluralize && attribNonEmpty) 1366 | { 1367 | return true; 1368 | } 1369 | } 1370 | else static if (is(Type: const BitFlags!BaseType, BaseType)) 1371 | { 1372 | if (field == BaseType.stringof.toLower.pluralize) 1373 | { 1374 | return true; 1375 | } 1376 | } 1377 | 1378 | return field == Type.stringof.toLower 1379 | || (field == "time" && is(Type == SysTime)) 1380 | || (field == "id" && is(typeof(Type.toString))); 1381 | } 1382 | 1383 | private string toLower(string text) 1384 | { 1385 | import std.string : stdToLower = toLower; 1386 | 1387 | string result = null; 1388 | 1389 | foreach (ub; cast(immutable(ubyte)[]) text) 1390 | { 1391 | if (ub >= 0x80) // utf-8, non-ascii 1392 | { 1393 | return text.stdToLower; 1394 | } 1395 | if (ub >= 'A' && ub <= 'Z') 1396 | { 1397 | result ~= cast(char) (ub + ('a' - 'A')); 1398 | } 1399 | else 1400 | { 1401 | result ~= cast(char) ub; 1402 | } 1403 | } 1404 | return result; 1405 | } 1406 | 1407 | // http://code.activestate.com/recipes/82102/ 1408 | private string pluralize(string label) 1409 | { 1410 | import std.algorithm.searching : contain = canFind; 1411 | 1412 | string postfix = "s"; 1413 | if (label.length > 2) 1414 | { 1415 | enum vowels = "aeiou"; 1416 | 1417 | if (label.stringEndsWith("ch") || label.stringEndsWith("sh")) 1418 | { 1419 | postfix = "es"; 1420 | } 1421 | else if (auto before = label.stringEndsWith("y")) 1422 | { 1423 | if (!vowels.contain(label[$ - 2])) 1424 | { 1425 | postfix = "ies"; 1426 | label = before; 1427 | } 1428 | } 1429 | else if (auto before = label.stringEndsWith("is")) 1430 | { 1431 | postfix = "es"; 1432 | label = before; 1433 | } 1434 | else if ("sxz".contain(label[$-1])) 1435 | { 1436 | postfix = "es"; // glasses 1437 | } 1438 | } 1439 | return label ~ postfix; 1440 | } 1441 | 1442 | @("has functioning pluralize()") 1443 | unittest 1444 | { 1445 | "dog".pluralize.shouldEqual("dogs"); 1446 | "ash".pluralize.shouldEqual("ashes"); 1447 | "day".pluralize.shouldEqual("days"); 1448 | "entity".pluralize.shouldEqual("entities"); 1449 | "thesis".pluralize.shouldEqual("theses"); 1450 | "glass".pluralize.shouldEqual("glasses"); 1451 | } 1452 | 1453 | private string stringEndsWith(const string text, const string suffix) 1454 | { 1455 | import std.range : dropBack; 1456 | import std.string : endsWith; 1457 | 1458 | if (text.endsWith(suffix)) 1459 | { 1460 | return text.dropBack(suffix.length); 1461 | } 1462 | return null; 1463 | } 1464 | 1465 | @("has functioning stringEndsWith()") 1466 | unittest 1467 | { 1468 | "".stringEndsWith("").shouldNotBeNull; 1469 | "".stringEndsWith("x").shouldBeNull; 1470 | "Hello".stringEndsWith("Hello").shouldNotBeNull; 1471 | "Hello".stringEndsWith("Hello").shouldEqual(""); 1472 | "Hello".stringEndsWith("lo").shouldEqual("Hel"); 1473 | } 1474 | 1475 | template hasOwnFunction(Aggregate, Super, string Name, Type) 1476 | { 1477 | import std.meta : AliasSeq, Filter; 1478 | import std.traits : Unqual; 1479 | enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1480 | 1481 | alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name)); 1482 | alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions); 1483 | enum hasFunction = MatchingFunctions.length == 1; 1484 | 1485 | alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name)); 1486 | alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions); 1487 | enum superHasFunction = SuperMatchingFunctions.length == 1; 1488 | 1489 | static if (hasFunction) 1490 | { 1491 | static if (superHasFunction) 1492 | { 1493 | enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]); 1494 | } 1495 | else 1496 | { 1497 | enum hasOwnFunction = true; 1498 | } 1499 | } 1500 | else 1501 | { 1502 | enum hasOwnFunction = false; 1503 | } 1504 | } 1505 | 1506 | /** 1507 | * Find qualified name of `T` including any containing types; not including containing functions or modules. 1508 | */ 1509 | public template typeName(T) 1510 | { 1511 | static if (__traits(compiles, __traits(parent, T))) 1512 | { 1513 | alias parent = Alias!(__traits(parent, T)); 1514 | enum isSame = __traits(isSame, T, parent); 1515 | 1516 | static if (!isSame && ( 1517 | is(parent == struct) || is(parent == union) || is(parent == enum) || 1518 | is(parent == class) || is(parent == interface))) 1519 | { 1520 | enum typeName = typeName!parent ~ "." ~ Unqual!T.stringof; 1521 | } 1522 | else 1523 | { 1524 | enum typeName = Unqual!T.stringof; 1525 | } 1526 | } 1527 | else 1528 | { 1529 | enum typeName = Unqual!T.stringof; 1530 | } 1531 | } 1532 | 1533 | public template hasOwnStringToString(Aggregate, Super) 1534 | if (is(Aggregate: Object)) 1535 | { 1536 | enum hasOwnStringToString = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString)); 1537 | } 1538 | 1539 | public template hasOwnStringToString(Aggregate) 1540 | if (is(Aggregate == struct)) 1541 | { 1542 | static if (is(typeof(Aggregate.init.toString()) == string)) 1543 | { 1544 | enum hasOwnStringToString = !isFromAliasThis!( 1545 | Aggregate, "toString", typeof(StringToStringSample.toString)); 1546 | } 1547 | else 1548 | { 1549 | enum hasOwnStringToString = false; 1550 | } 1551 | } 1552 | 1553 | public template hasOwnVoidToString(Aggregate, Super) 1554 | if (is(Aggregate: Object)) 1555 | { 1556 | enum hasOwnVoidToString = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString)); 1557 | } 1558 | 1559 | public template hasOwnVoidToString(Aggregate) 1560 | if (is(Aggregate == struct)) 1561 | { 1562 | static if (is(typeof(Aggregate.init.toString((void delegate(const(char)[])).init)) == void)) 1563 | { 1564 | enum hasOwnVoidToString = !isFromAliasThis!( 1565 | Aggregate, "toString", typeof(VoidToStringSample.toString)); 1566 | } 1567 | else 1568 | { 1569 | enum hasOwnVoidToString = false; 1570 | } 1571 | } 1572 | 1573 | private final abstract class StringToStringSample 1574 | { 1575 | override string toString(); 1576 | } 1577 | 1578 | private final abstract class VoidToStringSample 1579 | { 1580 | void toString(scope void delegate(const(char)[]) sink); 1581 | } 1582 | 1583 | public template isFromAliasThis(T, string member, Type) 1584 | { 1585 | import std.meta : AliasSeq, anySatisfy, Filter; 1586 | 1587 | enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1588 | 1589 | private template isFromThatAliasThis(string field) 1590 | { 1591 | alias aliasMembers = AliasSeq!(__traits(getOverloads, __traits(getMember, T.init, field), member)); 1592 | alias ownMembers = AliasSeq!(__traits(getOverloads, T, member)); 1593 | 1594 | enum bool isFromThatAliasThis = __traits(isSame, 1595 | Filter!(FunctionMatchesType, aliasMembers), 1596 | Filter!(FunctionMatchesType, ownMembers)); 1597 | } 1598 | 1599 | enum bool isFromAliasThis = anySatisfy!(isFromThatAliasThis, __traits(getAliasThis, T)); 1600 | } 1601 | 1602 | @("correctly recognizes the existence of string toString() in a class") 1603 | unittest 1604 | { 1605 | class Class1 1606 | { 1607 | override string toString() { return null; } 1608 | static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1609 | static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1610 | } 1611 | 1612 | class Class2 1613 | { 1614 | override string toString() const { return null; } 1615 | static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1616 | static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1617 | } 1618 | 1619 | class Class3 1620 | { 1621 | void toString(scope void delegate(const(char)[]) sink) const { } 1622 | override string toString() const { return null; } 1623 | static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1624 | static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1625 | } 1626 | 1627 | class Class4 1628 | { 1629 | void toString(scope void delegate(const(char)[]) sink) const { } 1630 | static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1631 | static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1632 | } 1633 | 1634 | class Class5 1635 | { 1636 | mixin(GenerateToString); 1637 | } 1638 | 1639 | class ChildClass1 : Class1 1640 | { 1641 | static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1642 | } 1643 | 1644 | class ChildClass2 : Class2 1645 | { 1646 | static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1647 | } 1648 | 1649 | class ChildClass3 : Class3 1650 | { 1651 | static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1652 | } 1653 | 1654 | class ChildClass5 : Class5 1655 | { 1656 | static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1657 | } 1658 | } 1659 | -------------------------------------------------------------------------------- /src/boilerplate/builder.d: -------------------------------------------------------------------------------- 1 | module boilerplate.builder; 2 | 3 | import std.typecons : Nullable, Tuple; 4 | 5 | private alias Info = Tuple!(string, "typeField", string, "builderField"); 6 | 7 | public alias Builder(T) = typeof(T.Builder()); 8 | 9 | public mixin template BuilderImpl(T, Info = Info, alias BuilderProxy = BuilderProxy, alias _toInfo = _toInfo) 10 | { 11 | import boilerplate.util : Optional, optionallyRemoveTrailingUnderline, removeTrailingUnderline; 12 | static import std.algorithm; 13 | static import std.format; 14 | static import std.meta; 15 | static import std.range; 16 | static import std.typecons; 17 | 18 | static assert(__traits(hasMember, T, "ConstructorInfo")); 19 | 20 | static if (T.ConstructorInfo.fields.length > 0) 21 | { 22 | private enum string[] builderFields = [ 23 | std.meta.staticMap!(optionallyRemoveTrailingUnderline, 24 | std.meta.aliasSeqOf!(T.ConstructorInfo.fields))]; 25 | } 26 | else 27 | { 28 | private enum string[] builderFields = []; 29 | } 30 | private enum fieldInfoList = std.range.zip(T.ConstructorInfo.fields, builderFields); 31 | 32 | private template BuilderFieldInfo(string member) 33 | { 34 | mixin(std.format.format!q{alias FieldType = T.ConstructorInfo.FieldInfo.%s.Type;}(member)); 35 | 36 | import std.typecons : Nullable; 37 | 38 | static if (is(FieldType : Nullable!Arg, Arg)) 39 | { 40 | alias BaseType = Arg; 41 | } 42 | else 43 | { 44 | alias BaseType = FieldType; 45 | } 46 | 47 | // type has a builder 48 | static if (__traits(compiles, BaseType.Builder())) 49 | { 50 | alias Type = BuilderProxy!FieldType; 51 | enum isBuildable = true; 52 | } 53 | else static if (is(FieldType == E[], E)) 54 | { 55 | static if (__traits(compiles, E.Builder())) 56 | { 57 | alias Type = BuilderProxy!FieldType; 58 | enum isBuildable = true; 59 | } 60 | else 61 | { 62 | alias Type = Optional!FieldType; 63 | enum isBuildable = false; 64 | } 65 | } 66 | else 67 | { 68 | alias Type = Optional!FieldType; 69 | enum isBuildable = false; 70 | } 71 | } 72 | 73 | static foreach (typeField, builderField; fieldInfoList) 74 | { 75 | mixin(`public BuilderFieldInfo!typeField.Type ` ~ builderField ~ `;`); 76 | } 77 | 78 | public bool isValid() const 79 | { 80 | return this.getError().isNull; 81 | } 82 | 83 | public std.typecons.Nullable!string getError() const 84 | { 85 | alias Nullable = std.typecons.Nullable; 86 | 87 | static foreach (typeField, builderField; fieldInfoList) 88 | { 89 | static if (BuilderFieldInfo!(typeField).isBuildable) 90 | { 91 | // if the proxy has never been used as a builder, 92 | // ie. either a value was assigned or it was untouched 93 | // then a default value may be used instead. 94 | if (__traits(getMember, this, builderField)._isUnset) 95 | { 96 | static if (!__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 97 | { 98 | return Nullable!string( 99 | "required field '" ~ builderField ~ "' not set in builder of " ~ T.stringof); 100 | } 101 | } 102 | else if (__traits(getMember, this, builderField)._isBuilder) 103 | { 104 | auto subError = __traits(getMember, this, builderField)._builder.getError; 105 | 106 | if (!subError.isNull) 107 | { 108 | return Nullable!string(subError.get ~ " of " ~ T.stringof); 109 | } 110 | } 111 | // else it carries a full value. 112 | } 113 | else 114 | { 115 | static if (!__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 116 | { 117 | if (__traits(getMember, this, builderField).isNull) 118 | { 119 | return Nullable!string( 120 | "required field '" ~ builderField ~ "' not set in builder of " ~ T.stringof); 121 | } 122 | } 123 | } 124 | } 125 | return Nullable!string(); 126 | } 127 | 128 | public @property T builderValue(size_t line = __LINE__, string file = __FILE__) 129 | in 130 | { 131 | import core.exception : AssertError; 132 | 133 | if (!this.isValid) 134 | { 135 | throw new AssertError(this.getError.get, file, line); 136 | } 137 | } 138 | do 139 | { 140 | auto getArg(string typeField, string builderField)() 141 | { 142 | static if (BuilderFieldInfo!(typeField).isBuildable) 143 | { 144 | import std.meta : Alias; 145 | 146 | alias Type = Alias!(__traits(getMember, T.ConstructorInfo.FieldInfo, typeField)).Type; 147 | 148 | static if (is(Type == E[], E)) 149 | { 150 | if (__traits(getMember, this, builderField)._isArray) 151 | { 152 | return __traits(getMember, this, builderField)._arrayValue; 153 | } 154 | else if (__traits(getMember, this, builderField)._isValue) 155 | { 156 | return __traits(getMember, this, builderField)._value; 157 | } 158 | } 159 | else 160 | { 161 | if (__traits(getMember, this, builderField)._isBuilder) 162 | { 163 | return __traits(getMember, this, builderField)._builderValue; 164 | } 165 | else if (__traits(getMember, this, builderField)._isValue) 166 | { 167 | return __traits(getMember, this, builderField)._value; 168 | } 169 | } 170 | assert(__traits(getMember, this, builderField)._isUnset); 171 | 172 | static if (__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 173 | { 174 | return __traits(getMember, T.ConstructorInfo.FieldInfo, typeField).fieldDefault; 175 | } 176 | else 177 | { 178 | assert(false, "isValid/build do not match 1"); 179 | } 180 | } 181 | else 182 | { 183 | if (!__traits(getMember, this, builderField).isNull) 184 | { 185 | return __traits(getMember, this, builderField)._get; 186 | } 187 | else 188 | { 189 | static if (__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 190 | { 191 | return __traits(getMember, T.ConstructorInfo.FieldInfo, typeField).fieldDefault; 192 | } 193 | else 194 | { 195 | assert(false, "isValid/build do not match 2"); 196 | } 197 | } 198 | } 199 | } 200 | 201 | enum getArgArray = std.range.array( 202 | std.algorithm.map!(i => std.format.format!`getArg!(fieldInfoList[%s][0], fieldInfoList[%s][1])`(i, i))( 203 | std.range.iota(fieldInfoList.length))); 204 | 205 | static if (is(T == class)) 206 | { 207 | return mixin(std.format.format!q{new T(%-(%s, %))}(getArgArray)); 208 | } 209 | else 210 | { 211 | return mixin(std.format.format!q{T(%-(%s, %))}(getArgArray)); 212 | } 213 | } 214 | 215 | static foreach (aliasMember; __traits(getAliasThis, T)) 216 | { 217 | mixin(`alias ` ~ optionallyRemoveTrailingUnderline!aliasMember ~ ` this;`); 218 | } 219 | 220 | static if (!std.algorithm.canFind( 221 | std.algorithm.map!removeTrailingUnderline(T.ConstructorInfo.fields), 222 | "value")) 223 | { 224 | public alias value = builderValue; 225 | } 226 | } 227 | 228 | // value that is either a T, or a Builder for T. 229 | // Used for nested builder initialization. 230 | public struct BuilderProxy(T) 231 | { 232 | private enum Mode 233 | { 234 | unset, 235 | builder, 236 | value, 237 | array, // array of builders 238 | } 239 | 240 | static if (is(T : Nullable!Arg, Arg)) 241 | { 242 | enum isNullable = true; 243 | alias InnerType = Arg; 244 | } 245 | else 246 | { 247 | enum isNullable = false; 248 | alias InnerType = T; 249 | } 250 | 251 | private union Data 252 | { 253 | T value; 254 | 255 | this(inout(T) value) inout pure 256 | { 257 | this.value = value; 258 | } 259 | 260 | static if (is(T == E[], E)) 261 | { 262 | E.BuilderType!()[] array; 263 | 264 | this(inout(E.BuilderType!())[] array) inout pure 265 | { 266 | this.array = array; 267 | } 268 | } 269 | else 270 | { 271 | InnerType.BuilderType!() builder; 272 | 273 | this(inout(InnerType.BuilderType!()) builder) inout pure 274 | { 275 | this.builder = builder; 276 | } 277 | } 278 | } 279 | 280 | struct DataWrapper 281 | { 282 | Data data; 283 | } 284 | 285 | private Mode mode = Mode.unset; 286 | 287 | private DataWrapper wrapper = DataWrapper.init; 288 | 289 | public this(T value) 290 | { 291 | opAssign(value); 292 | } 293 | 294 | public void opAssign(T value) 295 | in(this.mode != Mode.builder, 296 | "Builder: cannot set field by value since a subfield has already been set.") 297 | { 298 | import boilerplate.util : move, moveEmplace; 299 | 300 | static if (isNullable) 301 | { 302 | DataWrapper newWrapper = DataWrapper(Data(value)); 303 | } 304 | else 305 | { 306 | DataWrapper newWrapper = DataWrapper(Data(value)); 307 | } 308 | 309 | if (this.mode == Mode.value) 310 | { 311 | move(newWrapper, this.wrapper); 312 | } 313 | else 314 | { 315 | moveEmplace(newWrapper, this.wrapper); 316 | } 317 | this.mode = Mode.value; 318 | } 319 | 320 | static if (isNullable) 321 | { 322 | public void opAssign(InnerType value) 323 | { 324 | return opAssign(T(value)); 325 | } 326 | } 327 | 328 | public bool _isUnset() const 329 | { 330 | return this.mode == Mode.unset; 331 | } 332 | 333 | public bool _isValue() const 334 | { 335 | return this.mode == Mode.value; 336 | } 337 | 338 | public bool _isBuilder() const 339 | { 340 | return this.mode == Mode.builder; 341 | } 342 | 343 | public bool _isArray() const 344 | { 345 | return this.mode == Mode.array; 346 | } 347 | 348 | public inout(T) _value() inout 349 | in (this.mode == Mode.value) 350 | { 351 | return this.wrapper.data.value; 352 | } 353 | 354 | public ref auto _builder() inout 355 | in (this.mode == Mode.builder) 356 | { 357 | static if (is(T == E[], E)) 358 | { 359 | int i = 0; 360 | 361 | assert(i != 0); // assert(false) but return stays "reachable" 362 | return E.Builder(); 363 | } 364 | else 365 | { 366 | return this.wrapper.data.builder; 367 | } 368 | } 369 | 370 | public auto _builderValue() 371 | in (this.mode == Mode.builder) 372 | { 373 | static if (is(T == E[], E)) 374 | { 375 | int i = 0; 376 | 377 | assert(i != 0); // assert(false) but return stays "reachable" 378 | return E.Builder(); 379 | } 380 | else static if (isNullable) 381 | { 382 | return T(this.wrapper.data.builder.builderValue); 383 | } 384 | else 385 | { 386 | return this.wrapper.data.builder.builderValue; 387 | } 388 | } 389 | 390 | public T _arrayValue() 391 | in (this.mode == Mode.array) 392 | { 393 | import std.algorithm : map; 394 | import std.array : array; 395 | 396 | static if (is(T == E[], E)) 397 | { 398 | // enforce that E is the return value 399 | static E builderValue(Element)(Element element) { return element.builderValue; } 400 | 401 | return this.wrapper.data.array.map!builderValue.array; 402 | } 403 | else 404 | { 405 | assert(false); 406 | } 407 | } 408 | 409 | static if (is(T == E[], E)) 410 | { 411 | public ref E.BuilderType!() opIndex(size_t index) return 412 | in (this.mode == Mode.unset || this.mode == Mode.array, 413 | "cannot build array for already initialized field") 414 | { 415 | import boilerplate.util : moveEmplace; 416 | 417 | if (this.mode == Mode.unset) 418 | { 419 | auto newWrapper = DataWrapper(Data(new E.BuilderType!()[](index + 1))); 420 | 421 | this.mode = Mode.array; 422 | moveEmplace(newWrapper, this.wrapper); 423 | } 424 | else while (this.wrapper.data.array.length <= index) 425 | { 426 | this.wrapper.data.array ~= E.Builder(); 427 | } 428 | return this.wrapper.data.array[index]; 429 | } 430 | 431 | public void opOpAssign(string op, R)(R rhs) 432 | if (op == "~") 433 | in (this.mode == Mode.unset || this.mode == Mode.value, 434 | "Builder cannot append to array already initialized by index") 435 | { 436 | if (this.mode == Mode.unset) 437 | { 438 | opAssign(null); 439 | } 440 | opAssign(this.wrapper.data.value ~ rhs); 441 | } 442 | } 443 | else 444 | { 445 | public @property ref InnerType.BuilderType!() _implicitBuilder() 446 | { 447 | import boilerplate.util : move, moveEmplace; 448 | 449 | if (this.mode == Mode.unset) 450 | { 451 | auto newWrapper = DataWrapper(Data(InnerType.BuilderType!().init)); 452 | 453 | this.mode = Mode.builder; 454 | moveEmplace(newWrapper, this.wrapper); 455 | } 456 | else if (this.mode == Mode.value) 457 | { 458 | static if (isNullable) 459 | { 460 | assert( 461 | !this.wrapper.data.value.isNull, 462 | "Builder: cannot set sub-field directly since field was explicitly " ~ 463 | "initialized to Nullable.null"); 464 | auto value = this.wrapper.data.value.get; 465 | } 466 | else 467 | { 468 | auto value = this.wrapper.data.value; 469 | } 470 | static if (__traits(compiles, value.BuilderFrom())) 471 | { 472 | auto newWrapper = DataWrapper(Data(value.BuilderFrom())); 473 | 474 | this.mode = Mode.builder; 475 | move(newWrapper, this.wrapper); 476 | } 477 | else 478 | { 479 | assert( 480 | false, 481 | "Builder: cannot set sub-field directly since field is already being initialized by value " ~ 482 | "(and BuilderFrom is unavailable in " ~ typeof(this.wrapper.data.value).stringof ~ ")"); 483 | } 484 | } 485 | 486 | return this.wrapper.data.builder; 487 | } 488 | 489 | alias _implicitBuilder this; 490 | } 491 | } 492 | 493 | public Info _toInfo(Tuple!(string, string) pair) 494 | { 495 | return Info(pair[0], pair[1]); 496 | } 497 | -------------------------------------------------------------------------------- /src/boilerplate/conditions.d: -------------------------------------------------------------------------------- 1 | module boilerplate.conditions; 2 | 3 | import std.typecons; 4 | 5 | version(unittest) 6 | { 7 | import core.exception : AssertError; 8 | import unit_threaded.should; 9 | } 10 | 11 | /++ 12 | `GenerateInvariants` is a mixin string that automatically generates an `invariant{}` block 13 | for each field with a condition. 14 | +/ 15 | public enum string GenerateInvariants = ` 16 | import boilerplate.conditions : GenerateInvariantsTemplate; 17 | mixin GenerateInvariantsTemplate; 18 | mixin(typeof(this).generateInvariantsImpl()); 19 | `; 20 | 21 | /++ 22 | When a field is marked with `@NonEmpty`, `!field.empty` is asserted. 23 | +/ 24 | public struct NonEmpty 25 | { 26 | } 27 | 28 | /// 29 | @("throws when a NonEmpty field is initialized empty") 30 | unittest 31 | { 32 | class Class 33 | { 34 | @NonEmpty 35 | int[] array_; 36 | 37 | this(int[] array) 38 | { 39 | this.array_ = array; 40 | } 41 | 42 | mixin(GenerateInvariants); 43 | } 44 | 45 | (new Class(null)).shouldThrow!AssertError; 46 | } 47 | 48 | /// 49 | @("throws when a NonEmpty field is assigned empty") 50 | unittest 51 | { 52 | class Class 53 | { 54 | @NonEmpty 55 | private int[] array_; 56 | 57 | this(int[] array) 58 | { 59 | this.array_ = array; 60 | } 61 | 62 | public void array(int[] arrayValue) 63 | { 64 | this.array_ = arrayValue; 65 | } 66 | 67 | mixin(GenerateInvariants); 68 | } 69 | 70 | (new Class([2])).array(null).shouldThrow!AssertError; 71 | } 72 | 73 | /++ 74 | When a field is marked with `@NonNull`, `field !is null` is asserted. 75 | +/ 76 | public struct NonNull 77 | { 78 | } 79 | 80 | /// 81 | @("throws when a NonNull field is initialized null") 82 | unittest 83 | { 84 | class Class 85 | { 86 | @NonNull 87 | Object obj_; 88 | 89 | this(Object obj) 90 | { 91 | this.obj_ = obj; 92 | } 93 | 94 | mixin(GenerateInvariants); 95 | } 96 | 97 | (new Class(null)).shouldThrow!AssertError; 98 | } 99 | 100 | /++ 101 | When a field is marked with `@AllNonNull`, `field.all!"a !is null"` is asserted. 102 | +/ 103 | public struct AllNonNull 104 | { 105 | } 106 | 107 | /// 108 | @("throws when an AllNonNull field is initialized with an array containing null") 109 | unittest 110 | { 111 | class Class 112 | { 113 | @AllNonNull 114 | Object[] objs; 115 | 116 | this(Object[] objs) 117 | { 118 | this.objs = objs; 119 | } 120 | 121 | mixin(GenerateInvariants); 122 | } 123 | 124 | (new Class(null)).objs.shouldEqual(Object[].init); 125 | (new Class([null])).shouldThrow!AssertError; 126 | (new Class([new Object, null])).shouldThrow!AssertError; 127 | } 128 | 129 | /// `@AllNonNull` may be used with associative arrays. 130 | @("supports AllNonNull on associative arrays") 131 | unittest 132 | { 133 | class Class 134 | { 135 | @AllNonNull 136 | Object[int] objs; 137 | 138 | this(Object[int] objs) 139 | { 140 | this.objs = objs; 141 | } 142 | 143 | mixin(GenerateInvariants); 144 | } 145 | 146 | (new Class(null)).objs.shouldEqual(null); 147 | (new Class([0: null])).shouldThrow!AssertError; 148 | (new Class([0: new Object, 1: null])).shouldThrow!AssertError; 149 | } 150 | 151 | /// When used with associative arrays, `@AllNonNull` may check keys, values or both. 152 | @("supports AllNonNull on associative array keys") 153 | unittest 154 | { 155 | class Class 156 | { 157 | @AllNonNull 158 | int[Object] objs; 159 | 160 | this(int[Object] objs) 161 | { 162 | this.objs = objs; 163 | } 164 | 165 | mixin(GenerateInvariants); 166 | } 167 | 168 | (new Class(null)).objs.shouldEqual(null); 169 | (new Class([null: 0])).shouldThrow!AssertError; 170 | (new Class([new Object: 0, null: 1])).shouldThrow!AssertError; 171 | } 172 | 173 | /++ 174 | When a field is marked with `@NonInit`, `field !is T.init` is asserted. 175 | +/ 176 | public struct NonInit 177 | { 178 | } 179 | 180 | /// 181 | @("throws when a NonInit field is initialized with T.init") 182 | unittest 183 | { 184 | import core.time : Duration; 185 | 186 | class Class 187 | { 188 | @NonInit 189 | float f_; 190 | 191 | this(float f) { this.f_ = f; } 192 | 193 | mixin(GenerateInvariants); 194 | } 195 | 196 | (new Class(float.init)).shouldThrow!AssertError; 197 | } 198 | 199 | /++ 200 | When **any** condition check is applied to a nullable field, the test applies to the value, 201 | if any, contained in the field. The "null" state of the field is ignored. 202 | +/ 203 | @("doesn't throw when a Nullable field is null") 204 | unittest 205 | { 206 | class Class 207 | { 208 | @NonInit 209 | Nullable!float f_; 210 | 211 | this(Nullable!float f) 212 | { 213 | this.f_ = f; 214 | } 215 | 216 | mixin(GenerateInvariants); 217 | } 218 | 219 | (new Class(5f.nullable)).f_.isNull.shouldBeFalse; 220 | (new Class(Nullable!float())).f_.isNull.shouldBeTrue; 221 | (new Class(float.init.nullable)).shouldThrow!AssertError; 222 | } 223 | 224 | /++ 225 | Conditions can be applied to static attributes, generating static invariants. 226 | +/ 227 | @("does not allow invariants on static fields") 228 | unittest 229 | { 230 | static assert(!__traits(compiles, () 231 | { 232 | class Class 233 | { 234 | @NonNull 235 | private static Object obj; 236 | 237 | mixin(GenerateInvariants); 238 | } 239 | }), "invariant on static field compiled when it shouldn't"); 240 | } 241 | 242 | @("works with classes inheriting from templates") 243 | unittest 244 | { 245 | interface I(T) 246 | { 247 | } 248 | 249 | interface K(T) 250 | { 251 | } 252 | 253 | class C : I!ubyte, K!ubyte 254 | { 255 | } 256 | 257 | class S 258 | { 259 | C c; 260 | 261 | mixin(GenerateInvariants); 262 | } 263 | } 264 | 265 | mixin template GenerateInvariantsTemplate() 266 | { 267 | private static string generateInvariantsImpl() 268 | { 269 | if (!__ctfe) 270 | { 271 | return null; 272 | } 273 | 274 | import boilerplate.conditions : generateChecksForAttributes, IsConditionAttribute; 275 | import boilerplate.util : GenNormalMemberTuple, isStatic; 276 | import std.format : format; 277 | import std.meta : StdMetaFilter = Filter; 278 | 279 | string result = null; 280 | 281 | result ~= `invariant {` ~ 282 | `import std.format : format;` ~ 283 | `import std.array : empty;`; 284 | 285 | // TODO blocked by https://issues.dlang.org/show_bug.cgi?id=18504 286 | // note: synchronized without lock contention is basically free 287 | // IMPORTANT! Do not enable this until you have a solution for reliably detecting which attributes actually 288 | // require synchronization! overzealous synchronize has the potential to lead to needless deadlocks. 289 | // (consider implementing @GuardedBy) 290 | enum synchronize = false; 291 | 292 | result ~= synchronize ? `synchronized (this) {` : ``; 293 | 294 | mixin GenNormalMemberTuple; 295 | 296 | foreach (member; NormalMemberTuple) 297 | { 298 | mixin(`alias symbol = typeof(this).` ~ member ~ `;`); 299 | 300 | alias ConditionAttributes = StdMetaFilter!(IsConditionAttribute, __traits(getAttributes, symbol)); 301 | 302 | static if (mixin(isStatic(member)) && ConditionAttributes.length > 0) 303 | { 304 | result ~= format!(`static assert(false, ` 305 | ~ `"Cannot add constraint on static field %s: no support for static invariants");` 306 | )(member); 307 | } 308 | 309 | static if (__traits(compiles, typeof(symbol).init)) 310 | { 311 | result ~= generateChecksForAttributes!(typeof(symbol), ConditionAttributes)(`this.` ~ member); 312 | } 313 | } 314 | 315 | result ~= synchronize ? ` }` : ``; 316 | 317 | result ~= ` }`; 318 | 319 | return result; 320 | } 321 | } 322 | 323 | public string generateChecksForAttributes(T, Attributes...)(string memberExpression, string info = "") 324 | if (Attributes.length == 0) 325 | { 326 | return null; 327 | } 328 | 329 | private alias InfoTuple = Tuple!(string, "expr", string, "info", string, "typename"); 330 | 331 | public string generateChecksForAttributes(T, Attributes...)(string memberExpression, string info = "") 332 | if (Attributes.length > 0) 333 | { 334 | import boilerplate.util : formatNamed, udaIndex; 335 | import std.string : format; 336 | import std.traits : ConstOf; 337 | 338 | enum isNullable = is(T: Nullable!Args, Args...); 339 | 340 | static if (isNullable) 341 | { 342 | enum access = `%s.get`; 343 | } 344 | else 345 | { 346 | enum access = `%s`; 347 | } 348 | 349 | alias MemberType = typeof(mixin(isNullable ? `T.init.get` : `T.init`)); 350 | 351 | string expression = isNullable ? (memberExpression ~ `.get`) : memberExpression; 352 | 353 | auto values = InfoTuple(expression, info, MemberType.stringof); 354 | 355 | string checks; 356 | 357 | static if (udaIndex!(NonEmpty, Attributes) != -1) 358 | { 359 | checks ~= generateNonEmpty!MemberType(values); 360 | } 361 | 362 | static if (udaIndex!(NonNull, Attributes) != -1) 363 | { 364 | checks ~= generateNonNull!MemberType(values); 365 | } 366 | 367 | static if (udaIndex!(NonInit, Attributes) != -1) 368 | { 369 | checks ~= generateNonInit!MemberType(values); 370 | } 371 | 372 | static if (udaIndex!(AllNonNull, Attributes) != -1) 373 | { 374 | checks ~= generateAllNonNull!MemberType(values); 375 | } 376 | 377 | static if (isNullable) 378 | { 379 | return `if (!` ~ memberExpression ~ `.isNull) {` ~ checks ~ `}`; 380 | } 381 | else 382 | { 383 | return checks; 384 | } 385 | } 386 | 387 | private string generateNonEmpty(T)(InfoTuple values) 388 | { 389 | import boilerplate.util : formatNamed; 390 | import std.array : empty; 391 | 392 | string checks; 393 | 394 | static if (!__traits(compiles, T.init.empty())) 395 | { 396 | checks ~= formatNamed!`static assert(false, "Cannot call std.array.empty() on '%(expr)'");`.values(values); 397 | } 398 | 399 | enum canFormat = __traits(compiles, format(`%s`, ConstOf!MemberType.init)); 400 | 401 | static if (canFormat) 402 | { 403 | checks ~= formatNamed!(`assert(!%(expr).empty, ` 404 | ~ `format("@NonEmpty: assert(!%(expr).empty) failed%(info): %(expr) = %s", %(expr)));`) 405 | .values(values); 406 | } 407 | else 408 | { 409 | checks ~= formatNamed!`assert(!%(expr).empty(), "@NonEmpty: assert(!%(expr).empty) failed%(info)");` 410 | .values(values); 411 | } 412 | 413 | return checks; 414 | } 415 | 416 | private string generateNonNull(T)(InfoTuple values) 417 | { 418 | import boilerplate.util : formatNamed; 419 | 420 | string checks; 421 | 422 | static if (__traits(compiles, T.init.isNull)) 423 | { 424 | checks ~= formatNamed!`assert(!%(expr).isNull, "@NonNull: assert(!%(expr).isNull) failed%(info)");` 425 | .values(values); 426 | } 427 | else static if (__traits(compiles, T.init !is null)) 428 | { 429 | // Nothing good can come of printing something that is null. 430 | checks ~= formatNamed!`assert(%(expr) !is null, "@NonNull: assert(%(expr) !is null) failed%(info)");` 431 | .values(values); 432 | } 433 | else 434 | { 435 | checks ~= formatNamed!`static assert(false, "Cannot compare '%(expr)' to null");`.values(values); 436 | } 437 | 438 | return checks; 439 | } 440 | 441 | private string generateNonInit(T)(InfoTuple values) 442 | { 443 | import boilerplate.util : formatNamed; 444 | 445 | string checks; 446 | 447 | enum canFormat = __traits(compiles, format(`%s`, ConstOf!MemberType.init)); 448 | 449 | static if (!__traits(compiles, T.init !is T.init)) 450 | { 451 | checks ~= formatNamed!`static assert(false, "Cannot compare '%(expr)' to %(typename).init");` 452 | .values(values); 453 | } 454 | 455 | static if (canFormat) 456 | { 457 | checks ~= formatNamed!(`assert(%(expr) !is typeof(%(expr)).init, ` 458 | ~ `format("@NonInit: assert(%(expr) !is %(typename).init) failed%(info): %(expr) = %s", %(expr)));`) 459 | .values(values); 460 | } 461 | else 462 | { 463 | checks ~= formatNamed!(`assert(%(expr) !is typeof(%(expr)).init, ` 464 | ~ `"@NonInit: assert(%(expr) !is %(typename).init) failed%(info)");`) 465 | .values(values); 466 | } 467 | 468 | return checks; 469 | } 470 | 471 | private string generateAllNonNull(T)(InfoTuple values) 472 | { 473 | import boilerplate.util : formatNamed; 474 | import std.algorithm : all; 475 | import std.traits : isAssociativeArray; 476 | 477 | string checks; 478 | 479 | enum canFormat = __traits(compiles, format(`%s`, ConstOf!MemberType.init)); 480 | 481 | checks ~= `import std.algorithm: all;`; 482 | 483 | static if (__traits(compiles, T.init.all!"a !is null")) 484 | { 485 | static if (canFormat) 486 | { 487 | checks ~= formatNamed!(`assert(%(expr).all!"a !is null", format(` 488 | ~ `"@AllNonNull: assert(%(expr).all!\"a !is null\") failed%(info): %(expr) = %s", %(expr)));`) 489 | .values(values); 490 | } 491 | else 492 | { 493 | checks ~= formatNamed!(`assert(%(expr).all!"a !is null", ` 494 | ~ `"@AllNonNull: assert(%(expr).all!\"a !is null\") failed%(info)");`) 495 | .values(values); 496 | } 497 | } 498 | else static if (__traits(compiles, T.init.all!"!a.isNull")) 499 | { 500 | static if (canFormat) 501 | { 502 | checks ~= formatNamed!(`assert(%(expr).all!"!a.isNull", format(` 503 | ~ `"@AllNonNull: assert(%(expr).all!\"!a.isNull\") failed%(info): %(expr) = %s", %(expr)));`) 504 | .values(values); 505 | } 506 | else 507 | { 508 | checks ~= formatNamed!(`assert(%(expr).all!"!a.isNull", ` 509 | ~ `"@AllNonNull: assert(%(expr).all!\"!a.isNull\") failed%(info)");`) 510 | .values(values); 511 | } 512 | } 513 | else static if (__traits(compiles, isAssociativeArray!T) && isAssociativeArray!T) 514 | { 515 | enum checkValues = __traits(compiles, T.init.byValue.all!`a !is null`); 516 | enum checkKeys = __traits(compiles, T.init.byKey.all!"a !is null"); 517 | 518 | static if (!checkKeys && !checkValues) 519 | { 520 | checks ~= formatNamed!(`static assert(false, "Neither key nor value of associative array ` 521 | ~ `'%(expr)' can be checked against null.");`).values(values); 522 | } 523 | 524 | static if (checkValues) 525 | { 526 | checks ~= 527 | formatNamed!(`assert(%(expr).byValue.all!"a !is null", ` 528 | ~ `"@AllNonNull: assert(%(expr).byValue.all!\"a !is null\") failed%(info)");`) 529 | .values(values); 530 | } 531 | 532 | static if (checkKeys) 533 | { 534 | checks ~= 535 | formatNamed!(`assert(%(expr).byKey.all!"a !is null", ` 536 | ~ `"@AllNonNull: assert(%(expr).byKey.all!\"a !is null\") failed%(info)");`) 537 | .values(values); 538 | } 539 | } 540 | else 541 | { 542 | checks ~= formatNamed!`static assert(false, "Cannot compare all '%(expr)' to null");`.values(values); 543 | } 544 | 545 | return checks; 546 | } 547 | 548 | public enum IsConditionAttribute(alias A) = __traits(isSame, A, NonEmpty) || __traits(isSame, A, NonNull) 549 | || __traits(isSame, A, NonInit) || __traits(isSame, A, AllNonNull); 550 | -------------------------------------------------------------------------------- /src/boilerplate/constructor.d: -------------------------------------------------------------------------------- 1 | module boilerplate.constructor; 2 | 3 | import std.algorithm : canFind, map; 4 | import std.meta : AliasSeq, allSatisfy, ApplyLeft; 5 | import std.range : array; 6 | import std.traits : hasElaborateDestructor, isInstanceOf, isNested; 7 | import std.typecons : Tuple; 8 | 9 | version(unittest) 10 | { 11 | import unit_threaded.should; 12 | } 13 | 14 | /++ 15 | GenerateThis is a mixin string that automatically generates a this() function, customizable with UDA. 16 | +/ 17 | public enum string GenerateThis = ` 18 | import boilerplate.constructor : 19 | ConstructorField, GenerateThisTemplate, getUDADefaultOrNothing, saveConstructorInfo; 20 | import std.string : replace; 21 | mixin GenerateThisTemplate; 22 | mixin(typeof(this).generateThisImpl()); 23 | `; 24 | 25 | /** 26 | * `GenerateThis` creates a constructor with a parameter for every field. 27 | */ 28 | @("creates a constructor") 29 | unittest 30 | { 31 | class Class 32 | { 33 | int field; 34 | 35 | mixin(GenerateThis); 36 | } 37 | 38 | auto obj = new Class(5); 39 | 40 | obj.field.shouldEqual(5); 41 | } 42 | 43 | /** 44 | * When the super class also has a generated constructor, it will be called first. 45 | */ 46 | @("calls the super constructor if it exists") 47 | unittest 48 | { 49 | class Class 50 | { 51 | int field; 52 | 53 | mixin(GenerateThis); 54 | } 55 | 56 | class Child : Class 57 | { 58 | int field2; 59 | 60 | mixin(GenerateThis); 61 | } 62 | 63 | auto obj = new Child(5, 8); 64 | 65 | obj.field.shouldEqual(5); 66 | obj.field2.shouldEqual(8); 67 | } 68 | 69 | /** 70 | * Methods are ignored when generating constructors. 71 | */ 72 | @("separates fields from methods") 73 | unittest 74 | { 75 | class Class 76 | { 77 | int field; 78 | 79 | void method() { } 80 | 81 | mixin(GenerateThis); 82 | } 83 | 84 | auto obj = new Class(5); 85 | 86 | obj.field.shouldEqual(5); 87 | } 88 | 89 | /** 90 | * When passing arrays to the constructor, these arrays are automatically `dup`-ed. 91 | */ 92 | @("dups arrays") 93 | unittest 94 | { 95 | class Class 96 | { 97 | int[] array; 98 | 99 | mixin(GenerateThis); 100 | } 101 | 102 | auto array = [2, 3, 4]; 103 | auto obj = new Class(array); 104 | 105 | array[0] = 1; 106 | obj.array[0].shouldEqual(2); 107 | } 108 | 109 | /** 110 | * Arrays passed to the constructor are `dup`-ed even if they're inside a Nullable. 111 | */ 112 | @("dups arrays hidden behind Nullable") 113 | unittest 114 | { 115 | import std.typecons : Nullable, nullable; 116 | 117 | class Class 118 | { 119 | Nullable!(int[]) array; 120 | 121 | mixin(GenerateThis); 122 | } 123 | 124 | auto array = [2, 3, 4]; 125 | auto obj = new Class(array.nullable); 126 | 127 | array[0] = 1; 128 | obj.array.get[0].shouldEqual(2); 129 | 130 | obj = new Class(Nullable!(int[]).init); 131 | obj.array.isNull.shouldBeTrue; 132 | } 133 | 134 | /** 135 | * Associative arrays are also `dup`-ed. 136 | */ 137 | @("dups associative arrays") 138 | unittest 139 | { 140 | class Class 141 | { 142 | int[int] array; 143 | 144 | mixin(GenerateThis); 145 | } 146 | 147 | auto array = [2: 3]; 148 | auto obj = new Class(array); 149 | 150 | array[2] = 4; 151 | obj.array.shouldEqual([2: 3]); 152 | } 153 | 154 | /** 155 | * `@(This.Default!value)` defines a default value for the constructor parameter. 156 | */ 157 | @("uses default value for default constructor parameter") 158 | unittest 159 | { 160 | class Class 161 | { 162 | @(This.Default!5) 163 | int value = 5; 164 | 165 | mixin(GenerateThis); 166 | } 167 | 168 | auto obj1 = new Class(); 169 | 170 | obj1.value.shouldEqual(5); 171 | 172 | auto obj2 = new Class(6); 173 | 174 | obj2.value.shouldEqual(6); 175 | } 176 | 177 | /** 178 | * When using `GenerateThis` in an empty struct, no constructor is created. 179 | * 180 | * This is because D does not allow empty constructor methods. 181 | */ 182 | @("creates no constructor for an empty struct") 183 | unittest 184 | { 185 | struct Struct 186 | { 187 | mixin(GenerateThis); 188 | } 189 | 190 | auto strct = Struct(); 191 | } 192 | 193 | /** 194 | * `@(This.Default!(lambda))` calls the lambda to generate the default value. 195 | * 196 | * This is to handle cases like `@(This.Default!(new Class))`, where D would allocate 197 | * the class during startup and reuse the same reference for every constructor call. 198 | */ 199 | @("properly generates new default values on each call") 200 | unittest 201 | { 202 | import std.conv : to; 203 | 204 | class Class 205 | { 206 | @(This.Default!(() => new Object)) 207 | Object obj; 208 | 209 | mixin(GenerateThis); 210 | } 211 | 212 | auto obj1 = new Class(); 213 | auto obj2 = new Class(); 214 | 215 | (cast(void*) obj1.obj).shouldNotEqual(cast(void*) obj2.obj); 216 | } 217 | 218 | /** 219 | * When the superclass has a generated constructor, the order of parameters is: 220 | * 221 | * - Super class fields 222 | * - Class fields 223 | * - Class fields with default value 224 | * - Super class fields with default value 225 | */ 226 | @("establishes the parent-child parameter order: parent explicit, child explicit, child implicit, parent implicit.") 227 | unittest 228 | { 229 | class Parent 230 | { 231 | int field1; 232 | 233 | @(This.Default!2) 234 | int field2 = 2; 235 | 236 | mixin(GenerateThis); 237 | } 238 | 239 | class Child : Parent 240 | { 241 | int field3; 242 | 243 | @(This.Default!4) 244 | int field4 = 4; 245 | 246 | mixin(GenerateThis); 247 | } 248 | 249 | auto obj = new Child(1, 2, 3, 4); 250 | 251 | obj.field1.shouldEqual(1); 252 | obj.field3.shouldEqual(2); 253 | obj.field4.shouldEqual(3); 254 | obj.field2.shouldEqual(4); 255 | } 256 | 257 | /** 258 | * No constructor parameter is generated for static fields. 259 | */ 260 | @("disregards static fields") 261 | unittest 262 | { 263 | class Class 264 | { 265 | static int field1; 266 | int field2; 267 | 268 | mixin(GenerateThis); 269 | } 270 | 271 | auto obj = new Class(5); 272 | 273 | obj.field1.shouldEqual(0); 274 | obj.field2.shouldEqual(5); 275 | } 276 | 277 | /** 278 | * Immutable arrays are supported as constructor parameters. 279 | */ 280 | @("can initialize with immutable arrays") 281 | unittest 282 | { 283 | class Class 284 | { 285 | immutable(Object)[] array; 286 | 287 | mixin(GenerateThis); 288 | } 289 | } 290 | 291 | /** 292 | * `@(This.Private/Protected/Public)` can be used to define the visibility scope of the constructor. 293 | */ 294 | @("can define scope for constructor") 295 | unittest 296 | { 297 | @(This.Private) 298 | class PrivateClass 299 | { 300 | mixin(GenerateThis); 301 | } 302 | 303 | @(This.Protected) 304 | class ProtectedClass 305 | { 306 | mixin(GenerateThis); 307 | } 308 | 309 | @(This.Package) 310 | class PackageClass 311 | { 312 | mixin(GenerateThis); 313 | } 314 | 315 | @(This.Package("boilerplate")) 316 | class SubPackageClass 317 | { 318 | mixin(GenerateThis); 319 | } 320 | 321 | class PublicClass 322 | { 323 | mixin(GenerateThis); 324 | } 325 | 326 | static assert(__traits(getProtection, PrivateClass.__ctor) == "private"); 327 | static assert(__traits(getProtection, ProtectedClass.__ctor) == "protected"); 328 | static assert(__traits(getProtection, PackageClass.__ctor) == "package"); 329 | // getProtection does not return the package name of a package() attribute 330 | // static assert(__traits(getProtection, SubPackageClass.__ctor) == `package(boilerplate)`); 331 | static assert(__traits(getProtection, PublicClass.__ctor) == "public"); 332 | } 333 | 334 | /** 335 | * `@(This.Private/Protected/Public)` also assigns a visibility scope to the generated builder. 336 | */ 337 | @("will assign the same scope to Builder") 338 | unittest 339 | { 340 | @(This.Private) 341 | class PrivateClass 342 | { 343 | mixin(GenerateThis); 344 | } 345 | 346 | @(This.Protected) 347 | class ProtectedClass 348 | { 349 | mixin(GenerateThis); 350 | } 351 | 352 | @(This.Package) 353 | class PackageClass 354 | { 355 | mixin(GenerateThis); 356 | } 357 | 358 | @(This.Package("boilerplate")) 359 | class SubPackageClass 360 | { 361 | mixin(GenerateThis); 362 | } 363 | 364 | class PublicClass 365 | { 366 | mixin(GenerateThis); 367 | } 368 | 369 | static assert(__traits(getProtection, PrivateClass.Builder) == "private"); 370 | static assert(__traits(getProtection, ProtectedClass.Builder) == "protected"); 371 | static assert(__traits(getProtection, PackageClass.Builder) == "package"); 372 | static assert(__traits(getProtection, PublicClass.Builder) == "public"); 373 | } 374 | 375 | /** 376 | * `@(This.Default)` without a parameter uses the default value of the type. 377 | */ 378 | @("empty default tag means T()") 379 | unittest 380 | { 381 | class Class 382 | { 383 | @(This.Default) 384 | string s; 385 | 386 | @(This.Default) 387 | int i; 388 | 389 | mixin(GenerateThis); 390 | } 391 | 392 | (new Class()).i.shouldEqual(0); 393 | (new Class()).s.shouldEqual(string.init); 394 | } 395 | 396 | /** 397 | * `@(This.Exclude)` excludes a field from being set by the generated constructor. 398 | */ 399 | @("can exclude fields from constructor") 400 | unittest 401 | { 402 | class Class 403 | { 404 | @(This.Exclude) 405 | int i = 5; 406 | 407 | mixin(GenerateThis); 408 | } 409 | 410 | (new Class).i.shouldEqual(5); 411 | } 412 | 413 | /** 414 | * Even if the class holds a field that is not const, a const array can be passed to it 415 | * as long as the `dup` of this field does not leak mutable references. 416 | */ 417 | @("marks duppy parameters as const when this does not prevent dupping") 418 | unittest 419 | { 420 | 421 | struct Struct 422 | { 423 | } 424 | 425 | class Class 426 | { 427 | Struct[] values_; 428 | 429 | mixin(GenerateThis); 430 | } 431 | 432 | const Struct[] constValues; 433 | auto obj = new Class(constValues); 434 | } 435 | 436 | /** 437 | * Property functions are disregarded by the generated constructor 438 | */ 439 | @("does not include property functions in constructor list") 440 | unittest 441 | { 442 | class Class 443 | { 444 | int a; 445 | 446 | @property int foo() const 447 | { 448 | return 0; 449 | } 450 | 451 | mixin(GenerateThis); 452 | } 453 | 454 | static assert(__traits(compiles, new Class(0))); 455 | static assert(!__traits(compiles, new Class(0, 0))); 456 | } 457 | 458 | /** 459 | * When no parameters need to be dupped, the generated constructor is `@nogc`. 460 | */ 461 | @("declares @nogc on non-dupping constructors") 462 | @nogc unittest 463 | { 464 | struct Struct 465 | { 466 | int a; 467 | 468 | mixin(GenerateThis); 469 | } 470 | 471 | auto str = Struct(5); 472 | } 473 | 474 | /** 475 | * `@(This.Init!value)` defines a value for the field that will be assigned in the constructor, 476 | * and excludes the field from being a constructor parameter. 477 | */ 478 | @("can initialize fields using init value") 479 | unittest 480 | { 481 | class Class 482 | { 483 | @(This.Init!5) 484 | int field1; 485 | 486 | @(This.Init!(() => 8)) 487 | int field2; 488 | 489 | mixin(GenerateThis); 490 | } 491 | 492 | auto obj = new Class; 493 | 494 | obj.field1.shouldEqual(5); 495 | obj.field2.shouldEqual(8); 496 | } 497 | 498 | /** 499 | * `@(This.Init!lambda)`, like `@(This.Default!lambda)`, will be called automatically with `this` 500 | * to determine the initializer value. 501 | */ 502 | @("can initialize fields using init value, with lambda that accesses previous value") 503 | unittest 504 | { 505 | class Class 506 | { 507 | int field1; 508 | 509 | @(This.Init!(self => self.field1 + 5)) 510 | int field2; 511 | 512 | mixin(GenerateThis); 513 | } 514 | 515 | auto obj = new Class(5); 516 | 517 | obj.field1.shouldEqual(5); 518 | obj.field2.shouldEqual(10); 519 | } 520 | 521 | /** 522 | * `@(This.Init!lambda)` can allocate runtime values. 523 | */ 524 | @("can initialize fields with allocated types") 525 | unittest 526 | { 527 | class Class1 528 | { 529 | @(This.Init!(self => new Object)) 530 | Object object; 531 | 532 | mixin(GenerateThis); 533 | } 534 | 535 | class Class2 536 | { 537 | @(This.Init!(() => new Object)) 538 | Object object; 539 | 540 | mixin(GenerateThis); 541 | } 542 | 543 | class Class3 : Class2 544 | { 545 | mixin(GenerateThis); 546 | } 547 | } 548 | 549 | /** 550 | * `GenerateThis` creates a `.Builder()` property that can be used to incrementally construct the value. 551 | * 552 | * `builder.value` will call the generated constructor with the assigned values. 553 | */ 554 | @("generates Builder class that gathers constructor parameters, then calls constructor with them") 555 | unittest 556 | { 557 | static class Class 558 | { 559 | int field1; 560 | int field2; 561 | int field3; 562 | 563 | mixin(GenerateThis); 564 | } 565 | 566 | auto obj = { 567 | with (Class.Builder()) 568 | { 569 | field1 = 1; 570 | field2 = 2; 571 | field3 = 3; 572 | return value; 573 | } 574 | }(); 575 | 576 | with (obj) 577 | { 578 | field1.shouldEqual(1); 579 | field2.shouldEqual(2); 580 | field3.shouldEqual(3); 581 | } 582 | } 583 | 584 | /** 585 | * The order in which `Builder` fields are set is irrelevant. 586 | */ 587 | @("builder field order doesn't matter") 588 | unittest 589 | { 590 | static class Class 591 | { 592 | int field1; 593 | int field2; 594 | int field3; 595 | 596 | mixin(GenerateThis); 597 | } 598 | 599 | auto obj = { 600 | with (Class.Builder()) 601 | { 602 | field3 = 1; 603 | field1 = 2; 604 | field2 = 3; 605 | return value; 606 | } 607 | }(); 608 | 609 | with (obj) 610 | { 611 | field1.shouldEqual(2); 612 | field2.shouldEqual(3); 613 | field3.shouldEqual(1); 614 | } 615 | } 616 | 617 | /** 618 | * `Builder` fields with a `@(This.Default)` value can be omitted. 619 | */ 620 | @("default fields can be left out when assigning builder") 621 | unittest 622 | { 623 | static class Class 624 | { 625 | int field1; 626 | @(This.Default!5) 627 | int field2; 628 | int field3; 629 | 630 | mixin(GenerateThis); 631 | } 632 | 633 | // constructor is this(field1, field3, field2 = 5) 634 | auto obj = { 635 | with (Class.Builder()) 636 | { 637 | field1 = 1; 638 | field3 = 3; 639 | return value; 640 | } 641 | }(); 642 | 643 | with (obj) 644 | { 645 | field1.shouldEqual(1); 646 | field2.shouldEqual(5); 647 | field3.shouldEqual(3); 648 | } 649 | } 650 | 651 | /** 652 | * `Builder` can be used with structs as well as classes. 653 | */ 654 | @("supports Builder in structs") 655 | unittest 656 | { 657 | struct Struct 658 | { 659 | int field1; 660 | int field2; 661 | int field3; 662 | 663 | mixin(GenerateThis); 664 | } 665 | 666 | auto value = { 667 | with (Struct.Builder()) 668 | { 669 | field1 = 1; 670 | field3 = 3; 671 | field2 = 5; 672 | return value; 673 | } 674 | }(); 675 | 676 | static assert(is(typeof(value) == Struct)); 677 | 678 | with (value) 679 | { 680 | field1.shouldEqual(1); 681 | field2.shouldEqual(5); 682 | field3.shouldEqual(3); 683 | } 684 | } 685 | 686 | /** 687 | * `Builder` fields don't contain the trailing newline of a private field. 688 | */ 689 | @("builder strips trailing underlines") 690 | unittest 691 | { 692 | struct Struct 693 | { 694 | private int a_; 695 | 696 | mixin(GenerateThis); 697 | } 698 | 699 | auto builder = Struct.Builder(); 700 | 701 | builder.a = 1; 702 | 703 | auto value = builder.value; 704 | 705 | value.shouldEqual(Struct(1)); 706 | } 707 | 708 | /** 709 | * When a field in the type has a generated constructor itself, its fields can be set directly 710 | * on a `Builder` of the outer type. 711 | */ 712 | @("builder supports nested initialization") 713 | unittest 714 | { 715 | struct Struct1 716 | { 717 | int a; 718 | int b; 719 | 720 | mixin(GenerateThis); 721 | } 722 | 723 | struct Struct2 724 | { 725 | int c; 726 | Struct1 struct1; 727 | int d; 728 | 729 | mixin(GenerateThis); 730 | } 731 | 732 | auto builder = Struct2.Builder(); 733 | 734 | builder.struct1.a = 1; 735 | builder.struct1.b = 2; 736 | builder.c = 3; 737 | builder.d = 4; 738 | 739 | auto value = builder.value; 740 | 741 | static assert(is(typeof(value) == Struct2)); 742 | 743 | with (value) 744 | { 745 | struct1.a.shouldEqual(1); 746 | struct1.b.shouldEqual(2); 747 | c.shouldEqual(3); 748 | d.shouldEqual(4); 749 | } 750 | } 751 | 752 | /** 753 | * `Builder` can use `@(This.Default)` values for nested fields. 754 | */ 755 | @("builder supports defaults for nested values") 756 | unittest 757 | { 758 | struct Struct1 759 | { 760 | int a; 761 | int b; 762 | 763 | mixin(GenerateThis); 764 | } 765 | 766 | struct Struct2 767 | { 768 | int c; 769 | @(This.Default!(Struct1(3, 4))) 770 | Struct1 struct1; 771 | int d; 772 | 773 | mixin(GenerateThis); 774 | } 775 | 776 | auto builder = Struct2.Builder(); 777 | 778 | builder.c = 1; 779 | builder.d = 2; 780 | 781 | builder.value.shouldEqual(Struct2(1, 2, Struct1(3, 4))); 782 | } 783 | 784 | /** 785 | * `Builder` also allows assigning a single value directly for a nested `Builder` field. 786 | */ 787 | @("builder supports direct value assignment for nested values") 788 | unittest 789 | { 790 | struct Struct1 791 | { 792 | int a; 793 | int b; 794 | 795 | mixin(GenerateThis); 796 | } 797 | 798 | struct Struct2 799 | { 800 | int c; 801 | Struct1 struct1; 802 | int d; 803 | 804 | mixin(GenerateThis); 805 | } 806 | 807 | auto builder = Struct2.Builder(); 808 | 809 | builder.struct1 = Struct1(2, 3); 810 | builder.c = 1; 811 | builder.d = 4; 812 | 813 | builder.value.shouldEqual(Struct2(1, Struct1(2, 3), 4)); 814 | } 815 | 816 | /** 817 | * When a `Builder` field is an array, it can be initialized by 818 | * specifying the value for each desired index. 819 | */ 820 | @("builder supports recursive array index initialization") 821 | unittest 822 | { 823 | struct Struct1 824 | { 825 | int value; 826 | 827 | mixin(GenerateThis); 828 | } 829 | 830 | struct Struct2 831 | { 832 | Struct1[] array; 833 | 834 | mixin(GenerateThis); 835 | } 836 | 837 | auto builder = Struct2.Builder(); 838 | 839 | builder.array[0].value = 1; 840 | builder.array[1].value = 2; 841 | 842 | builder.value.shouldEqual(Struct2([Struct1(1), Struct1(2)])); 843 | } 844 | 845 | /** 846 | * `Builder` handles fields that are aliased to `this`. 847 | */ 848 | @("builder supports nested struct with alias this") 849 | unittest 850 | { 851 | struct Struct2 852 | { 853 | string text; 854 | 855 | alias text this; 856 | 857 | mixin(GenerateThis); 858 | } 859 | 860 | struct Struct1 861 | { 862 | Struct2 nested; 863 | 864 | mixin(GenerateThis); 865 | } 866 | 867 | auto builder = Struct1.Builder(); 868 | 869 | builder.nested.text = "foo"; 870 | 871 | builder.value.shouldEqual(Struct1(Struct2("foo"))); 872 | } 873 | 874 | /** 875 | * When a `Builder` field is an array, it can also be initialized by appending values. 876 | */ 877 | @("builder supports arrays as values") 878 | unittest 879 | { 880 | struct Struct1 881 | { 882 | int value; 883 | 884 | mixin(GenerateThis); 885 | } 886 | 887 | struct Struct2 888 | { 889 | Struct1[] array; 890 | 891 | mixin(GenerateThis); 892 | } 893 | 894 | auto builder = Struct2.Builder(); 895 | 896 | builder.array ~= Struct1(1); 897 | builder.array ~= Struct1(2); 898 | 899 | builder.value.shouldEqual(Struct2([Struct1(1), Struct1(2)])); 900 | } 901 | 902 | /** 903 | * When a value has been assigned to a recursive `Builder` field, its fields can still 904 | * be individually overridden. 905 | * This uses the `BuilderFrom` reverse-`Builder` property. 906 | */ 907 | @("builder supports overriding value assignment with field assignment later") 908 | unittest 909 | { 910 | struct Struct1 911 | { 912 | int a; 913 | int b; 914 | 915 | mixin(GenerateThis); 916 | } 917 | 918 | struct Struct2 919 | { 920 | Struct1 struct1; 921 | 922 | mixin(GenerateThis); 923 | } 924 | 925 | auto builder = Struct2.Builder(); 926 | 927 | builder.struct1 = Struct1(2, 3); 928 | builder.struct1.b = 4; 929 | 930 | builder.value.shouldEqual(Struct2(Struct1(2, 4))); 931 | } 932 | 933 | /** 934 | * When a recursive `Builder` field has already been directly assigned, it cannot be 935 | * later overwritten with a whole-value assignment. 936 | */ 937 | @("builder refuses overriding field assignment with value assignment") 938 | unittest 939 | { 940 | import core.exception : AssertError; 941 | 942 | struct Struct1 943 | { 944 | int a; 945 | int b; 946 | 947 | mixin(GenerateThis); 948 | } 949 | 950 | struct Struct2 951 | { 952 | Struct1 struct1; 953 | 954 | mixin(GenerateThis); 955 | } 956 | 957 | auto builder = Struct2.Builder(); 958 | 959 | builder.struct1.b = 4; 960 | 961 | void set() 962 | { 963 | builder.struct1 = Struct1(2, 3); 964 | } 965 | set().shouldThrow!AssertError("Builder: cannot set field by value since a subfield has already been set."); 966 | } 967 | 968 | /** 969 | * `Builder` supports assigning const fields. 970 | */ 971 | @("builder supports const args") 972 | unittest 973 | { 974 | struct Struct 975 | { 976 | const int a; 977 | 978 | mixin(GenerateThis); 979 | } 980 | 981 | with (Struct.Builder()) 982 | { 983 | a = 5; 984 | 985 | value.shouldEqual(Struct(5)); 986 | } 987 | } 988 | 989 | /** 990 | * `Builder` supports assigning recursive values with a destructor. 991 | */ 992 | @("builder supports fields with destructor") 993 | unittest 994 | { 995 | static struct Struct1 996 | { 997 | ~this() pure @safe @nogc nothrow { } 998 | } 999 | 1000 | struct Struct2 1001 | { 1002 | Struct1 struct1; 1003 | 1004 | mixin(GenerateThis); 1005 | } 1006 | 1007 | with (Struct2.Builder()) 1008 | { 1009 | struct1 = Struct1(); 1010 | 1011 | value.shouldEqual(Struct2(Struct1())); 1012 | } 1013 | } 1014 | 1015 | /** 1016 | * When a `Builder` field is `Nullable!T`, it can be directly assigned a `T`. 1017 | */ 1018 | @("builder supports direct assignment to Nullables") 1019 | unittest 1020 | { 1021 | import std.typecons : Nullable, nullable; 1022 | 1023 | struct Struct 1024 | { 1025 | const Nullable!int a; 1026 | 1027 | mixin(GenerateThis); 1028 | } 1029 | 1030 | with (Struct.Builder()) 1031 | { 1032 | a = 5; 1033 | 1034 | value.shouldEqual(Struct(5.nullable)); 1035 | } 1036 | } 1037 | 1038 | /** 1039 | * When a `Builder` field is `Nullable!T`, and `T` has a `Builder`, `T`'s fields can be directly assigned. 1040 | */ 1041 | @("builder with passthrough assignment to Nullable structs") 1042 | unittest 1043 | { 1044 | import std.typecons : Nullable, nullable; 1045 | 1046 | struct Struct1 1047 | { 1048 | int a; 1049 | 1050 | mixin(GenerateThis); 1051 | } 1052 | 1053 | struct Struct2 1054 | { 1055 | @(This.Default) 1056 | Nullable!Struct1 b; 1057 | 1058 | mixin(GenerateThis); 1059 | } 1060 | 1061 | with (Struct2.Builder()) 1062 | { 1063 | value.shouldEqual(Struct2(Nullable!Struct1())); 1064 | 1065 | b.a = 5; 1066 | 1067 | value.shouldEqual(Struct2(Nullable!Struct1(Struct1(5)))); 1068 | } 1069 | } 1070 | 1071 | /** 1072 | * When a `Builder` field is `Nullable!T`, and `T` has a `Builder`, `T` can still be assigned either as 1073 | * `T` or `Nullable!T`. 1074 | */ 1075 | @("builder with value assignment to Nullable struct field") 1076 | unittest 1077 | { 1078 | import std.typecons : Nullable, nullable; 1079 | 1080 | struct Struct1 1081 | { 1082 | mixin(GenerateThis); 1083 | } 1084 | 1085 | struct Struct2 1086 | { 1087 | @(This.Default) 1088 | Nullable!Struct1 value; 1089 | 1090 | mixin(GenerateThis); 1091 | } 1092 | 1093 | with (Struct2.Builder()) 1094 | { 1095 | builderValue.shouldEqual(Struct2()); 1096 | 1097 | value = Struct1(); 1098 | 1099 | builderValue.shouldEqual(Struct2(Struct1().nullable)); 1100 | } 1101 | 1102 | with (Struct2.Builder()) 1103 | { 1104 | value = Nullable!Struct1(); 1105 | 1106 | builderValue.shouldEqual(Struct2()); 1107 | } 1108 | } 1109 | 1110 | /** 1111 | * A value with `GenerateThis` can be turned back into a builder using `BuilderFrom()`. 1112 | * This can be used to reassign immutable fields. 1113 | */ 1114 | @("builder supports reconstruction from value") 1115 | unittest 1116 | { 1117 | import std.typecons : Nullable, nullable; 1118 | 1119 | struct Struct 1120 | { 1121 | private int a_; 1122 | 1123 | int[] b; 1124 | 1125 | mixin(GenerateThis); 1126 | } 1127 | 1128 | const originalValue = Struct(2, [3]); 1129 | 1130 | with (originalValue.BuilderFrom()) 1131 | { 1132 | a = 5; 1133 | 1134 | value.shouldEqual(Struct(5, [3])); 1135 | } 1136 | } 1137 | 1138 | /** 1139 | * When a type already has a `value` field, `builderValue` can be used to get the builder value. 1140 | */ 1141 | @("builder supports struct that already contains a value field") 1142 | unittest 1143 | { 1144 | import std.typecons : Nullable, nullable; 1145 | 1146 | struct Struct 1147 | { 1148 | private int value_; 1149 | 1150 | mixin(GenerateThis); 1151 | } 1152 | 1153 | with (Struct.Builder()) 1154 | { 1155 | value = 5; 1156 | 1157 | builderValue.shouldEqual(Struct(5)); 1158 | } 1159 | } 1160 | 1161 | /** 1162 | * `Builder` will handle structs that contain structs with `@disable(this)`. 1163 | */ 1164 | @("builder supports struct that contains struct that has @disable(this)") 1165 | unittest 1166 | { 1167 | import std.typecons : Nullable, nullable; 1168 | 1169 | static struct Inner 1170 | { 1171 | private int i_; 1172 | 1173 | @disable this(); 1174 | 1175 | mixin(GenerateThis); 1176 | } 1177 | 1178 | static struct Struct 1179 | { 1180 | private Inner inner_; 1181 | 1182 | mixin(GenerateThis); 1183 | } 1184 | 1185 | with (Struct.Builder()) 1186 | { 1187 | inner.i = 3; 1188 | 1189 | value.shouldEqual(Struct(Inner(3))); 1190 | } 1191 | } 1192 | 1193 | @("destructors with code that is unsafe, system or throws exceptions") 1194 | { 1195 | struct S 1196 | { 1197 | ~this() { throw new Exception("test"); } 1198 | } 1199 | 1200 | struct T 1201 | { 1202 | S s; 1203 | 1204 | mixin(GenerateThis); 1205 | } 1206 | } 1207 | 1208 | @("builder supports appending to transitive non-const fields") 1209 | unittest 1210 | { 1211 | struct Struct1 1212 | { 1213 | int[] values; 1214 | 1215 | mixin(GenerateThis); 1216 | } 1217 | 1218 | struct Struct2 1219 | { 1220 | Struct1[] array; 1221 | 1222 | mixin(GenerateThis); 1223 | } 1224 | 1225 | auto builder = Struct2.Builder(); 1226 | 1227 | builder.array ~= [Struct1([1]), Struct1([2])]; 1228 | 1229 | builder.value.shouldEqual(Struct2([Struct1([1]), Struct1([2])])); 1230 | } 1231 | 1232 | import std.string : format; 1233 | 1234 | mixin template GenerateThisTemplate() 1235 | { 1236 | private static generateThisImpl() 1237 | { 1238 | if (!__ctfe) 1239 | { 1240 | return null; 1241 | } 1242 | 1243 | import boilerplate.constructor : filterCanFind, mapFormat, This; 1244 | import boilerplate.util : 1245 | bucketSort, GenNormalMemberTuple, needToDup, 1246 | optionallyRemoveTrailingUnderline, 1247 | removeTrailingUnderline, reorder, udaIndex; 1248 | import std.algorithm : all, canFind, filter, map; 1249 | import std.meta : Alias, aliasSeqOf, staticMap; 1250 | import std.range : array, drop, empty, iota, zip; 1251 | import std.string : endsWith, format, join; 1252 | import std.typecons : Nullable; 1253 | 1254 | mixin GenNormalMemberTuple; 1255 | 1256 | string result = null; 1257 | 1258 | string visibility = "public"; 1259 | 1260 | foreach (uda; __traits(getAttributes, typeof(this))) 1261 | { 1262 | static if (is(typeof(uda) == ThisEnum)) 1263 | { 1264 | static if (uda == This.Protected) 1265 | { 1266 | visibility = "protected"; 1267 | } 1268 | static if (uda == This.Private) 1269 | { 1270 | visibility = "private"; 1271 | } 1272 | } 1273 | else static if (is(uda == This.Package)) 1274 | { 1275 | visibility = "package"; 1276 | } 1277 | else static if (is(typeof(uda) == This.Package)) 1278 | { 1279 | visibility = "package(" ~ uda.packageMask ~ ")"; 1280 | } 1281 | } 1282 | 1283 | string[] constructorAttributes = ["pure", "nothrow", "@safe", "@nogc"]; 1284 | 1285 | static if (is(typeof(typeof(super).ConstructorInfo))) 1286 | { 1287 | enum argsPassedToSuper = typeof(super).ConstructorInfo.fields.length; 1288 | enum members = typeof(super).ConstructorInfo.fields ~ [NormalMemberTuple]; 1289 | 1290 | constructorAttributes = typeof(super).GeneratedConstructorAttributes_; 1291 | } 1292 | else 1293 | { 1294 | enum argsPassedToSuper = 0; 1295 | static if (NormalMemberTuple.length > 0) 1296 | { 1297 | enum members = [NormalMemberTuple]; 1298 | } 1299 | else 1300 | { 1301 | enum string[] members = null; 1302 | } 1303 | } 1304 | 1305 | string[] fields; 1306 | string[] args; 1307 | string[] argexprs; 1308 | string[] defaultAssignments; 1309 | bool[] fieldUseDefault; 1310 | string[] fieldDefault; 1311 | string[] fieldAttributes; 1312 | string[] types; 1313 | string[] directInitFields; 1314 | int[] directInitIndex; 1315 | bool[] directInitUseSelf; 1316 | 1317 | foreach (i; aliasSeqOf!(members.length.iota)) 1318 | { 1319 | enum member = members[i]; 1320 | 1321 | static if (i < argsPassedToSuper) 1322 | { 1323 | enum bool useDefault = __traits(getMember, typeof(super).ConstructorInfo.FieldInfo, member).useDefault; 1324 | enum string memberTypeAsString = "typeof(super).ConstructorInfo.FieldInfo." ~ member ~ ".Type"; 1325 | enum string default_ = "typeof(super).ConstructorInfo.FieldInfo." ~ member ~ ".fieldDefault"; 1326 | enum string attributes = "typeof(super).ConstructorInfo.FieldInfo." ~ member ~ ".attributes"; 1327 | } 1328 | else 1329 | { 1330 | enum bool useDefault = udaIndex!(This.Default, __traits(getAttributes, __traits(getMember, typeof(this), member))) != -1; 1331 | enum string memberTypeAsString = "typeof(this." ~ member ~ ")"; 1332 | enum string default_ = "getUDADefaultOrNothing!(typeof(this." ~ member ~ "), __traits(getAttributes, this." ~ member ~ "))"; 1333 | enum string attributes = "__traits(getAttributes, this." ~ member ~ ")"; 1334 | } 1335 | 1336 | mixin(`alias Type = ` ~ memberTypeAsString ~ `;`); 1337 | 1338 | bool includeMember = false; 1339 | 1340 | enum isNullable = is(Type: Nullable!Arg, Arg); 1341 | 1342 | static if (!isNullable) 1343 | { 1344 | enum bool dupExpr = needToDup!Type; 1345 | enum bool passExprAsConst = dupExpr && __traits(compiles, { Type value = const(Type).init.dup; }); 1346 | } 1347 | else 1348 | { 1349 | // unpack nullable for dup 1350 | enum bool dupExpr = needToDup!(typeof(Type.init.get)); 1351 | enum bool passExprAsConst = dupExpr && __traits(compiles, { Type value = const(Type).init.get.dup; }); 1352 | } 1353 | 1354 | enum scopeAttributes = [__traits(getFunctionAttributes, { 1355 | static if (passExprAsConst) { const Type parameter = Type.init; } 1356 | else { Type parameter = Type.init; } 1357 | 1358 | static if (isNullable) { auto value = parameter.get; } 1359 | else { auto value = parameter; } 1360 | 1361 | static if (dupExpr) 1362 | { 1363 | Type dupped = value.dup; 1364 | } 1365 | })]; 1366 | constructorAttributes = constructorAttributes.filterCanFind(scopeAttributes); 1367 | 1368 | bool forSuper = false; 1369 | 1370 | static if (i < argsPassedToSuper) 1371 | { 1372 | includeMember = true; 1373 | forSuper = true; 1374 | } 1375 | else 1376 | { 1377 | mixin("alias symbol = typeof(this)." ~ member ~ ";"); 1378 | 1379 | static assert (is(typeof(symbol)) && !__traits(isTemplate, symbol)); /* must have a resolvable type */ 1380 | 1381 | import boilerplate.util: isStatic; 1382 | 1383 | includeMember = !mixin(isStatic(member)); 1384 | 1385 | static if (udaIndex!(This.Init, __traits(getAttributes, symbol)) != -1) 1386 | { 1387 | enum udaFieldIndex = udaIndex!(This.Init, __traits(getAttributes, symbol)); 1388 | alias initArg = Alias!(__traits(getAttributes, symbol)[udaFieldIndex].value); 1389 | enum lambdaWithSelf = __traits(compiles, initArg(typeof(this).init)); 1390 | enum nakedLambda = __traits(compiles, initArg()); 1391 | 1392 | directInitFields ~= member; 1393 | directInitIndex ~= udaFieldIndex; 1394 | directInitUseSelf ~= __traits(compiles, 1395 | __traits(getAttributes, symbol)[udaFieldIndex].value(typeof(this).init)); 1396 | includeMember = false; 1397 | 1398 | static if (lambdaWithSelf) 1399 | { 1400 | static if (__traits(compiles, initArg!(typeof(this)))) 1401 | { 1402 | enum lambdaAttributes = [__traits(getFunctionAttributes, initArg!(typeof(this)))]; 1403 | } 1404 | else 1405 | { 1406 | enum lambdaAttributes = [__traits(getFunctionAttributes, initArg)]; 1407 | } 1408 | constructorAttributes = constructorAttributes.filterCanFind(lambdaAttributes); 1409 | } 1410 | else static if (nakedLambda) 1411 | { 1412 | enum lambdaAttributes = [__traits(getFunctionAttributes, initArg)]; 1413 | 1414 | constructorAttributes = constructorAttributes.filterCanFind(lambdaAttributes); 1415 | } 1416 | } 1417 | 1418 | static if (udaIndex!(This.Exclude, __traits(getAttributes, symbol)) != -1) 1419 | { 1420 | includeMember = false; 1421 | } 1422 | } 1423 | 1424 | if (!includeMember) continue; 1425 | 1426 | enum paramName = optionallyRemoveTrailingUnderline!member; 1427 | 1428 | string argexpr = paramName; 1429 | 1430 | if (dupExpr) 1431 | { 1432 | constructorAttributes = constructorAttributes.filter!(q{a != "@nogc"}).array; 1433 | 1434 | static if (isNullable) 1435 | { 1436 | argexpr = format!`%s.isNull ? %s.init : %s(%s.get.dup)` 1437 | (argexpr, memberTypeAsString, memberTypeAsString, argexpr); 1438 | } 1439 | else 1440 | { 1441 | argexpr = argexpr ~ ".dup"; 1442 | } 1443 | } 1444 | 1445 | fields ~= member; 1446 | args ~= paramName; 1447 | argexprs ~= argexpr; 1448 | fieldUseDefault ~= useDefault; 1449 | fieldDefault ~= default_; 1450 | fieldAttributes ~= attributes; 1451 | defaultAssignments ~= useDefault ? (` = ` ~ default_) : ``; 1452 | types ~= passExprAsConst ? (`const ` ~ memberTypeAsString) : memberTypeAsString; 1453 | } 1454 | 1455 | size_t establishParameterRank(size_t i) 1456 | { 1457 | // parent explicit, our explicit, our implicit, parent implicit 1458 | const fieldOfParent = i < argsPassedToSuper; 1459 | return fieldUseDefault[i] * 2 + (fieldUseDefault[i] == fieldOfParent); 1460 | } 1461 | 1462 | auto constructorFieldOrder = fields.length.iota.array.bucketSort(&establishParameterRank); 1463 | 1464 | assert(fields.length == types.length); 1465 | assert(fields.length == fieldUseDefault.length); 1466 | assert(fields.length == fieldDefault.length); 1467 | 1468 | result ~= format!` 1469 | public static alias ConstructorInfo = 1470 | saveConstructorInfo!(%s, %-(%s, %));` 1471 | ( 1472 | fields.reorder(constructorFieldOrder), 1473 | zip( 1474 | types.reorder(constructorFieldOrder), 1475 | fieldUseDefault.reorder(constructorFieldOrder), 1476 | fieldDefault.reorder(constructorFieldOrder), 1477 | fieldAttributes.reorder(constructorFieldOrder), 1478 | ) 1479 | .map!(q{format!`ConstructorField!(%s, %s, %s, %s)`(a[0], a[1], a[2], a[3])}) 1480 | .array 1481 | ); 1482 | 1483 | // don't emit this(a = b, c = d) for structs - 1484 | // the compiler complains that it collides with this(), which is reserved. 1485 | if (is(typeof(this) == struct) && fieldUseDefault.all) 1486 | { 1487 | // If there are fields, their direct-construction types may diverge from ours 1488 | // specifically, see the "struct with only default fields" test below 1489 | if (!fields.empty) 1490 | { 1491 | result ~= `static assert( 1492 | is(typeof(this.tupleof) == ConstructorInfo.Types), 1493 | "Structs with fields, that are all default, cannot use GenerateThis when their " ~ 1494 | "constructor types would diverge from their native types: " ~ 1495 | typeof(this).stringof ~ ".this" ~ typeof(this.tupleof).stringof ~ ", " ~ 1496 | "but generated constructor would have been " ~ typeof(this).stringof ~ ".this" 1497 | ~ ConstructorInfo.Types.stringof 1498 | );`; 1499 | } 1500 | } 1501 | else 1502 | { 1503 | result ~= visibility ~ ` this(` 1504 | ~ constructorFieldOrder.mapFormat!`%s %s%s`(types, args, defaultAssignments).join(`, `) 1505 | ~ format!`) %-(%s %)`(constructorAttributes); 1506 | 1507 | result ~= `{`; 1508 | 1509 | static if (is(typeof(typeof(super).ConstructorInfo))) 1510 | { 1511 | result ~= `super(` ~ args[0 .. argsPassedToSuper].join(", ") ~ `);`; 1512 | } 1513 | 1514 | result ~= fields.length.iota.drop(argsPassedToSuper).mapFormat!`this.%s = %s;`(fields, argexprs).join; 1515 | 1516 | foreach (i, field; directInitFields) 1517 | { 1518 | if (directInitUseSelf[i]) 1519 | { 1520 | result ~= format!`this.%s = __traits(getAttributes, this.%s)[%s].value(this);` 1521 | (field, field, directInitIndex[i]); 1522 | } 1523 | else 1524 | { 1525 | result ~= format!`this.%s = __traits(getAttributes, this.%s)[%s].value;` 1526 | (field, field, directInitIndex[i]); 1527 | } 1528 | } 1529 | 1530 | result ~= `}`; 1531 | 1532 | result ~= `protected static enum string[] GeneratedConstructorAttributes_ = [` 1533 | ~ constructorAttributes.map!(q{`"` ~ a ~ `"`}).join(`, `) 1534 | ~ `];`; 1535 | } 1536 | 1537 | result ~= visibility ~ ` static struct BuilderType(alias T = typeof(this)) 1538 | { 1539 | import boilerplate.builder : BuilderImpl; 1540 | 1541 | mixin BuilderImpl!T; 1542 | }`; 1543 | 1544 | result ~= visibility ~ ` static auto Builder()() 1545 | { 1546 | return BuilderType!()(); 1547 | }`; 1548 | 1549 | /** 1550 | * We are allowed to read the private field values here. 1551 | * We aren't actually leaking private or mutable information because: 1552 | * - the constructor will dup it again anyways, if required 1553 | * - we cannot read it from the Builder, because Builders are write-only 1554 | * - if we can't read it off the current value, the builderValue will 1555 | * have the same type - so we can't read it off there either! 1556 | */ 1557 | result ~= visibility ~ ` auto BuilderFrom(this This)() 1558 | { 1559 | import boilerplate.util : optionallyRemoveTrailingUnderline; 1560 | 1561 | auto builder = BuilderType!()(); 1562 | 1563 | static foreach (field; ConstructorInfo.fields) 1564 | { 1565 | mixin("builder." ~ optionallyRemoveTrailingUnderline!field ~ " = this." ~ field ~ ";"); 1566 | } 1567 | return builder; 1568 | }`; 1569 | 1570 | return result; 1571 | } 1572 | } 1573 | 1574 | public template ConstructorField(Type_, bool useDefault_, alias fieldDefault_, attributes_...) 1575 | { 1576 | public alias Type = Type_; 1577 | public enum useDefault = useDefault_; 1578 | public alias fieldDefault = fieldDefault_; 1579 | public alias attributes = attributes_; 1580 | } 1581 | 1582 | public template saveConstructorInfo(string[] fields_, Fields...) 1583 | // if (fields_.length == Fields.length 1584 | // && allSatisfy!(ApplyLeft!(isInstanceOf, ConstructorField), Fields)) 1585 | { 1586 | import std.format : format; 1587 | 1588 | public enum fields = fields_; 1589 | 1590 | private template FieldInfo_() { 1591 | static foreach (i, field; fields) 1592 | { 1593 | mixin(format!q{public alias %s = Fields[%s];}(field, i)); 1594 | } 1595 | } 1596 | 1597 | public alias FieldInfo = FieldInfo_!(); 1598 | 1599 | mixin( 1600 | format!q{public alias Types = AliasSeq!(%-(%s, %)); } 1601 | (fields.map!(field => format!"FieldInfo.%s.Type"(field)).array)); 1602 | } 1603 | 1604 | enum ThisEnum 1605 | { 1606 | Private, 1607 | Protected, 1608 | Exclude 1609 | } 1610 | 1611 | struct This 1612 | { 1613 | enum Private = ThisEnum.Private; 1614 | enum Protected = ThisEnum.Protected; 1615 | struct Package 1616 | { 1617 | string packageMask = null; 1618 | } 1619 | enum Exclude = ThisEnum.Exclude; 1620 | 1621 | // construct with value 1622 | static struct Init(alias Alias) 1623 | { 1624 | static if (__traits(compiles, Alias())) 1625 | { 1626 | @property static auto value() { return Alias(); } 1627 | } 1628 | else 1629 | { 1630 | alias value = Alias; 1631 | } 1632 | } 1633 | 1634 | static struct Default(alias Alias) 1635 | { 1636 | static if (__traits(compiles, Alias())) 1637 | { 1638 | @property static auto value() { return Alias(); } 1639 | } 1640 | else 1641 | { 1642 | alias value = Alias; 1643 | } 1644 | } 1645 | } 1646 | 1647 | public template getUDADefaultOrNothing(T, attributes...) 1648 | { 1649 | import boilerplate.util : udaIndex; 1650 | 1651 | template EnumTest() 1652 | { 1653 | enum EnumTest = attributes[udaIndex!(This.Default, attributes)].value; 1654 | } 1655 | 1656 | static if (udaIndex!(This.Default, attributes) == -1) 1657 | { 1658 | enum getUDADefaultOrNothing = 0; 1659 | } 1660 | // @(This.Default) 1661 | else static if (__traits(isSame, attributes[udaIndex!(This.Default, attributes)], This.Default)) 1662 | { 1663 | enum getUDADefaultOrNothing = T.init; 1664 | } 1665 | else static if (__traits(compiles, EnumTest!())) 1666 | { 1667 | enum getUDADefaultOrNothing = attributes[udaIndex!(This.Default, attributes)].value; 1668 | } 1669 | else 1670 | { 1671 | @property static auto getUDADefaultOrNothing() 1672 | { 1673 | return attributes[udaIndex!(This.Default, attributes)].value; 1674 | } 1675 | } 1676 | } 1677 | 1678 | @("struct with only default fields cannot use GenerateThis unless the default this() type matches the generated one") 1679 | unittest 1680 | { 1681 | static assert(!__traits(compiles, { 1682 | struct Foo 1683 | { 1684 | @(This.Default) 1685 | int[] array; 1686 | 1687 | mixin(GenerateThis); 1688 | } 1689 | 1690 | // because you would be able to do 1691 | // const array = [2]; 1692 | // auto foo = Foo(array); 1693 | // which would be an error, but work with a generated constructor 1694 | // however, no constructor could be generated, as it would collide with this() 1695 | })); 1696 | 1697 | // This works though. 1698 | struct Bar 1699 | { 1700 | @(This.Default) 1701 | const int[] array; 1702 | 1703 | mixin(GenerateThis); 1704 | } 1705 | 1706 | const array = [2]; 1707 | auto bar = Bar(array); 1708 | } 1709 | 1710 | @("very large types can be used") 1711 | unittest 1712 | { 1713 | import std.format : format; 1714 | import std.range : iota; 1715 | 1716 | struct VeryLargeType 1717 | { 1718 | static foreach (i; 500.iota) 1719 | { 1720 | mixin(format!"int v%s;"(i)); 1721 | } 1722 | 1723 | mixin(GenerateThis); 1724 | } 1725 | 1726 | struct Wrapper 1727 | { 1728 | VeryLargeType field; 1729 | 1730 | mixin(GenerateThis); 1731 | } 1732 | 1733 | auto builder = Wrapper.Builder(); 1734 | } 1735 | 1736 | @("const nullable assignment") 1737 | unittest 1738 | { 1739 | import std.typecons : Nullable; 1740 | 1741 | // non-reference type 1742 | struct Foo 1743 | { 1744 | } 1745 | 1746 | struct Bar 1747 | { 1748 | Nullable!Foo foo; 1749 | 1750 | mixin(GenerateThis); 1751 | } 1752 | 1753 | auto builder = Bar.Builder(); 1754 | 1755 | // trigger assignment bug where dmd tries to roundtrip over const(Foo), implicitly triggering .get 1756 | // avoided by additional assignment overload in the Nullable case 1757 | builder.foo = Nullable!(const Foo)(); 1758 | } 1759 | 1760 | // can't strip const, because int[] is a reference type and precludes it 1761 | @("const nullable assignment with reference type") 1762 | unittest 1763 | { 1764 | import std.typecons : Nullable, nullable; 1765 | 1766 | struct Foo 1767 | { 1768 | int[] reference; 1769 | } 1770 | 1771 | struct Bar 1772 | { 1773 | Nullable!Foo foo; 1774 | 1775 | mixin(GenerateThis); 1776 | } 1777 | 1778 | auto builder = Bar.Builder(); 1779 | 1780 | int[] array = [2]; 1781 | auto foo = Foo(array); 1782 | 1783 | // direct assignment still works 1784 | static assert(__traits(compiles, { builder.foo = foo.nullable; })); 1785 | // but const assignment is blocked by opAssign(U) 1786 | static assert(!__traits(compiles, { builder.foo = (cast(const) foo).nullable; })); 1787 | } 1788 | 1789 | @("nullable null assignment to buildable field") 1790 | unittest 1791 | { 1792 | import std.typecons : Nullable; 1793 | 1794 | struct Foo 1795 | { 1796 | mixin(GenerateThis); 1797 | } 1798 | 1799 | struct Bar 1800 | { 1801 | Nullable!Foo foo; 1802 | 1803 | mixin(GenerateThis); 1804 | } 1805 | 1806 | auto builder = Bar.Builder(); 1807 | 1808 | builder.foo = Nullable!Foo(); 1809 | 1810 | builder.value.shouldEqual(Bar(Nullable!Foo())); 1811 | } 1812 | 1813 | @("@safe generated class constructor") 1814 | unittest 1815 | { 1816 | class Foo 1817 | { 1818 | pure: 1819 | @safe: 1820 | 1821 | int i; 1822 | 1823 | mixin(GenerateThis); 1824 | } 1825 | } 1826 | 1827 | // helper to avoid lambda, std.algorithm use in heavily-reused mixin GenerateThisTemplate 1828 | public string[] filterCanFind(string[] array, string[] other) nothrow pure @safe 1829 | { 1830 | import std.algorithm : canFind, filter; 1831 | 1832 | return array.filter!(a => other.canFind(a)).array; 1833 | } 1834 | 1835 | // ditto 1836 | public string[] mapFormat(string fmt, Range, T...)(Range range, T args) 1837 | { 1838 | import std.algorithm : map; 1839 | import std.format : format; 1840 | import std.range : iota, join; 1841 | 1842 | enum argElements = T.length.iota.map!(k => format!"args[%s][i]"(k)).join(", "); 1843 | 1844 | return range.map!((i) { 1845 | return mixin("format!fmt(" ~ argElements ~ ")"); 1846 | }).array; 1847 | } 1848 | -------------------------------------------------------------------------------- /src/boilerplate/package.d: -------------------------------------------------------------------------------- 1 | module boilerplate; 2 | 3 | public import boilerplate.accessors; 4 | 5 | public import boilerplate.autostring; 6 | 7 | public import boilerplate.conditions; 8 | 9 | public import boilerplate.constructor; 10 | 11 | enum GenerateAll = GenerateThis ~ GenerateToString ~ GenerateFieldAccessors ~ GenerateInvariants; 12 | 13 | @("can use all four generators at once") 14 | unittest 15 | { 16 | import core.exception : AssertError; 17 | import std.conv : to; 18 | import unit_threaded.should : shouldEqual, shouldThrow; 19 | 20 | class Class 21 | { 22 | @ConstRead @Write @NonInit 23 | int i_; 24 | 25 | mixin(GenerateAll); 26 | } 27 | 28 | auto obj = new Class(5); 29 | 30 | obj.i.shouldEqual(5); 31 | obj.to!string.shouldEqual("Class(i=5)"); 32 | obj.i(0).shouldThrow!AssertError; 33 | } 34 | 35 | // regression test for workaround for https://issues.dlang.org/show_bug.cgi?id=19731 36 | @("accessor on field in struct with invariant and constructor") 37 | unittest 38 | { 39 | import core.exception : AssertError; 40 | import unit_threaded.should : shouldThrow; 41 | 42 | struct Struct 43 | { 44 | @NonNull 45 | @ConstRead 46 | Object constObject_; 47 | 48 | @NonNull 49 | @Read 50 | Object object_; 51 | 52 | mixin(GenerateAll); 53 | } 54 | 55 | Struct().constObject.shouldThrow!AssertError; 56 | Struct().object.shouldThrow!AssertError; 57 | } 58 | 59 | @("field with reserved name") 60 | unittest 61 | { 62 | struct Struct 63 | { 64 | int version_; 65 | 66 | mixin(GenerateAll); 67 | } 68 | 69 | with (Struct.Builder()) 70 | { 71 | version_ = 5; 72 | 73 | assert(value.version_ == 5); 74 | assert(value.BuilderFrom().value.version_ == 5); 75 | } 76 | } 77 | 78 | @("class with no members") 79 | unittest 80 | { 81 | static class Class 82 | { 83 | mixin(GenerateAll); 84 | } 85 | 86 | auto instance = Class.Builder().value; 87 | } 88 | 89 | @("underscore property is aliased to this") 90 | unittest 91 | { 92 | struct Foo 93 | { 94 | @ConstRead 95 | int i_; 96 | 97 | alias i_ this; 98 | 99 | mixin(GenerateAll); 100 | } 101 | 102 | auto builder = Foo.Builder(); 103 | 104 | cast(void) builder; 105 | } 106 | -------------------------------------------------------------------------------- /src/boilerplate/toString/bitflags.d: -------------------------------------------------------------------------------- 1 | module boilerplate.toString.bitflags; 2 | 3 | import std.typecons : BitFlags; 4 | 5 | void toString(T: const BitFlags!Enum, Enum)(const T field, scope void delegate(const(char)[]) sink) 6 | { 7 | import std.conv : to; 8 | import std.traits : EnumMembers; 9 | 10 | bool firstMember = true; 11 | 12 | sink(Enum.stringof); 13 | sink("("); 14 | 15 | static foreach (member; EnumMembers!Enum) 16 | { 17 | if (field & member) 18 | { 19 | if (firstMember) 20 | { 21 | firstMember = false; 22 | } 23 | else 24 | { 25 | sink(", "); 26 | } 27 | 28 | enum name = to!string(member); 29 | 30 | sink(name); 31 | } 32 | } 33 | sink(")"); 34 | } 35 | 36 | @("can format bitflags") 37 | unittest 38 | { 39 | import unit_threaded.should : shouldEqual; 40 | 41 | string generatedString; 42 | 43 | scope sink = (const(char)[] fragment) { 44 | generatedString ~= fragment; 45 | }; 46 | 47 | enum Enum 48 | { 49 | A = 1, 50 | B = 2, 51 | } 52 | 53 | const BitFlags!Enum flags = BitFlags!Enum(Enum.A, Enum.B); 54 | 55 | toString(flags, sink); 56 | 57 | generatedString.shouldEqual("Enum(A, B)"); 58 | } 59 | -------------------------------------------------------------------------------- /src/boilerplate/toString/systime.d: -------------------------------------------------------------------------------- 1 | module boilerplate.toString.systime; 2 | 3 | import std.datetime; 4 | 5 | void toString(SysTime time, scope void delegate(const(char)[]) sink) 6 | { 7 | sink(time.toISOExtString); 8 | } 9 | 10 | @("can format SysTime") 11 | unittest 12 | { 13 | import unit_threaded.should : shouldEqual; 14 | 15 | string generatedString; 16 | 17 | scope void delegate(const(char)[]) sink = (const(char)[] fragment) { 18 | generatedString ~= fragment; 19 | }; 20 | 21 | const SysTime time = SysTime.fromISOExtString("2003-02-01T11:55:00Z"); 22 | toString(time, sink); 23 | 24 | generatedString.shouldEqual("2003-02-01T11:55:00Z"); 25 | } 26 | -------------------------------------------------------------------------------- /src/boilerplate/util.d: -------------------------------------------------------------------------------- 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) nothrow pure @safe rankfn) nothrow pure @safe 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 | } 740 | -------------------------------------------------------------------------------- /unittest/config/string.d: -------------------------------------------------------------------------------- 1 | module config.string; 2 | 3 | public import boilerplate.toString.bitflags; 4 | public import boilerplate.toString.systime; 5 | --------------------------------------------------------------------------------