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