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