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