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 && T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 66 { 67 } 68 else 69 { 70 if (this.%(builderField)._isBuilder) 71 { 72 auto subError = this.%(builderField)._builder.getError; 73 74 if (!subError.isNull) 75 { 76 return Nullable!string(subError.get ~ " of " ~ T.stringof); 77 } 78 } 79 } 80 } 81 else 82 { 83 static if (!T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 84 { 85 if (this.%(builderField).isNull) 86 { 87 return Nullable!string( 88 "required field '%(builderField)' not set in builder of " ~ T.stringof); 89 } 90 } 91 } 92 }.values(info)); 93 } 94 return Nullable!string(); 95 } 96 97 public @property T builderValue(size_t line = __LINE__, string file = __FILE__) 98 in 99 { 100 import core.exception : AssertError; 101 102 if (!this.isValid) 103 { 104 throw new AssertError(this.getError.get, file, line); 105 } 106 } 107 do 108 { 109 auto getArg(Info info)() 110 { 111 mixin(formatNamed!q{ 112 static if (BuilderFieldInfo!(info.typeField).isBuildable) 113 { 114 if (this.%(builderField)._isBuilder) 115 { 116 return this.%(builderField)._builder.builderValue; 117 } 118 else if (this.%(builderField)._isValue) 119 { 120 return this.%(builderField)._value; 121 } 122 else 123 { 124 assert(this.%(builderField)._isUnset); 125 126 static if (T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 127 { 128 return T.ConstructorInfo.FieldInfo.%(typeField).fieldDefault; 129 } 130 else 131 { 132 assert(false, "isValid/build do not match 1"); 133 } 134 } 135 } 136 else 137 { 138 if (!this.%(builderField).isNull) 139 { 140 return this.%(builderField)._get; 141 } 142 else 143 { 144 static if (T.ConstructorInfo.FieldInfo.%(typeField).useDefault) 145 { 146 return T.ConstructorInfo.FieldInfo.%(typeField).fieldDefault; 147 } 148 else 149 { 150 assert(false, "isValid/build do not match 2"); 151 } 152 } 153 } 154 }.values(info)); 155 } 156 157 enum getArgArray = std.range.array( 158 std.algorithm.map!(i => std.format.format!`getArg!(fieldInfoList[%s])`(i))( 159 std.range.iota(fieldInfoList.length))); 160 161 static if (is(T == class)) 162 { 163 return mixin(std.format.format!q{new T(%-(%s, %))}(getArgArray)); 164 } 165 else 166 { 167 return mixin(std.format.format!q{T(%-(%s, %))}(getArgArray)); 168 } 169 } 170 171 static if (!std.algorithm.canFind(T.ConstructorInfo.fields, "value")) 172 { 173 public alias value = builderValue; 174 } 175 } 176 177 // value that is either a T, or a Builder for T. 178 // Used for nested builder initialization. 179 public struct BuilderProxy(T) 180 { 181 private enum Mode 182 { 183 unset, 184 builder, 185 value, 186 } 187 188 private union Data 189 { 190 T value; 191 Builder!T builder; 192 } 193 194 private Mode mode = Mode.unset; 195 196 private Data data; 197 198 public void opAssign(T value) 199 in 200 { 201 assert( 202 this.mode != Mode.builder, 203 "Builder: cannot set sub-field by value since a subfield has already been set."); 204 } 205 do 206 { 207 Data newData = Data(value); 208 209 this.mode = Mode.value; 210 this.data = newData; 211 } 212 213 public bool _isUnset() const 214 { 215 return this.mode == Mode.unset; 216 } 217 218 public bool _isValue() const 219 { 220 return this.mode == Mode.value; 221 } 222 223 public bool _isBuilder() const 224 { 225 return this.mode == Mode.builder; 226 } 227 228 public inout(T) _value() inout 229 in 230 { 231 assert(this.mode == Mode.value); 232 } 233 do 234 { 235 return this.data.value; 236 } 237 238 public ref auto _builder() inout 239 in 240 { 241 assert(this.mode == Mode.builder); 242 } 243 do 244 { 245 return this.data.builder; 246 } 247 248 alias _implicitBuilder this; 249 250 public @property ref Builder!T _implicitBuilder() 251 in 252 { 253 assert( 254 this.mode != Mode.value, 255 "Builder: cannot set sub-field directly since field is already being initialized by value"); 256 } 257 do 258 { 259 if (this.mode == Mode.unset) 260 { 261 this.mode = Mode.builder; 262 this.data.builder = Builder!T.init; 263 } 264 265 return this.data.builder; 266 } 267 } 268 269 public Info _toInfo(Tuple!(string, string) pair) 270 { 271 return Info(pair[0], pair[1]); 272 }