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