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 enum Priority
71 {
72     low,
73     medium,
74     high,
75 }
76 
77 private template cmpPriority(T)
78 {
79     enum cmpPriority(alias a, alias b) = getUDA!(a, T).priority > getUDA!(b, T).priority;
80 }
81 
82 unittest
83 {
84     struct Tester
85     {
86         @PostValidate(Priority.low)
87         void priorityLow() { }
88 
89         @PostValidate(Priority.medium)
90         void priorityMedium() { }
91 
92         @PostValidate(Priority.high)
93         void priorityHigh() { }
94     }
95 
96     alias compare = cmpPriority!PostValidate;
97 
98     static assert(compare!(
99         Tester.priorityHigh,
100         Tester.priorityLow,
101     ));
102     static assert(!compare!(
103         Tester.priorityLow,
104         Tester.priorityHigh,
105     ));
106     static assert(!compare!(
107         Tester.priorityMedium,
108         Tester.priorityMedium,
109     ));
110 }
111 
112 
113 /// Call this method on the result of `parseArgs` to execute validations and
114 /// validation hooks.
115 Options processOptions(Options)(Options options)
116 {
117     import darg :
118         Argument,
119         Option;
120     import darg_plus.exception : CLIException;
121     import std.format : format;
122     import std.meta : staticSort;
123     import std..string : wrap;
124     import std.traits :
125         getSymbolsByUDA,
126         getUDAs;
127 
128     alias preValidateQueue = staticSort!(
129         cmpPriority!PreValidate,
130         getSymbolsByUDA!(Options, PreValidate),
131     );
132 
133     static foreach (alias symbol; preValidateQueue)
134     {
135         mixin("options." ~ __traits(identifier, symbol) ~ "();");
136     }
137 
138     static foreach (alias symbol; getSymbolsByUDA!(Options, Validate))
139     {{
140         alias validateUDAs = getUDAs!(symbol, Validate);
141 
142         foreach (validateUDA; validateUDAs)
143         {
144             alias validate = validateUDA.validate;
145             auto value = __traits(getMember, options, __traits(identifier, symbol));
146             alias Value = typeof(value);
147             alias Validator = typeof(validate);
148 
149             try
150             {
151                 static if (is(typeof(validate(value, options))))
152                     cast(void) validate(value, options);
153                 else
154                     cast(void) validate(value);
155             }
156             catch (Exception cause)
157             {
158                 enum isOption = getUDAs!(symbol, Option).length > 0;
159                 enum isArgument = getUDAs!(symbol, Argument).length > 0;
160 
161                 static if (isOption)
162                 {
163                     enum thing = "option";
164                     enum name = getUDAs!(symbol, Option)[0].toString();
165                 }
166                 else static if (isArgument)
167                 {
168                     enum thing = "argument";
169                     enum name = getUDAs!(symbol, Argument)[0].name;
170                 }
171                 else
172                 {
173                     enum thing = "property";
174                     enum name = __traits(identifier, symbol);
175                 }
176 
177                 throw new CLIException("invalid " ~ thing ~ " " ~ name ~ ": " ~ cause.msg, cause);
178             }
179         }
180     }}
181 
182     alias postValidateQueue = staticSort!(
183         cmpPriority!PostValidate,
184         getSymbolsByUDA!(Options, PostValidate),
185     );
186 
187     static foreach (alias symbol; postValidateQueue)
188     {
189         mixin("options." ~ __traits(identifier, symbol) ~ "();");
190     }
191 
192     return options;
193 }
194 
195 ///
196 unittest
197 {
198     import std.exception :
199         assertThrown,
200         enforce;
201 
202     struct Tester
203     {
204         @Validate!(value => enforce(value == 1))
205         int a = 1;
206 
207         @Validate!((value, options) => enforce(value == 2 * options.a))
208         int b = 2;
209 
210         string[] calls;
211 
212         @PostValidate(Priority.low)
213         void priorityLow() {
214             calls ~= "priorityLow";
215         }
216 
217         @PostValidate(Priority.medium)
218         void priorityMedium() {
219             calls ~= "priorityMedium";
220         }
221 
222         @PostValidate(Priority.high)
223         void priorityHigh() {
224             calls ~= "priorityHigh";
225         }
226     }
227 
228     Tester options;
229 
230     options = processOptions(options);
231 
232     assert(options.calls == [
233         "priorityHigh",
234         "priorityMedium",
235         "priorityLow",
236     ]);
237 
238     options.a = 2;
239 
240     assertThrown!Exception(processOptions(options));
241 }
242 
243 
244 /// Call this method when your program is about to stop execution to enable
245 /// execution of `CleanUp` hooks.
246 ///
247 /// Example:
248 /// ---
249 ///
250 /// void main(in string[] args)
251 /// {
252 ///     auto options = processOptions(parseArgs!Options(args[1 .. $]));
253 ///
254 ///     scope (exit) cast(void) cleanUp(options);
255 ///
256 ///     /// doing something productive ...
257 /// }
258 /// ---
259 Options cleanUp(Options)(Options options)
260 {
261     import std.meta : staticSort;
262     import std.traits : getSymbolsByUDA;
263 
264     alias cleanUpQueue = staticSort!(
265         cmpPriority!CleanUp,
266         getSymbolsByUDA!(Options, CleanUp),
267     );
268 
269     static foreach (alias symbol; cleanUpQueue)
270     {
271         mixin("options." ~ __traits(identifier, symbol) ~ "();");
272     }
273 
274     return options;
275 }
276 
277 unittest
278 {
279     import std.exception : assertThrown;
280 
281     struct Tester
282     {
283         string[] calls;
284 
285         @CleanUp(Priority.low)
286         void priorityLow() {
287             calls ~= "priorityLow";
288         }
289 
290         @CleanUp(Priority.medium)
291         void priorityMedium() {
292             calls ~= "priorityMedium";
293         }
294 
295         @CleanUp(Priority.high)
296         void priorityHigh() {
297             calls ~= "priorityHigh";
298         }
299     }
300 
301     Tester options;
302 
303     options = cleanUp(options);
304 
305     assert(options.calls == [
306         "priorityHigh",
307         "priorityMedium",
308         "priorityLow",
309     ]);
310 }
311 
312 import std.traits : getUDAs;
313 
314 private enum getUDA(alias symbol, T) = getUDAs!(symbol, T)[0];