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