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