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