1 /**
2     Functions and decorators that provide support for life cycle hooks.
3 
4     Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de>
5     License: Subject to the terms of the MIT license, as written in the
6              included LICENSE file.
7     Authors: Arne Ludwig <arne.ludwig@posteo.de>
8 */
9 module darg_plus.hooks;
10 
11 
12 /**
13     Decorate `Argument`s or `OptionFlag`s to declare validation procedures.
14     Can be used multiple times per entity.
15 
16     Params:
17         _validate = Performs required validations and throws if a validation
18                     fails.
19         isEnabled = This validation will have no effect if set to false.
20 */
21 struct Validate(alias _validate, bool isEnabled = true) {
22 private:
23 
24     static if (isEnabled)
25         alias validate = _validate;
26     else
27         alias validate = __noop;
28 
29     static void __noop(T)(T) { }
30 }
31 
32 
33 /**
34     Decorate methods of the options struct to declare a hook that executes
35     before all validations.
36 
37     Params:
38         priority = the `Priority` of the hook. Higher priorities get executed
39                    first.
40 */
41 struct PreValidate {
42     Priority priority;
43 }
44 
45 
46 /**
47     Decorate methods of the options struct to declare a hook that executes
48     after all validations.
49 
50     Params:
51         priority = the `Priority` of the hook. Higher priorities get executed
52                    first.
53 */
54 struct PostValidate {
55     Priority priority;
56 }
57 
58 
59 /**
60     Decorate methods of the options struct to declare a hook that executes
61     just before end of program execution.
62 
63     Params:
64         priority = the `Priority` of the hook. Higher priorities get executed
65                    first.
66 */
67 struct CleanUp {
68     Priority priority;
69 }
70 
71 
72 /**
73     Defines the priority of a hook.
74 
75     See_also:
76         PreValidate, PostValidate
77 */
78 enum Priority
79 {
80     low,
81     medium,
82     high,
83 }
84 
85 private template cmpPriority(T)
86 {
87     enum cmpPriority(alias a, alias b) = getUDA!(a, T).priority > getUDA!(b, T).priority;
88 }
89 
90 unittest
91 {
92     struct Tester
93     {
94         @PostValidate(Priority.low)
95         void priorityLow() { }
96 
97         @PostValidate(Priority.medium)
98         void priorityMedium() { }
99 
100         @PostValidate(Priority.high)
101         void priorityHigh() { }
102     }
103 
104     alias compare = cmpPriority!PostValidate;
105 
106     static assert(compare!(
107         Tester.priorityHigh,
108         Tester.priorityLow,
109     ));
110     static assert(!compare!(
111         Tester.priorityLow,
112         Tester.priorityHigh,
113     ));
114     static assert(!compare!(
115         Tester.priorityMedium,
116         Tester.priorityMedium,
117     ));
118 }
119 
120 
121 /// Call this method on the result of `parseArgs` to execute validations and
122 /// validation hooks.
123 Options processOptions(Options)(Options options)
124 {
125     import darg :
126         Argument,
127         Option;
128     import darg_plus.exception : CLIException;
129     import std.format : format;
130     import std.meta : staticSort;
131     import std..string : wrap;
132     import std.traits :
133         getSymbolsByUDA,
134         getUDAs;
135 
136     alias preValidateQueue = staticSort!(
137         cmpPriority!PreValidate,
138         getSymbolsByUDA!(Options, PreValidate),
139     );
140 
141     static foreach (alias symbol; preValidateQueue)
142     {
143         mixin("options." ~ __traits(identifier, symbol) ~ "();");
144     }
145 
146     static foreach (alias symbol; getSymbolsByUDA!(Options, Validate))
147     {{
148         alias validateUDAs = getUDAs!(symbol, Validate);
149 
150         foreach (validateUDA; validateUDAs)
151         {
152             alias validate = validateUDA.validate;
153             auto value = __traits(getMember, options, __traits(identifier, symbol));
154             alias Value = typeof(value);
155             alias Validator = typeof(validate);
156 
157             try
158             {
159                 static if (is(typeof(validate(value))))
160                     cast(void) validate(value);
161                 else static if (is(typeof(validate(value, options))))
162                     cast(void) validate(value, options);
163                 else
164                     static assert(0, format!q"{
165                         validator for %s.%s should have a signature of
166                         `void (T value);` or `void (T value, Options options);` -
167                         maybe the validator does not compile?
168                     }"(Options.stringof, symbol.stringof).wrap(size_t.max));
169             }
170             catch (Exception cause)
171             {
172                 enum isOption = getUDAs!(symbol, Option).length > 0;
173                 enum isArgument = getUDAs!(symbol, Argument).length > 0;
174 
175                 static if (isOption)
176                 {
177                     enum thing = "option";
178                     enum name = getUDAs!(symbol, Option)[0].toString();
179                 }
180                 else static if (isArgument)
181                 {
182                     enum thing = "argument";
183                     enum name = getUDAs!(symbol, Argument)[0].name;
184                 }
185                 else
186                 {
187                     enum thing = "property";
188                     enum name = __traits(identifier, symbol);
189                 }
190 
191                 throw new CLIException("invalid " ~ thing ~ " " ~ name ~ ": " ~ cause.msg, cause);
192             }
193         }
194     }}
195 
196     alias postValidateQueue = staticSort!(
197         cmpPriority!PostValidate,
198         getSymbolsByUDA!(Options, PostValidate),
199     );
200 
201     static foreach (alias symbol; postValidateQueue)
202     {
203         mixin("options." ~ __traits(identifier, symbol) ~ "();");
204     }
205 
206     return options;
207 }
208 
209 ///
210 unittest
211 {
212     import std.exception :
213         assertThrown,
214         enforce;
215 
216     struct Tester
217     {
218         @Validate!(value => enforce(value == 1))
219         int a = 1;
220 
221         @Validate!((value, options) => enforce(value == 2 * options.a))
222         int b = 2;
223 
224         string[] calls;
225 
226         @PostValidate(Priority.low)
227         void priorityLow() {
228             calls ~= "priorityLow";
229         }
230 
231         @PostValidate(Priority.medium)
232         void priorityMedium() {
233             calls ~= "priorityMedium";
234         }
235 
236         @PostValidate(Priority.high)
237         void priorityHigh() {
238             calls ~= "priorityHigh";
239         }
240     }
241 
242     Tester options;
243 
244     options = processOptions(options);
245 
246     assert(options.calls == [
247         "priorityHigh",
248         "priorityMedium",
249         "priorityLow",
250     ]);
251 
252     options.a = 2;
253 
254     assertThrown!Exception(processOptions(options));
255 }
256 
257 
258 /// Call this method when your program is about to stop execution to enable
259 /// execution of `CleanUp` hooks.
260 ///
261 /// Example:
262 /// ---
263 ///
264 /// void main(in string[] args)
265 /// {
266 ///     auto options = processOptions(parseArgs!Options(args[1 .. $]));
267 ///
268 ///     scope (exit) cast(void) cleanUp(options);
269 ///
270 ///     /// doing something productive ...
271 /// }
272 /// ---
273 Options cleanUp(Options)(Options options)
274 {
275     import std.meta : staticSort;
276     import std.traits : getSymbolsByUDA;
277 
278     alias cleanUpQueue = staticSort!(
279         cmpPriority!CleanUp,
280         getSymbolsByUDA!(Options, CleanUp),
281     );
282 
283     static foreach (alias symbol; cleanUpQueue)
284     {
285         mixin("options." ~ __traits(identifier, symbol) ~ "();");
286     }
287 
288     return options;
289 }
290 
291 unittest
292 {
293     import std.exception : assertThrown;
294 
295     struct Tester
296     {
297         string[] calls;
298 
299         @CleanUp(Priority.low)
300         void priorityLow() {
301             calls ~= "priorityLow";
302         }
303 
304         @CleanUp(Priority.medium)
305         void priorityMedium() {
306             calls ~= "priorityMedium";
307         }
308 
309         @CleanUp(Priority.high)
310         void priorityHigh() {
311             calls ~= "priorityHigh";
312         }
313     }
314 
315     Tester options;
316 
317     options = cleanUp(options);
318 
319     assert(options.calls == [
320         "priorityHigh",
321         "priorityMedium",
322         "priorityLow",
323     ]);
324 }
325 
326 import std.traits : getUDAs;
327 
328 private enum getUDA(alias symbol, T) = getUDAs!(symbol, T)[0];