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 struct PreValidate {
38     /// Priority of execution. Higher priorities get executed first.
39     Priority priority;
40 }
41 
42 
43 /**
44     Decorate methods of the options struct to declare a hook that executes
45     after all validations.
46 */
47 struct PostValidate {
48     /// Priority of execution. Higher priorities get executed first.
49     Priority priority;
50 }
51 
52 
53 /**
54     Decorate methods of the options struct to declare a hook that executes
55     just before end of program execution.
56 */
57 struct CleanUp {
58     /// Priority of execution. Higher priorities get executed first.
59     Priority priority;
60 }
61 
62 
63 /**
64     Defines the priority of execution of a hook. Higher priorities get
65     executed first.
66 
67     See_also:
68         PreValidate, PostValidate
69 */
70 struct Priority
71 {
72     /// Pre-defined priorities provide good readbility and suffice in most
73     /// cases.
74     enum min = Priority(int.min);
75     /// ditto
76     enum low = Priority(-100);
77     /// ditto
78     enum medium = Priority(0);
79     /// ditto
80     enum high = Priority(100);
81     /// ditto
82     enum max = Priority(int.max);
83 
84     int priority;
85     alias priority this;
86 
87 
88     ///
89     this(int priority) pure nothrow @safe @nogc
90     {
91         this.priority = priority;
92     }
93 
94 
95     /// Operator overloads give fine-grained control over priorities.
96     Priority opBinary(string op)(int offset) const pure nothrow @safe @nogc
97     {
98         return mixin("Priority(priority "~op~" offset)");
99     }
100 
101 
102     /// ditto
103     Priority opBinaryRight(string op)(int offset) const pure nothrow @safe @nogc
104     {
105         return mixin("Priority(offset "~op~" priority)");
106     }
107 }
108 
109 /// Operator overloads give fine-grained control over priorities.
110 unittest
111 {
112     struct Options
113     {
114         @PreValidate(Priority.max)
115         void initialPreparationStep1() { }
116 
117         @PreValidate(Priority.max - 1)
118         void initialPreparationStep2() { }
119 
120         @PreValidate(Priority.medium)
121         void hookSetDefaultValue() { }
122 
123         @PostValidate(Priority.medium)
124         void hookCreateTmpdir() { }
125     }
126 }
127 
128 private template cmpPriority(T)
129 {
130     enum cmpPriority(alias a, alias b) = getUDA!(a, T).priority > getUDA!(b, T).priority;
131 }
132 
133 unittest
134 {
135     struct Tester
136     {
137         @PostValidate(Priority.low)
138         void priorityLow() { }
139 
140         @PostValidate(Priority.medium)
141         void priorityMedium() { }
142 
143         @PostValidate(Priority.high)
144         void priorityHigh() { }
145     }
146 
147     alias compare = cmpPriority!PostValidate;
148 
149     static assert(compare!(
150         Tester.priorityHigh,
151         Tester.priorityLow,
152     ));
153     static assert(!compare!(
154         Tester.priorityLow,
155         Tester.priorityHigh,
156     ));
157     static assert(!compare!(
158         Tester.priorityMedium,
159         Tester.priorityMedium,
160     ));
161 }
162 
163 
164 /// Call this method on the result of `parseArgs` to execute validations and
165 /// validation hooks.
166 Options processOptions(Options)(Options options)
167 {
168     import darg :
169         Argument,
170         Option;
171     import darg_plus.exception : CLIException;
172     import std.format : format;
173     import std.meta : staticSort;
174     import std..string : wrap;
175     import std.traits :
176         getSymbolsByUDA,
177         getUDAs;
178 
179     alias preValidateQueue = staticSort!(
180         cmpPriority!PreValidate,
181         getSymbolsByUDA!(Options, PreValidate),
182     );
183 
184     static foreach (alias symbol; preValidateQueue)
185     {
186         mixin("options." ~ __traits(identifier, symbol) ~ "();");
187     }
188 
189     static foreach (alias symbol; getSymbolsByUDA!(Options, Validate))
190     {{
191         alias validateUDAs = getUDAs!(symbol, Validate);
192 
193         foreach (validateUDA; validateUDAs)
194         {
195             alias validate = validateUDA.validate;
196             auto value = __traits(getMember, options, __traits(identifier, symbol));
197             alias Value = typeof(value);
198             alias Validator = typeof(validate);
199 
200             try
201             {
202                 static if (is(typeof(validate(value, options))))
203                     cast(void) validate(value, options);
204                 else
205                     cast(void) validate(value);
206             }
207             catch (Exception cause)
208             {
209                 enum isOption = getUDAs!(symbol, Option).length > 0;
210                 enum isArgument = getUDAs!(symbol, Argument).length > 0;
211 
212                 static if (isOption)
213                 {
214                     enum thing = "option";
215                     enum name = getUDAs!(symbol, Option)[0].toString();
216                 }
217                 else static if (isArgument)
218                 {
219                     enum thing = "argument";
220                     enum name = getUDAs!(symbol, Argument)[0].name;
221                 }
222                 else
223                 {
224                     enum thing = "property";
225                     enum name = __traits(identifier, symbol);
226                 }
227 
228                 throw new CLIException("invalid " ~ thing ~ " " ~ name ~ ": " ~ cause.msg, cause);
229             }
230         }
231     }}
232 
233     alias postValidateQueue = staticSort!(
234         cmpPriority!PostValidate,
235         getSymbolsByUDA!(Options, PostValidate),
236     );
237 
238     static foreach (alias symbol; postValidateQueue)
239     {
240         mixin("options." ~ __traits(identifier, symbol) ~ "();");
241     }
242 
243     return options;
244 }
245 
246 ///
247 unittest
248 {
249     import std.exception :
250         assertThrown,
251         enforce;
252 
253     struct Tester
254     {
255         @Validate!(value => enforce(value == 1))
256         int a = 1;
257 
258         @Validate!((value, options) => enforce(value == 2 * options.a))
259         int b = 2;
260 
261         string[] calls;
262 
263         @PostValidate(Priority.low)
264         void priorityLow() {
265             calls ~= "priorityLow";
266         }
267 
268         @PostValidate(Priority.medium)
269         void priorityMedium() {
270             calls ~= "priorityMedium";
271         }
272 
273         @PostValidate(Priority.high)
274         void priorityHigh() {
275             calls ~= "priorityHigh";
276         }
277     }
278 
279     Tester options;
280 
281     options = processOptions(options);
282 
283     assert(options.calls == [
284         "priorityHigh",
285         "priorityMedium",
286         "priorityLow",
287     ]);
288 
289     options.a = 2;
290 
291     assertThrown!Exception(processOptions(options));
292 }
293 
294 
295 /// Call this method when your program is about to stop execution to enable
296 /// execution of `CleanUp` hooks.
297 ///
298 /// Example:
299 /// ---
300 ///
301 /// void main(in string[] args)
302 /// {
303 ///     auto options = processOptions(parseArgs!Options(args[1 .. $]));
304 ///
305 ///     scope (exit) cast(void) cleanUp(options);
306 ///
307 ///     /// doing something productive ...
308 /// }
309 /// ---
310 Options cleanUp(Options)(Options options)
311 {
312     import std.meta : staticSort;
313     import std.traits : getSymbolsByUDA;
314 
315     alias cleanUpQueue = staticSort!(
316         cmpPriority!CleanUp,
317         getSymbolsByUDA!(Options, CleanUp),
318     );
319 
320     static foreach (alias symbol; cleanUpQueue)
321     {
322         mixin("options." ~ __traits(identifier, symbol) ~ "();");
323     }
324 
325     return options;
326 }
327 
328 unittest
329 {
330     import std.exception : assertThrown;
331 
332     struct Tester
333     {
334         string[] calls;
335 
336         @CleanUp(Priority.low)
337         void priorityLow() {
338             calls ~= "priorityLow";
339         }
340 
341         @CleanUp(Priority.medium)
342         void priorityMedium() {
343             calls ~= "priorityMedium";
344         }
345 
346         @CleanUp(Priority.high)
347         void priorityHigh() {
348             calls ~= "priorityHigh";
349         }
350     }
351 
352     Tester options;
353 
354     options = cleanUp(options);
355 
356     assert(options.calls == [
357         "priorityHigh",
358         "priorityMedium",
359         "priorityLow",
360     ]);
361 }
362 
363 import std.traits : getUDAs;
364 
365 private enum getUDA(alias symbol, T) = getUDAs!(symbol, T)[0];