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