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 : formatNamed, Optional, removeTrailingUnderline; 12 static import std.algorithm; 13 static import std.format; 14 static import std.range; 15 static import std.typecons; 16 17 static assert(__traits(hasMember, T, "ConstructorInfo")); 18 19 private enum fieldInfoList = std.range.array( 20 std.algorithm.map!_toInfo( 21 std.range.zip(T.ConstructorInfo.fields, 22 std.algorithm.map!removeTrailingUnderline(T.ConstructorInfo.fields)))); 23 24 private template BuilderFieldInfo(string member) 25 { 26 mixin(std.format.format!q{alias FieldType = T.ConstructorInfo.FieldInfo.%s.Type;}(member)); 27 28 static if (is(FieldType : Nullable!Arg, Arg)) 29 { 30 alias BaseType = Arg; 31 } 32 else 33 { 34 alias BaseType = FieldType; 35 } 36 37 // type has a builder ... that constructs it 38 // protects from such IDIOTIC DESIGN ERRORS as `alias Nullable!T.get this` 39 static if (__traits(hasMember, BaseType, "Builder") 40 && is(typeof(BaseType.Builder().builderValue): BaseType)) 41 { 42 alias Type = BuilderProxy!FieldType; 43 enum isBuildable = true; 44 } 45 else 46 { 47 alias Type = Optional!FieldType; 48 enum isBuildable = false; 49 } 50 } 51 52 static foreach (info; fieldInfoList) 53 { 54 mixin(formatNamed!q{public BuilderFieldInfo!(info.typeField).Type %(builderField);}.values(info)); 55 } 56 57 public bool isValid() const 58 { 59 return this.getError().isNull; 60 } 61 62 public std.typecons.Nullable!string getError() const 63 { 64 alias Nullable = std.typecons.Nullable; 65 66 static foreach (info; fieldInfoList) 67 { 68 mixin(formatNamed!q{ 69 static if (BuilderFieldInfo!(info.typeField).isBuildable) 70 { 71 // if the proxy has never been used as a builder, 72 // ie. either a value was assigned or it was untouched 73 // then a default value may be used instead. 74 if (this.%(builderField)._isUnset) 75 { 76 static if (!T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 77 { 78 return Nullable!string( 79 "required field '%(builderField)' not set in builder of " ~ T.stringof); 80 } 81 } 82 else if (this.%(builderField)._isBuilder) 83 { 84 auto subError = this.%(builderField)._builder.getError; 85 86 if (!subError.isNull) 87 { 88 return Nullable!string(subError.get ~ " of " ~ T.stringof); 89 } 90 } 91 // else it carries a full value. 92 } 93 else 94 { 95 static if (!T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 96 { 97 if (this.%(builderField).isNull) 98 { 99 return Nullable!string( 100 "required field '%(builderField)' not set in builder of " ~ T.stringof); 101 } 102 } 103 } 104 }.values(info)); 105 } 106 return Nullable!string(); 107 } 108 109 public @property T builderValue(size_t line = __LINE__, string file = __FILE__) 110 in 111 { 112 import core.exception : AssertError; 113 114 if (!this.isValid) 115 { 116 throw new AssertError(this.getError.get, file, line); 117 } 118 } 119 do 120 { 121 auto getArg(Info info)() 122 { 123 mixin(formatNamed!q{ 124 static if (BuilderFieldInfo!(info.typeField).isBuildable) 125 { 126 if (this.%(builderField)._isBuilder) 127 { 128 return this.%(builderField)._builderValue; 129 } 130 else if (this.%(builderField)._isValue) 131 { 132 return this.%(builderField)._value; 133 } 134 else 135 { 136 assert(this.%(builderField)._isUnset); 137 138 static if (T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 139 { 140 return T.ConstructorInfo.FieldInfo.%(typeField).fieldDefault; 141 } 142 else 143 { 144 assert(false, "isValid/build do not match 1"); 145 } 146 } 147 } 148 else 149 { 150 if (!this.%(builderField).isNull) 151 { 152 return this.%(builderField)._get; 153 } 154 else 155 { 156 static if (T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 157 { 158 return T.ConstructorInfo.FieldInfo.%(typeField).fieldDefault; 159 } 160 else 161 { 162 assert(false, "isValid/build do not match 2"); 163 } 164 } 165 } 166 }.values(info)); 167 } 168 169 enum getArgArray = std.range.array( 170 std.algorithm.map!(i => std.format.format!`getArg!(fieldInfoList[%s])`(i))( 171 std.range.iota(fieldInfoList.length))); 172 173 static if (is(T == class)) 174 { 175 return mixin(std.format.format!q{new T(%-(%s, %))}(getArgArray)); 176 } 177 else 178 { 179 return mixin(std.format.format!q{T(%-(%s, %))}(getArgArray)); 180 } 181 } 182 183 static foreach (aliasMember; __traits(getAliasThis, T)) 184 { 185 mixin(`alias ` ~ aliasMember ~ ` this;`); 186 } 187 188 static if (!std.algorithm.canFind( 189 std.algorithm.map!removeTrailingUnderline(T.ConstructorInfo.fields), 190 "value")) 191 { 192 public alias value = builderValue; 193 } 194 } 195 196 // value that is either a T, or a Builder for T. 197 // Used for nested builder initialization. 198 public struct BuilderProxy(T) 199 { 200 private enum Mode 201 { 202 unset, 203 builder, 204 value, 205 } 206 207 static if (is(T : Nullable!Arg, Arg)) 208 { 209 enum isNullable = true; 210 alias InnerType = Arg; 211 } 212 else 213 { 214 enum isNullable = false; 215 alias InnerType = T; 216 } 217 218 private union Data 219 { 220 InnerType value; 221 222 Builder!InnerType builder; 223 224 this(inout(InnerType) value) inout pure 225 { 226 this.value = value; 227 } 228 229 this(inout(Builder!InnerType) builder) inout pure 230 { 231 this.builder = builder; 232 } 233 } 234 235 struct DataWrapper 236 { 237 Data data; 238 } 239 240 private Mode mode = Mode.unset; 241 242 private DataWrapper wrapper = DataWrapper.init; 243 244 public this(T value) 245 { 246 opAssign(value); 247 } 248 249 public void opAssign(T value) 250 in 251 { 252 assert( 253 this.mode != Mode.builder, 254 "Builder: cannot set field by value since a subfield has already been set."); 255 } 256 do 257 { 258 import boilerplate.util : move, moveEmplace; 259 260 static if (isNullable) 261 { 262 if (value.isNull) 263 { 264 this.mode = Mode.unset; 265 return; 266 } 267 268 DataWrapper newWrapper = DataWrapper(Data(value.get)); 269 } 270 else 271 { 272 DataWrapper newWrapper = DataWrapper(Data(value)); 273 } 274 275 if (this.mode == Mode.value) 276 { 277 move(newWrapper, this.wrapper); 278 } 279 else 280 { 281 moveEmplace(newWrapper, this.wrapper); 282 } 283 this.mode = Mode.value; 284 } 285 286 static if (isNullable) 287 { 288 public void opAssign(InnerType value) 289 { 290 return opAssign(T(value)); 291 } 292 } 293 294 public bool _isUnset() const 295 { 296 return this.mode == Mode.unset; 297 } 298 299 public bool _isValue() const 300 { 301 return this.mode == Mode.value; 302 } 303 304 public bool _isBuilder() const 305 { 306 return this.mode == Mode.builder; 307 } 308 309 public inout(T) _value() inout 310 in 311 { 312 assert(this.mode == Mode.value); 313 } 314 do 315 { 316 static if (isNullable) 317 { 318 return inout(T)(this.wrapper.data.value); 319 } 320 else 321 { 322 return this.wrapper.data.value; 323 } 324 } 325 326 public ref auto _builder() inout 327 in 328 { 329 assert(this.mode == Mode.builder); 330 } 331 do 332 { 333 return this.wrapper.data.builder; 334 } 335 336 public auto _builderValue() 337 in 338 { 339 assert(this.mode == Mode.builder); 340 } 341 do 342 { 343 static if (isNullable) 344 { 345 return T(this.wrapper.data.builder.builderValue); 346 } 347 else 348 { 349 return this.wrapper.data.builder.builderValue; 350 } 351 } 352 353 alias _implicitBuilder this; 354 355 public @property ref Builder!InnerType _implicitBuilder() 356 { 357 import boilerplate.util : move, moveEmplace; 358 359 if (this.mode == Mode.unset) 360 { 361 auto newWrapper = DataWrapper(Data(Builder!InnerType.init)); 362 363 this.mode = Mode.builder; 364 moveEmplace(newWrapper, this.wrapper); 365 } 366 else if (this.mode == Mode.value) 367 { 368 static if (__traits(compiles, value.BuilderFrom())) 369 { 370 auto value = this.wrapper.data.value; 371 auto newWrapper = DataWrapper(Data(value.BuilderFrom())); 372 373 this.mode = Mode.builder; 374 move(newWrapper, this.wrapper); 375 } 376 else 377 { 378 assert( 379 false, 380 "Builder: cannot set sub-field directly since field is already being initialized by value " ~ 381 "(and BuilderFrom is unavailable in " ~ typeof(this.wrapper.data.value).stringof ~ ")"); 382 } 383 } 384 385 return this.wrapper.data.builder; 386 } 387 } 388 389 public Info _toInfo(Tuple!(string, string) pair) 390 { 391 return Info(pair[0], pair[1]); 392 }