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 : Optional, formatNamed, 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 if (!std.algorithm.canFind(T.ConstructorInfo.fields, "value"))
175     {
176         public alias value = builderValue;
177     }
178 }
179 
180 // value that is either a T, or a Builder for T.
181 // Used for nested builder initialization.
182 public struct BuilderProxy(T)
183 {
184     private enum Mode
185     {
186         unset,
187         builder,
188         value,
189     }
190 
191     private union Data
192     {
193         T value;
194         Builder!T builder;
195     }
196 
197     private Mode mode = Mode.unset;
198 
199     private Data data;
200 
201     public this(T value)
202     {
203         opAssign(value);
204     }
205 
206     public void opAssign(T value)
207     in
208     {
209         assert(
210             this.mode != Mode.builder,
211             "Builder: cannot set sub-field by value since a subfield has already been set.");
212     }
213     do
214     {
215         Data newData = Data(value);
216 
217         this.mode = Mode.value;
218         this.data = newData;
219     }
220 
221     public bool _isUnset() const
222     {
223         return this.mode == Mode.unset;
224     }
225 
226     public bool _isValue() const
227     {
228         return this.mode == Mode.value;
229     }
230 
231     public bool _isBuilder() const
232     {
233         return this.mode == Mode.builder;
234     }
235 
236     public inout(T) _value() inout
237     in
238     {
239         assert(this.mode == Mode.value);
240     }
241     do
242     {
243         return this.data.value;
244     }
245 
246     public ref auto _builder() inout
247     in
248     {
249         assert(this.mode == Mode.builder);
250     }
251     do
252     {
253         return this.data.builder;
254     }
255 
256     alias _implicitBuilder this;
257 
258     public @property ref Builder!T _implicitBuilder()
259     in
260     {
261         assert(
262             this.mode != Mode.value,
263             "Builder: cannot set sub-field directly since field is already being initialized by value");
264     }
265     do
266     {
267         if (this.mode == Mode.unset)
268         {
269             this.mode = Mode.builder;
270             this.data.builder = Builder!T.init;
271         }
272 
273         return this.data.builder;
274     }
275 }
276 
277 public Info _toInfo(Tuple!(string, string) pair)
278 {
279     return Info(pair[0], pair[1]);
280 }