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 : Optional, formatNamed, 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 if (!std.algorithm.canFind(T.ConstructorInfo.fields, "value")) 175 { 176 public alias value = builderValue; 177 } 178 } 179 180 // value that is either a T, or a Builder for T. 181 // Used for nested builder initialization. 182 public struct BuilderProxy(T) 183 { 184 private enum Mode 185 { 186 unset, 187 builder, 188 value, 189 } 190 191 private union Data 192 { 193 T value; 194 Builder!T builder; 195 } 196 197 private Mode mode = Mode.unset; 198 199 private Data data; 200 201 public this(T value) 202 { 203 opAssign(value); 204 } 205 206 public void opAssign(T value) 207 in 208 { 209 assert( 210 this.mode != Mode.builder, 211 "Builder: cannot set sub-field by value since a subfield has already been set."); 212 } 213 do 214 { 215 Data newData = Data(value); 216 217 this.mode = Mode.value; 218 this.data = newData; 219 } 220 221 public bool _isUnset() const 222 { 223 return this.mode == Mode.unset; 224 } 225 226 public bool _isValue() const 227 { 228 return this.mode == Mode.value; 229 } 230 231 public bool _isBuilder() const 232 { 233 return this.mode == Mode.builder; 234 } 235 236 public inout(T) _value() inout 237 in 238 { 239 assert(this.mode == Mode.value); 240 } 241 do 242 { 243 return this.data.value; 244 } 245 246 public ref auto _builder() inout 247 in 248 { 249 assert(this.mode == Mode.builder); 250 } 251 do 252 { 253 return this.data.builder; 254 } 255 256 alias _implicitBuilder this; 257 258 public @property ref Builder!T _implicitBuilder() 259 in 260 { 261 assert( 262 this.mode != Mode.value, 263 "Builder: cannot set sub-field directly since field is already being initialized by value"); 264 } 265 do 266 { 267 if (this.mode == Mode.unset) 268 { 269 this.mode = Mode.builder; 270 this.data.builder = Builder!T.init; 271 } 272 273 return this.data.builder; 274 } 275 } 276 277 public Info _toInfo(Tuple!(string, string) pair) 278 { 279 return Info(pair[0], pair[1]); 280 }