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 ... that constructs it 48 // protects from such IDIOTIC DESIGN ERRORS as `alias Nullable!T.get this` 49 // NOTE: can't use hasMember because https://issues.dlang.org/show_bug.cgi?id=13269 50 static if (__traits(compiles, BaseType.Builder())) 51 { 52 alias BuilderResultType = typeof(BaseType.Builder().builderValue); 53 54 static if (is(BuilderResultType: BaseType)) 55 { 56 alias Type = BuilderProxy!FieldType; 57 enum isBuildable = true; 58 } 59 else 60 { 61 alias Type = Optional!FieldType; 62 enum isBuildable = false; 63 } 64 } 65 else static if (is(FieldType == E[], E)) 66 { 67 static if (__traits(compiles, E.Builder())) 68 { 69 alias Type = BuilderProxy!FieldType; 70 enum isBuildable = true; 71 } 72 else 73 { 74 alias Type = Optional!FieldType; 75 enum isBuildable = false; 76 } 77 } 78 else 79 { 80 alias Type = Optional!FieldType; 81 enum isBuildable = false; 82 } 83 } 84 85 static foreach (typeField, builderField; fieldInfoList) 86 { 87 mixin(`public BuilderFieldInfo!typeField.Type ` ~ builderField ~ `;`); 88 } 89 90 public bool isValid() const 91 { 92 return this.getError().isNull; 93 } 94 95 public std.typecons.Nullable!string getError() const 96 { 97 alias Nullable = std.typecons.Nullable; 98 99 static foreach (typeField, builderField; fieldInfoList) 100 { 101 static if (BuilderFieldInfo!(typeField).isBuildable) 102 { 103 // if the proxy has never been used as a builder, 104 // ie. either a value was assigned or it was untouched 105 // then a default value may be used instead. 106 if (__traits(getMember, this, builderField)._isUnset) 107 { 108 static if (!__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 109 { 110 return Nullable!string( 111 "required field '" ~ builderField ~ "' not set in builder of " ~ T.stringof); 112 } 113 } 114 else if (__traits(getMember, this, builderField)._isBuilder) 115 { 116 auto subError = __traits(getMember, this, builderField)._builder.getError; 117 118 if (!subError.isNull) 119 { 120 return Nullable!string(subError.get ~ " of " ~ T.stringof); 121 } 122 } 123 // else it carries a full value. 124 } 125 else 126 { 127 static if (!__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 128 { 129 if (__traits(getMember, this, builderField).isNull) 130 { 131 return Nullable!string( 132 "required field '" ~ builderField ~ "' not set in builder of " ~ T.stringof); 133 } 134 } 135 } 136 } 137 return Nullable!string(); 138 } 139 140 public @property T builderValue(size_t line = __LINE__, string file = __FILE__) 141 in 142 { 143 import core.exception : AssertError; 144 145 if (!this.isValid) 146 { 147 throw new AssertError(this.getError.get, file, line); 148 } 149 } 150 do 151 { 152 auto getArg(string typeField, string builderField)() 153 { 154 static if (BuilderFieldInfo!(typeField).isBuildable) 155 { 156 import std.meta : Alias; 157 158 alias Type = Alias!(__traits(getMember, T.ConstructorInfo.FieldInfo, typeField)).Type; 159 160 static if (is(Type == E[], E)) 161 { 162 if (__traits(getMember, this, builderField)._isArray) 163 { 164 return __traits(getMember, this, builderField)._arrayValue; 165 } 166 else if (__traits(getMember, this, builderField)._isValue) 167 { 168 return __traits(getMember, this, builderField)._value; 169 } 170 } 171 else 172 { 173 if (__traits(getMember, this, builderField)._isBuilder) 174 { 175 return __traits(getMember, this, builderField)._builderValue; 176 } 177 else if (__traits(getMember, this, builderField)._isValue) 178 { 179 return __traits(getMember, this, builderField)._value; 180 } 181 } 182 assert(__traits(getMember, this, builderField)._isUnset); 183 184 static if (__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 185 { 186 return __traits(getMember, T.ConstructorInfo.FieldInfo, typeField).fieldDefault; 187 } 188 else 189 { 190 assert(false, "isValid/build do not match 1"); 191 } 192 } 193 else 194 { 195 if (!__traits(getMember, this, builderField).isNull) 196 { 197 return __traits(getMember, this, builderField)._get; 198 } 199 else 200 { 201 static if (__traits(getMember, T.ConstructorInfo.FieldInfo, typeField).useDefault) 202 { 203 return __traits(getMember, T.ConstructorInfo.FieldInfo, typeField).fieldDefault; 204 } 205 else 206 { 207 assert(false, "isValid/build do not match 2"); 208 } 209 } 210 } 211 } 212 213 enum getArgArray = std.range.array( 214 std.algorithm.map!(i => std.format.format!`getArg!(fieldInfoList[%s][0], fieldInfoList[%s][1])`(i, i))( 215 std.range.iota(fieldInfoList.length))); 216 217 static if (is(T == class)) 218 { 219 return mixin(std.format.format!q{new T(%-(%s, %))}(getArgArray)); 220 } 221 else 222 { 223 return mixin(std.format.format!q{T(%-(%s, %))}(getArgArray)); 224 } 225 } 226 227 static foreach (aliasMember; __traits(getAliasThis, T)) 228 { 229 mixin(`alias ` ~ optionallyRemoveTrailingUnderline!aliasMember ~ ` this;`); 230 } 231 232 static if (!std.algorithm.canFind( 233 std.algorithm.map!removeTrailingUnderline(T.ConstructorInfo.fields), 234 "value")) 235 { 236 public alias value = builderValue; 237 } 238 } 239 240 // value that is either a T, or a Builder for T. 241 // Used for nested builder initialization. 242 public struct BuilderProxy(T) 243 { 244 private enum Mode 245 { 246 unset, 247 builder, 248 value, 249 array, // array of builders 250 } 251 252 static if (is(T : Nullable!Arg, Arg)) 253 { 254 enum isNullable = true; 255 alias InnerType = Arg; 256 } 257 else 258 { 259 enum isNullable = false; 260 alias InnerType = T; 261 } 262 263 private union Data 264 { 265 T value; 266 267 this(inout(T) value) inout pure 268 { 269 this.value = value; 270 } 271 272 static if (is(T == E[], E)) 273 { 274 E.BuilderType!()[] array; 275 276 this(inout(E.BuilderType!())[] array) inout pure 277 { 278 this.array = array; 279 } 280 } 281 else 282 { 283 InnerType.BuilderType!() builder; 284 285 this(inout(InnerType.BuilderType!()) builder) inout pure 286 { 287 this.builder = builder; 288 } 289 } 290 } 291 292 struct DataWrapper 293 { 294 Data data; 295 } 296 297 private Mode mode = Mode.unset; 298 299 private DataWrapper wrapper = DataWrapper.init; 300 301 public this(T value) 302 { 303 opAssign(value); 304 } 305 306 public void opAssign(T value) 307 in(this.mode != Mode.builder, 308 "Builder: cannot set field by value since a subfield has already been set.") 309 { 310 import boilerplate.util : move, moveEmplace; 311 312 static if (isNullable) 313 { 314 DataWrapper newWrapper = DataWrapper(Data(value)); 315 } 316 else 317 { 318 DataWrapper newWrapper = DataWrapper(Data(value)); 319 } 320 321 if (this.mode == Mode.value) 322 { 323 move(newWrapper, this.wrapper); 324 } 325 else 326 { 327 moveEmplace(newWrapper, this.wrapper); 328 } 329 this.mode = Mode.value; 330 } 331 332 static if (isNullable) 333 { 334 public void opAssign(InnerType value) 335 { 336 return opAssign(T(value)); 337 } 338 } 339 340 public bool _isUnset() const 341 { 342 return this.mode == Mode.unset; 343 } 344 345 public bool _isValue() const 346 { 347 return this.mode == Mode.value; 348 } 349 350 public bool _isBuilder() const 351 { 352 return this.mode == Mode.builder; 353 } 354 355 public bool _isArray() const 356 { 357 return this.mode == Mode.array; 358 } 359 360 public inout(T) _value() inout 361 in (this.mode == Mode.value) 362 { 363 return this.wrapper.data.value; 364 } 365 366 public ref auto _builder() inout 367 in (this.mode == Mode.builder) 368 { 369 static if (is(T == E[], E)) 370 { 371 int i = 0; 372 373 assert(i != 0); // assert(false) but return stays "reachable" 374 return E.Builder(); 375 } 376 else 377 { 378 return this.wrapper.data.builder; 379 } 380 } 381 382 public auto _builderValue() 383 in (this.mode == Mode.builder) 384 { 385 static if (is(T == E[], E)) 386 { 387 int i = 0; 388 389 assert(i != 0); // assert(false) but return stays "reachable" 390 return E.Builder(); 391 } 392 else static if (isNullable) 393 { 394 return T(this.wrapper.data.builder.builderValue); 395 } 396 else 397 { 398 return this.wrapper.data.builder.builderValue; 399 } 400 } 401 402 public T _arrayValue() 403 in (this.mode == Mode.array) 404 { 405 import std.algorithm : map; 406 import std.array : array; 407 408 static if (is(T == E[], E)) 409 { 410 // enforce that E is the return value 411 static E builderValue(Element)(Element element) { return element.builderValue; } 412 413 return this.wrapper.data.array.map!builderValue.array; 414 } 415 else 416 { 417 assert(false); 418 } 419 } 420 421 static if (is(T == E[], E)) 422 { 423 public ref E.BuilderType!() opIndex(size_t index) return 424 in (this.mode == Mode.unset || this.mode == Mode.array, 425 "cannot build array for already initialized field") 426 { 427 import boilerplate.util : moveEmplace; 428 429 if (this.mode == Mode.unset) 430 { 431 auto newWrapper = DataWrapper(Data(new E.BuilderType!()[](index + 1))); 432 433 this.mode = Mode.array; 434 moveEmplace(newWrapper, this.wrapper); 435 } 436 else while (this.wrapper.data.array.length <= index) 437 { 438 this.wrapper.data.array ~= E.Builder(); 439 } 440 return this.wrapper.data.array[index]; 441 } 442 443 public void opOpAssign(string op, R)(R rhs) 444 if (op == "~") 445 in (this.mode == Mode.unset || this.mode == Mode.value, 446 "Builder cannot append to array already initialized by index") 447 { 448 if (this.mode == Mode.unset) 449 { 450 opAssign(null); 451 } 452 opAssign(this.wrapper.data.value ~ rhs); 453 } 454 } 455 else 456 { 457 public @property ref InnerType.BuilderType!() _implicitBuilder() 458 { 459 import boilerplate.util : move, moveEmplace; 460 461 if (this.mode == Mode.unset) 462 { 463 auto newWrapper = DataWrapper(Data(InnerType.BuilderType!().init)); 464 465 this.mode = Mode.builder; 466 moveEmplace(newWrapper, this.wrapper); 467 } 468 else if (this.mode == Mode.value) 469 { 470 static if (isNullable) 471 { 472 assert( 473 !this.wrapper.data.value.isNull, 474 "Builder: cannot set sub-field directly since field was explicitly " ~ 475 "initialized to Nullable.null"); 476 auto value = this.wrapper.data.value.get; 477 } 478 else 479 { 480 auto value = this.wrapper.data.value; 481 } 482 static if (__traits(compiles, value.BuilderFrom())) 483 { 484 auto newWrapper = DataWrapper(Data(value.BuilderFrom())); 485 486 this.mode = Mode.builder; 487 move(newWrapper, this.wrapper); 488 } 489 else 490 { 491 assert( 492 false, 493 "Builder: cannot set sub-field directly since field is already being initialized by value " ~ 494 "(and BuilderFrom is unavailable in " ~ typeof(this.wrapper.data.value).stringof ~ ")"); 495 } 496 } 497 498 return this.wrapper.data.builder; 499 } 500 501 alias _implicitBuilder this; 502 } 503 } 504 505 public Info _toInfo(Tuple!(string, string) pair) 506 { 507 return Info(pair[0], pair[1]); 508 }