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 && T.ConstructorInfo.FieldInfo.%(typeField).useDefault)
66                     {
67                     }
68                     else
69                     {
70                         if (this.%(builderField)._isBuilder)
71                         {
72                             auto subError = this.%(builderField)._builder.getError;
73 
74                             if (!subError.isNull)
75                             {
76                                 return Nullable!string(subError.get ~ " of " ~ T.stringof);
77                             }
78                         }
79                     }
80                 }
81                 else
82                 {
83                     static if (!T.ConstructorInfo.FieldInfo.%(typeField).useDefault)
84                     {
85                         if (this.%(builderField).isNull)
86                         {
87                             return Nullable!string(
88                                 "required field '%(builderField)' not set in builder of " ~ T.stringof);
89                         }
90                     }
91                 }
92             }.values(info));
93         }
94         return Nullable!string();
95     }
96 
97     public @property T builderValue(size_t line = __LINE__, string file = __FILE__)
98     in
99     {
100         import core.exception : AssertError;
101 
102         if (!this.isValid)
103         {
104             throw new AssertError(this.getError.get, file, line);
105         }
106     }
107     do
108     {
109         auto getArg(Info info)()
110         {
111             mixin(formatNamed!q{
112                 static if (BuilderFieldInfo!(info.typeField).isBuildable)
113                 {
114                     if (this.%(builderField)._isBuilder)
115                     {
116                         return this.%(builderField)._builder.builderValue;
117                     }
118                     else if (this.%(builderField)._isValue)
119                     {
120                         return this.%(builderField)._value;
121                     }
122                     else
123                     {
124                         assert(this.%(builderField)._isUnset);
125 
126                         static if (T.ConstructorInfo.FieldInfo.%(typeField).useDefault)
127                         {
128                             return T.ConstructorInfo.FieldInfo.%(typeField).fieldDefault;
129                         }
130                         else
131                         {
132                             assert(false, "isValid/build do not match 1");
133                         }
134                     }
135                 }
136                 else
137                 {
138                     if (!this.%(builderField).isNull)
139                     {
140                         return this.%(builderField)._get;
141                     }
142                     else
143                     {
144                         static if (T.ConstructorInfo.FieldInfo.%(typeField).useDefault)
145                         {
146                             return T.ConstructorInfo.FieldInfo.%(typeField).fieldDefault;
147                         }
148                         else
149                         {
150                             assert(false, "isValid/build do not match 2");
151                         }
152                     }
153                 }
154             }.values(info));
155         }
156 
157         enum getArgArray = std.range.array(
158             std.algorithm.map!(i => std.format.format!`getArg!(fieldInfoList[%s])`(i))(
159                 std.range.iota(fieldInfoList.length)));
160 
161         static if (is(T == class))
162         {
163             return mixin(std.format.format!q{new T(%-(%s, %))}(getArgArray));
164         }
165         else
166         {
167             return mixin(std.format.format!q{T(%-(%s, %))}(getArgArray));
168         }
169     }
170 
171     static if (!std.algorithm.canFind(T.ConstructorInfo.fields, "value"))
172     {
173         public alias value = builderValue;
174     }
175 }
176 
177 // value that is either a T, or a Builder for T.
178 // Used for nested builder initialization.
179 public struct BuilderProxy(T)
180 {
181     private enum Mode
182     {
183         unset,
184         builder,
185         value,
186     }
187 
188     private union Data
189     {
190         T value;
191         Builder!T builder;
192     }
193 
194     private Mode mode = Mode.unset;
195 
196     private Data data;
197 
198     public void opAssign(T value)
199     in
200     {
201         assert(
202             this.mode != Mode.builder,
203             "Builder: cannot set sub-field by value since a subfield has already been set.");
204     }
205     do
206     {
207         Data newData = Data(value);
208 
209         this.mode = Mode.value;
210         this.data = newData;
211     }
212 
213     public bool _isUnset() const
214     {
215         return this.mode == Mode.unset;
216     }
217 
218     public bool _isValue() const
219     {
220         return this.mode == Mode.value;
221     }
222 
223     public bool _isBuilder() const
224     {
225         return this.mode == Mode.builder;
226     }
227 
228     public inout(T) _value() inout
229     in
230     {
231         assert(this.mode == Mode.value);
232     }
233     do
234     {
235         return this.data.value;
236     }
237 
238     public ref auto _builder() inout
239     in
240     {
241         assert(this.mode == Mode.builder);
242     }
243     do
244     {
245         return this.data.builder;
246     }
247 
248     alias _implicitBuilder this;
249 
250     public @property ref Builder!T _implicitBuilder()
251     in
252     {
253         assert(
254             this.mode != Mode.value,
255             "Builder: cannot set sub-field directly since field is already being initialized by value");
256     }
257     do
258     {
259         if (this.mode == Mode.unset)
260         {
261             this.mode = Mode.builder;
262             this.data.builder = Builder!T.init;
263         }
264 
265         return this.data.builder;
266     }
267 }
268 
269 public Info _toInfo(Tuple!(string, string) pair)
270 {
271     return Info(pair[0], pair[1]);
272 }