1 /// Utility for binding a string into arbitrary types, using user-defined functions. 2 module jaster.cli.binder; 3 4 private 5 { 6 import std.traits : isNumeric, hasUDA; 7 import jaster.cli.result, jaster.cli.internal; 8 } 9 10 /++ 11 + Attach this to any free-standing function to mark it as an argument binder. 12 + 13 + See_Also: 14 + `jaster.cli.binder.ArgBinder` for more details. 15 + ++/ 16 struct ArgBinderFunc {} 17 18 /++ 19 + Attach this to any struct to specify that it can be used as an arg validator. 20 + 21 + See_Also: 22 + `jaster.cli.binder.ArgBinder` for more details. 23 + ++/ 24 struct ArgValidator {} 25 26 // Kind of wanted to reuse `ArgBinderFunc`, but making it templated makes it a bit jank to use with functions, 27 // which don't need to provide any template values for it. So we have this UDA instead. 28 /++ 29 + Attach this onto an argument/provide it directly to `ArgBinder.bind`, to specify a specific function to use 30 + when binding the argument, instead of relying on ArgBinder's default behaviour. 31 + 32 + Notes: 33 + The `Func` should match the following signature: 34 + 35 + ``` 36 + Result!T Func(string arg); 37 + ``` 38 + 39 + Where `T` will be the type of the argument being bound to. 40 + 41 + Params: 42 + Func = The function to use to perform the binding. 43 + 44 + See_Also: 45 + `jaster.cli.binder.ArgBinder` and `jaster.cli.binder.ArgBinder.bind` for more details. 46 + ++/ 47 struct ArgBindWith(alias Func) 48 { 49 Result!T bind(T)(string arg) 50 { 51 return Func(arg); 52 } 53 } 54 55 /++ 56 + A static struct providing functionality for binding a string (the argument) to a value, as well as optionally validating it. 57 + 58 + Description: 59 + The ArgBinder itself does not directly contain functions to bind or validate arguments (e.g arg -> int, arg -> enum, etc.). 60 + 61 + Instead, arg binders are user-provided, free-standing functions that are automatically detected from the specified `Modules`. 62 + 63 + For each module passed in the `Modules` template parameter, the arg binder will search for any free-standing function marked with 64 + the `@ArgBinderFunc` UDA. These functions must follow a specific signature `@ArgBinderFunc void myBinder(string arg, ref TYPE value)`. 65 + 66 + The second parameter (marked 'TYPE') can be *any* type that is desired. The type of this second parameter defines which type it will 67 + bind/convert the given argument into. The second parameter may also be a template type, if needed, to allow for more generic binders. 68 + 69 + For example, the following binder `@ArgBinderFunc void argToInt(string arg, ref int value);` will be called anytime the arg binder 70 + needs to bind the argument into an `int` value. 71 + 72 + When binding a value, you can optionally pass in a set of Validators, which are (typically) struct UDAs that provide a certain 73 + interface for validation. 74 + 75 + Lookup_Rules: 76 + The arg binder functions off of a simple 'First come first served' ruleset. 77 + 78 + When looking for a suitable `@ArgBinderFunc` for the given value type, the following process is taken: 79 + * Foreach module in the `Modules` type parameter (from first to last). 80 + * Foreach free-standing function inside of the current module (usually in lexical order). 81 + * Do a compile-time check to see if this function can be called with a string as the first parameter, and the value type as the second. 82 + * If the check passes, use this function. 83 + * Otherwise, continue onto the next function. 84 + 85 + This means there is significant meaning in the order that the modules are passed. Because of this, the built-in binders (contained in the 86 + same module as this struct) will always be put at the very end of the list, meaning the user has the oppertunity to essentially 'override' any 87 + of the built-in binders. 88 + 89 + One may ask "Won't that be confusing? How do I know which binder is being used?". My answer, while not perfect, is in non-release builds, 90 + the binder will output a `debug pragma` to give detailed information on which binders are used for which types, and which ones are skipped over (and why they were skipped). 91 + 92 + Note that you must also add "JCLI_Verbose" as a version (either in your dub file, or cli, or whatever) for these messages to show up. 93 + 94 + While not perfect, this does go over the entire process the arg binder is doing to select which `@ArgBinderFunc` it will use. 95 + 96 + Specific_Binders: 97 + Instead of using the lookup rules above, you can make use of the `ArgBindWith` UDA to provide a specific function to perform the binding 98 + of an argument. 99 + 100 + Validation_: 101 + Validation structs can be passed via the `UDAs` template parameter present for the `ArgBinder.bind` function. 102 + 103 + If you are using `CommandLineInterface` (JCLI's default core), then a field's UDAs are passed through automatically as validator structs. 104 + 105 + A validator is simply a struct marked with `@ArgValidator` that defines either, or both of these function signatures (or compatible signatures): 106 + 107 + ``` 108 + Result!void onPreValidate(string arg); 109 + Result!void onValidate(VALUE_TYPE value); // Can be templated of course. 110 + ``` 111 + 112 + A validator containing the `onPreValidate` function can be used to validate the argument prior to it being ran through 113 + an `@ArgBinderFunc`. 114 + 115 + A validator containing the `onValidate` function can be used to validate the argument after it has been bound by an `@ArgBinderFunc`. 116 + 117 + If validation fails, the vaildator can set the error message with `Result!void.failure()`. If this is left as `null`, then one will be automatically 118 + generated for you. 119 + 120 + By specifying the "JCLI_Verbose" version, the `ArgBinder` will detail what validators are being used for what types, and for which stages of binding. 121 + 122 + Notes: 123 + While other parts of this library have special support for `Nullable` types. This struct doesn't directly have any special 124 + behaviour for them, and instead must be built on top of this struct (a templated `@ArgBinderFunc` just for nullables is totally possible!). 125 + 126 + Params: 127 + Modules = The modules to look over. Please read the 'Description' and 'Lookup Rules' sections of this documentation comment. 128 + +/ 129 static struct ArgBinder(Modules...) 130 { 131 import std.conv : to; 132 import std.traits : getSymbolsByUDA, Parameters, isFunction, fullyQualifiedName; 133 import std.meta : AliasSeq; 134 import std.format : format; 135 import jaster.cli.udas, jaster.cli.internal; 136 137 alias AllModules = AliasSeq!(Modules, jaster.cli.binder); 138 139 /+ PUBLIC INTERFACE +/ 140 public static 141 { 142 /++ 143 + Binds the given `arg` to the `value`, using the `@ArgBinderFunc` found by using the 'Lookup Rules' documented in the 144 + document comment for `ArgBinder`. 145 + 146 + Validators_: 147 + The `UDAs` template parameter is used to pass in different UDA structs, including validator structs (see ArgBinder's documentation comment). 148 + 149 + Anything inside of this template parameter that isn't a struct, and doesn't have the `ArgValidator` UDA 150 + will be completely ignored, so it is safe to simply pass the results of 151 + `__traits(getAttributes, someField)` without having to worry about filtering. 152 + 153 + Specific_Binders: 154 + The `UDAs` template paramter is used to pass in different UDA structs, including the `ArgBindWith` UDA. 155 + 156 + If the `ArgBindWith` UDA exists within the given parameter, arg binding will be performed using the function 157 + provided by `ArgBindWith`, instead of using the default lookup rules defined by `ArgBinder`. 158 + 159 + For example, say you have a several `File` arguments that need different binding behaviour (some are read-only, some truncate, etc.) 160 + In a case like this, you could have some of those arguments marked with `@ArgBindWith!openFileReadOnly` and others with 161 + a function for truncating, etc. 162 + 163 + Throws: 164 + `Exception` if any validator fails. 165 + 166 + Assertions: 167 + When an `@ArgBinderFunc` is found, it must have only 1 parameter. 168 + 169 + The first parameter of an `@ArgBinderFunc` must be a `string`. 170 + 171 + It must return an instance of the `Result` struct. It is recommended to use `Result!void` as the result's `Success.value` is ignored. 172 + 173 + If no appropriate binder func was found, then an assert(false) is used. 174 + 175 + If `@ArgBindWith` exists, then exactly 1 must exist, any more than 1 is an error. 176 + 177 + Params: 178 + arg = The argument to bind. 179 + value = The value to put the result in. 180 + UDAs = A tuple of UDA structs to use. 181 + ++/ 182 Result!T bind(T, UDAs...)(string arg) 183 { 184 import std.conv : to; 185 import std.traits : getSymbolsByUDA, isInstanceOf; 186 187 auto preValidateResult = onPreValidate!(T, UDAs)(arg); 188 if(preValidateResult.isFailure) 189 return Result!T.failure(preValidateResult.asFailure.error); 190 191 alias ArgBindWithInstance = TryGetArgBindWith!UDAs; 192 193 static if(is(ArgBindWithInstance == void)) 194 { 195 enum Binder = ArgBinderFor!(T, AllModules); 196 auto result = Binder.Binder(arg); 197 } 198 else 199 auto result = ArgBindWithInstance.init.bind!T(arg); // Looks weird, but trust me. Keep in mind it's an `alias` not an `enum`. 200 201 if(result.isSuccess) 202 { 203 auto postValidateResult = onValidate!(T, UDAs)(arg, result.asSuccess.value); 204 if(postValidateResult.isFailure) 205 return Result!T.failure(postValidateResult.asFailure.error); 206 } 207 208 return result; 209 } 210 211 private Result!void onPreValidate(T, UDAs...)(string arg) 212 { 213 static foreach(Validator; ValidatorsFrom!UDAs) 214 {{ 215 static if(isPreValidator!(Validator)) 216 { 217 debugPragma!("Using PRE VALIDATION validator %s for type %s".format(Validator, T.stringof)); 218 219 Result!void result = Validator.onPreValidate(arg); 220 if(!result.isSuccess) 221 { 222 return result.failure(createValidatorError( 223 "Pre validation", 224 "%s".format(Validator), 225 T.stringof, 226 arg, 227 "[N/A]", 228 result.asFailure.error 229 )); 230 } 231 } 232 }} 233 234 return Result!void.success(); 235 } 236 237 private Result!void onValidate(T, UDAs...)(string arg, T value) 238 { 239 static foreach(Validator; ValidatorsFrom!UDAs) 240 {{ 241 static if(isPostValidator!(Validator)) 242 { 243 debugPragma!("Using VALUE VALIDATION validator %s for type %s".format(Validator, T.stringof)); 244 245 Result!void result = Validator.onValidate(value); 246 if(!result.isSuccess) 247 { 248 return result.failure(createValidatorError( 249 "Value validation", 250 "%s".format(Validator), 251 T.stringof, 252 arg, 253 "%s".format(value), 254 result.asFailure.error 255 )); 256 } 257 } 258 }} 259 260 return Result!void.success(); 261 } 262 } 263 } 264 /// 265 @safe @("ArgBinder unittest") 266 unittest 267 { 268 alias Binder = ArgBinder!(jaster.cli.binder); 269 270 // Non-validated bindings. 271 auto value = Binder.bind!int("200"); 272 auto strValue = Binder.bind!string("200"); 273 274 assert(value.asSuccess.value == 200); 275 assert(strValue.asSuccess.value == "200"); 276 277 // Validated bindings 278 @ArgValidator 279 static struct GreaterThan 280 { 281 import std.traits : isNumeric; 282 ulong value; 283 284 Result!void onValidate(T)(T value) 285 if(isNumeric!T) 286 { 287 import std.format : format; 288 289 return value > this.value 290 ? Result!void.success() 291 : Result!void.failure("Value %s is NOT greater than %s".format(value, this.value)); 292 } 293 } 294 295 value = Binder.bind!(int, GreaterThan(68))("69"); 296 assert(value.asSuccess.value == 69); 297 298 // Failed validation 299 assert(Binder.bind!(int, GreaterThan(70))("69").isFailure); 300 } 301 302 @("Test that ArgBinder correctly discards non-validators") 303 unittest 304 { 305 alias Binder = ArgBinder!(jaster.cli.binder); 306 307 Binder.bind!(int, "string", null, 2020)("2"); 308 } 309 310 @("Test that __traits(getAttributes) works with ArgBinder") 311 unittest 312 { 313 @ArgValidator 314 static struct Dummy 315 { 316 Result!void onPreValidate(string arg) 317 { 318 return Result!void.failure(null); 319 } 320 } 321 322 alias Binder = ArgBinder!(jaster.cli.binder); 323 324 static struct S 325 { 326 @Dummy 327 int value; 328 } 329 330 assert(Binder.bind!(int, __traits(getAttributes, S.value))("200").isFailure); 331 } 332 333 @("Test that ArgBindWith works") 334 unittest 335 { 336 static struct S 337 { 338 @ArgBindWith!(str => Result!string.success(str ~ " lalafells")) 339 string arg; 340 } 341 342 alias Binder = ArgBinder!(jaster.cli.binder); 343 344 auto result = Binder.bind!(string, __traits(getAttributes, S.arg))("Destroy all"); 345 assert(result.isSuccess); 346 assert(result.asSuccess.value == "Destroy all lalafells"); 347 } 348 349 /+ HELPERS +/ 350 @safe 351 private string createValidatorError( 352 string stageName, 353 string validatorName, 354 string typeName, 355 string argValue, 356 string valueAsString, 357 string validatorError 358 ) 359 { 360 import std.format : format; 361 return (validatorError !is null) 362 ? validatorError 363 : "%s failed for type %s. Validator = %s; Arg = '%s'; Value = %s" 364 .format(stageName, typeName, validatorName, argValue, valueAsString); 365 } 366 367 private enum isValidator(alias V) = is(typeof(V) == struct) && hasUDA!(typeof(V), ArgValidator); 368 private enum isPreValidator(alias V) = isValidator!V && __traits(hasMember, typeof(V), "onPreValidate"); 369 private enum isPostValidator(alias V) = isValidator!V && __traits(hasMember, typeof(V), "onValidate"); 370 371 private template ValidatorsFrom(UDAs...) 372 { 373 import std.meta : staticMap, Filter; 374 import jaster.cli.udas : ctorUdaIfNeeded; 375 376 alias Validators = staticMap!(ctorUdaIfNeeded, UDAs); 377 alias ValidatorsFrom = Filter!(isValidator, Validators); 378 } 379 380 private struct BinderInfo(alias T, alias Symbol) 381 { 382 import std.traits : fullyQualifiedName, isFunction, Parameters, ReturnType; 383 384 // For templated binder funcs, we need a slightly different set of values. 385 static if(__traits(compiles, Symbol!T)) 386 { 387 alias Binder = Symbol!T; 388 const FQN = fullyQualifiedName!Binder~"!("~T.stringof~")"; 389 const IsTemplated = true; 390 } 391 else 392 { 393 alias Binder = Symbol; 394 const FQN = fullyQualifiedName!Binder; 395 const IsTemplated = false; 396 } 397 398 const IsFunction = isFunction!Binder; 399 400 static if(IsFunction) 401 { 402 alias Params = Parameters!Binder; 403 alias RetType = ReturnType!Binder; 404 } 405 } 406 407 private template ArgBinderMapper(T, alias Binder) 408 { 409 import std.traits : isInstanceOf; 410 411 enum Info = BinderInfo!(T, Binder)(); 412 413 // When the debugPragma isn't used inside a function, we have to make aliases to each call in order for it to work. 414 // Ugly, but whatever. 415 416 static if(!Info.IsFunction) 417 { 418 alias a = debugPragma!("Skipping arg binder `"~Info.FQN~"` for type `"~T.stringof~"` because `isFunction` is returning false."); 419 static if(Info.IsTemplated) 420 alias b = debugPragma!("This binder is templated, so it is likely that the binder's contract failed, or its code doesn't compile for this given type."); 421 422 alias ArgBinderMapper = void; 423 } 424 else static if(!__traits(compiles, { Result!T r = Info.Binder(""); })) 425 { 426 alias c = debugPragma!("Skipping arg binder `"~Info.FQN~"` for type `"~T.stringof~"` because it does not compile for the given type."); 427 428 alias ArgBinderMapper = void; 429 } 430 else 431 { 432 alias d = debugPragma!("Considering arg binder `"~Info.FQN~"` for type `"~T.stringof~"`."); 433 434 static assert(Info.Params.length == 1, 435 "The arg binder `"~Info.FQN~"` must only have `1` parameter, not `"~Info.Params.length.to!string~"` parameters." 436 ); 437 static assert(is(Info.Params[0] == string), 438 "The arg binder `"~Info.FQN~"` must have a `string` as their first parameter, not a(n) `"~Info.Params[0].stringof~"`." 439 ); 440 static assert(isInstanceOf!(Result, Info.RetType), 441 "The arg binder `"~Info.FQN~"` must return a `Result`, not `"~Info.RetType.stringof~"`" 442 ); 443 444 enum ArgBinderMapper = Info; 445 } 446 } 447 448 private template ArgBinderFor(alias T, Modules...) 449 { 450 import std.meta : staticMap, Filter; 451 import jaster.cli.udas : getSymbolsByUDAInModules; 452 453 enum isNotVoid(alias T) = !is(T == void); 454 alias Mapper(alias BinderT) = ArgBinderMapper!(T, BinderT); 455 456 alias Binders = getSymbolsByUDAInModules!(ArgBinderFunc, Modules); 457 alias BindersForT = staticMap!(Mapper, Binders); 458 alias BindersFiltered = Filter!(isNotVoid, BindersForT); 459 460 // Have to use static if here because the compiler's order of operations makes it so a single `static assert` wouldn't be evaluated at the right time, 461 // and so it wouldn't produce our error message, but instead an index out of bounds one. 462 static if(BindersFiltered.length > 0) 463 { 464 enum ArgBinderFor = BindersFiltered[0]; 465 alias a = debugPragma!("Using arg binder `"~ArgBinderFor.FQN~"` for type `"~T.stringof~"`"); 466 } 467 else 468 static assert(false, "No arg binder found for type `"~T.stringof~"`"); 469 } 470 471 private template TryGetArgBindWith(UDAs...) 472 { 473 import std.traits : isInstanceOf; 474 import std.meta : Filter; 475 476 enum FilterFunc(alias T) = isInstanceOf!(ArgBindWith, T); 477 alias Filtered = Filter!(FilterFunc, UDAs); 478 479 static if(Filtered.length == 0) 480 alias TryGetArgBindWith = void; 481 else static if(Filtered.length > 1) 482 static assert(false, "Multiple `ArgBindWith` instances were found, only one can be used."); 483 else 484 alias TryGetArgBindWith = Filtered[0]; 485 } 486 487 /+ BUILT-IN BINDERS +/ 488 489 /// arg -> string. The result is the contents of `arg` as-is. 490 @ArgBinderFunc @safe @nogc 491 Result!string stringBinder(string arg) nothrow pure 492 { 493 return Result!string.success(arg); 494 } 495 496 /// arg -> numeric | enum | bool. The result is `arg` converted to `T`. 497 @ArgBinderFunc @safe 498 Result!T convBinder(T)(string arg) pure 499 if(isNumeric!T || is(T == bool) || is(T == enum)) 500 { 501 import std.conv : to, ConvException; 502 503 try return Result!T.success(arg.to!T); 504 catch(ConvException ex) 505 return Result!T.failure(ex.msg); 506 } 507 508 /+ BUILT-IN VALIDATORS +/ 509 510 /++ 511 + An `@ArgValidator` that runs the given `Func` during post-binding validation. 512 + 513 + Notes: 514 + This validator is loosely typed, so if your validator function doesn't compile or doesn't work with whatever 515 + type you attach this validator to, you might get some long-winded errors. 516 + 517 + Params: 518 + Func = The function that provides validation on a value. 519 + ++/ 520 @ArgValidator 521 struct PostValidate(alias Func) 522 { 523 // We don't do any static checking of the parameter type, as we can utilise a very interesting use case of anonymous lambdas here 524 // by allowing the compiler to perform the checks for us. 525 // 526 // However that does mean we can't provide our own error message, but the scope is so small that a compiler generated one should suffice. 527 Result!void onValidate(ParamT)(ParamT arg) 528 { 529 return Func(arg); 530 } 531 } 532 533 // I didn't *want* this to be templated, but when it's not templated and asks directly for a 534 // `Result!void function(string)`, I get a very very odd error message: "expression __lambda2 is not a valid template value argument" 535 /++ 536 + An `@ArgValidator` that runs the given `Func` during pre-binding validation. 537 + 538 + Params: 539 + Func = The function that provides validation on an argument. 540 + ++/ 541 @ArgValidator 542 struct PreValidate(alias Func) 543 { 544 Result!void onPreValidate(string arg) 545 { 546 return Func(arg); 547 } 548 } 549 /// 550 @("PostValidate and PreValidate") 551 unittest 552 { 553 static struct S 554 { 555 @PreValidate!(str => Result!void.failureIf(str.length != 3, "Number must be 3 digits long.")) 556 @PostValidate!(i => Result!void.failureIf(i <= 200, "Number must be larger than 200.")) 557 int arg; 558 } 559 560 alias Binder = ArgBinder!(jaster.cli.binder); 561 alias UDAs = __traits(getAttributes, S.arg); 562 563 assert(Binder.bind!(int, UDAs)("20").isFailure); 564 assert(Binder.bind!(int, UDAs)("199").isFailure); 565 assert(Binder.bind!(int, UDAs)("300").asSuccess.value == 300); 566 }