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];