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