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 version(Amalgamation) 138 alias AllModules = AliasSeq!(Modules, jcli); 139 else 140 alias AllModules = AliasSeq!(Modules, jaster.cli.binder); 141 142 /+ PUBLIC INTERFACE +/ 143 public static 144 { 145 /++ 146 + Binds the given `arg` to the `value`, using the `@ArgBinderFunc` found by using the 'Lookup Rules' documented in the 147 + document comment for `ArgBinder`. 148 + 149 + Validators_: 150 + The `UDAs` template parameter is used to pass in different UDA structs, including validator structs (see ArgBinder's documentation comment). 151 + 152 + Anything inside of this template parameter that isn't a struct, and doesn't have the `ArgValidator` UDA 153 + will be completely ignored, so it is safe to simply pass the results of 154 + `__traits(getAttributes, someField)` without having to worry about filtering. 155 + 156 + Specific_Binders: 157 + The `UDAs` template paramter is used to pass in different UDA structs, including the `ArgBindWith` UDA. 158 + 159 + If the `ArgBindWith` UDA exists within the given parameter, arg binding will be performed using the function 160 + provided by `ArgBindWith`, instead of using the default lookup rules defined by `ArgBinder`. 161 + 162 + For example, say you have a several `File` arguments that need different binding behaviour (some are read-only, some truncate, etc.) 163 + In a case like this, you could have some of those arguments marked with `@ArgBindWith!openFileReadOnly` and others with 164 + a function for truncating, etc. 165 + 166 + Throws: 167 + `Exception` if any validator fails. 168 + 169 + Assertions: 170 + When an `@ArgBinderFunc` is found, it must have only 1 parameter. 171 + 172 + The first parameter of an `@ArgBinderFunc` must be a `string`. 173 + 174 + It must return an instance of the `Result` struct. It is recommended to use `Result!void` as the result's `Success.value` is ignored. 175 + 176 + If no appropriate binder func was found, then an assert(false) is used. 177 + 178 + If `@ArgBindWith` exists, then exactly 1 must exist, any more than 1 is an error. 179 + 180 + Params: 181 + arg = The argument to bind. 182 + value = The value to put the result in. 183 + UDAs = A tuple of UDA structs to use. 184 + ++/ 185 Result!T bind(T, UDAs...)(string arg) 186 { 187 import std.conv : to; 188 import std.traits : getSymbolsByUDA, isInstanceOf; 189 190 auto preValidateResult = onPreValidate!(T, UDAs)(arg); 191 if(preValidateResult.isFailure) 192 return Result!T.failure(preValidateResult.asFailure.error); 193 194 alias ArgBindWithInstance = TryGetArgBindWith!UDAs; 195 196 static if(is(ArgBindWithInstance == void)) 197 { 198 enum Binder = ArgBinderFor!(T, AllModules); 199 auto result = Binder.Binder(arg); 200 } 201 else 202 auto result = ArgBindWithInstance.init.bind!T(arg); // Looks weird, but trust me. Keep in mind it's an `alias` not an `enum`. 203 204 if(result.isSuccess) 205 { 206 auto postValidateResult = onValidate!(T, UDAs)(arg, result.asSuccess.value); 207 if(postValidateResult.isFailure) 208 return Result!T.failure(postValidateResult.asFailure.error); 209 } 210 211 return result; 212 } 213 214 private Result!void onPreValidate(T, UDAs...)(string arg) 215 { 216 static foreach(Validator; ValidatorsFrom!UDAs) 217 {{ 218 static if(isPreValidator!(Validator)) 219 { 220 debugPragma!("Using PRE VALIDATION validator %s for type %s".format(Validator, T.stringof)); 221 222 Result!void result = Validator.onPreValidate(arg); 223 if(!result.isSuccess) 224 { 225 return result.failure(createValidatorError( 226 "Pre validation", 227 "%s".format(Validator), 228 T.stringof, 229 arg, 230 "[N/A]", 231 result.asFailure.error 232 )); 233 } 234 } 235 }} 236 237 return Result!void.success(); 238 } 239 240 private Result!void onValidate(T, UDAs...)(string arg, T value) 241 { 242 static foreach(Validator; ValidatorsFrom!UDAs) 243 {{ 244 static if(isPostValidator!(Validator)) 245 { 246 debugPragma!("Using VALUE VALIDATION validator %s for type %s".format(Validator, T.stringof)); 247 248 Result!void result = Validator.onValidate(value); 249 if(!result.isSuccess) 250 { 251 return result.failure(createValidatorError( 252 "Value validation", 253 "%s".format(Validator), 254 T.stringof, 255 arg, 256 "%s".format(value), 257 result.asFailure.error 258 )); 259 } 260 } 261 }} 262 263 return Result!void.success(); 264 } 265 } 266 } 267 /// 268 @safe @("ArgBinder unittest") 269 unittest 270 { 271 alias Binder = ArgBinder!(jaster.cli.binder); 272 273 // Non-validated bindings. 274 auto value = Binder.bind!int("200"); 275 auto strValue = Binder.bind!string("200"); 276 277 assert(value.asSuccess.value == 200); 278 assert(strValue.asSuccess.value == "200"); 279 280 // Validated bindings 281 @ArgValidator 282 static struct GreaterThan 283 { 284 import std.traits : isNumeric; 285 ulong value; 286 287 Result!void onValidate(T)(T value) 288 if(isNumeric!T) 289 { 290 import std.format : format; 291 292 return value > this.value 293 ? Result!void.success() 294 : Result!void.failure("Value %s is NOT greater than %s".format(value, this.value)); 295 } 296 } 297 298 value = Binder.bind!(int, GreaterThan(68))("69"); 299 assert(value.asSuccess.value == 69); 300 301 // Failed validation 302 assert(Binder.bind!(int, GreaterThan(70))("69").isFailure); 303 } 304 305 @("Test that ArgBinder correctly discards non-validators") 306 unittest 307 { 308 alias Binder = ArgBinder!(jaster.cli.binder); 309 310 Binder.bind!(int, "string", null, 2020)("2"); 311 } 312 313 @("Test that __traits(getAttributes) works with ArgBinder") 314 unittest 315 { 316 @ArgValidator 317 static struct Dummy 318 { 319 Result!void onPreValidate(string arg) 320 { 321 return Result!void.failure(null); 322 } 323 } 324 325 alias Binder = ArgBinder!(jaster.cli.binder); 326 327 static struct S 328 { 329 @Dummy 330 int value; 331 } 332 333 assert(Binder.bind!(int, __traits(getAttributes, S.value))("200").isFailure); 334 } 335 336 @("Test that ArgBindWith works") 337 unittest 338 { 339 static struct S 340 { 341 @ArgBindWith!(str => Result!string.success(str ~ " lalafells")) 342 string arg; 343 } 344 345 alias Binder = ArgBinder!(jaster.cli.binder); 346 347 auto result = Binder.bind!(string, __traits(getAttributes, S.arg))("Destroy all"); 348 assert(result.isSuccess); 349 assert(result.asSuccess.value == "Destroy all lalafells"); 350 } 351 352 /+ HELPERS +/ 353 @safe 354 private string createValidatorError( 355 string stageName, 356 string validatorName, 357 string typeName, 358 string argValue, 359 string valueAsString, 360 string validatorError 361 ) 362 { 363 import std.format : format; 364 return (validatorError !is null) 365 ? validatorError 366 : "%s failed for type %s. Validator = %s; Arg = '%s'; Value = %s" 367 .format(stageName, typeName, validatorName, argValue, valueAsString); 368 } 369 370 private enum isValidator(alias V) = is(typeof(V) == struct) && hasUDA!(typeof(V), ArgValidator); 371 private enum isPreValidator(alias V) = isValidator!V && __traits(hasMember, typeof(V), "onPreValidate"); 372 private enum isPostValidator(alias V) = isValidator!V && __traits(hasMember, typeof(V), "onValidate"); 373 374 private template ValidatorsFrom(UDAs...) 375 { 376 import std.meta : staticMap, Filter; 377 import jaster.cli.udas : ctorUdaIfNeeded; 378 379 alias Validators = staticMap!(ctorUdaIfNeeded, UDAs); 380 alias ValidatorsFrom = Filter!(isValidator, Validators); 381 } 382 383 private struct BinderInfo(alias T, alias Symbol) 384 { 385 import std.traits : fullyQualifiedName, isFunction, Parameters, ReturnType; 386 387 // For templated binder funcs, we need a slightly different set of values. 388 static if(__traits(compiles, Symbol!T)) 389 { 390 alias Binder = Symbol!T; 391 const FQN = fullyQualifiedName!Binder~"!("~T.stringof~")"; 392 const IsTemplated = true; 393 } 394 else 395 { 396 alias Binder = Symbol; 397 const FQN = fullyQualifiedName!Binder; 398 const IsTemplated = false; 399 } 400 401 const IsFunction = isFunction!Binder; 402 403 static if(IsFunction) 404 { 405 alias Params = Parameters!Binder; 406 alias RetType = ReturnType!Binder; 407 } 408 } 409 410 private template ArgBinderMapper(T, alias Binder) 411 { 412 import std.traits : isInstanceOf; 413 414 enum Info = BinderInfo!(T, Binder)(); 415 416 // When the debugPragma isn't used inside a function, we have to make aliases to each call in order for it to work. 417 // Ugly, but whatever. 418 419 static if(!Info.IsFunction) 420 { 421 alias a = debugPragma!("Skipping arg binder `"~Info.FQN~"` for type `"~T.stringof~"` because `isFunction` is returning false."); 422 static if(Info.IsTemplated) 423 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."); 424 425 alias ArgBinderMapper = void; 426 } 427 else static if(!__traits(compiles, { Result!T r = Info.Binder(""); })) 428 { 429 alias c = debugPragma!("Skipping arg binder `"~Info.FQN~"` for type `"~T.stringof~"` because it does not compile for the given type."); 430 431 alias ArgBinderMapper = void; 432 } 433 else 434 { 435 alias d = debugPragma!("Considering arg binder `"~Info.FQN~"` for type `"~T.stringof~"`."); 436 437 static assert(Info.Params.length == 1, 438 "The arg binder `"~Info.FQN~"` must only have `1` parameter, not `"~Info.Params.length.to!string~"` parameters." 439 ); 440 static assert(is(Info.Params[0] == string), 441 "The arg binder `"~Info.FQN~"` must have a `string` as their first parameter, not a(n) `"~Info.Params[0].stringof~"`." 442 ); 443 static assert(isInstanceOf!(Result, Info.RetType), 444 "The arg binder `"~Info.FQN~"` must return a `Result`, not `"~Info.RetType.stringof~"`" 445 ); 446 447 enum ArgBinderMapper = Info; 448 } 449 } 450 451 private template ArgBinderFor(alias T, Modules...) 452 { 453 import std.meta : staticMap, Filter; 454 import jaster.cli.udas : getSymbolsByUDAInModules; 455 456 enum isNotVoid(alias T) = !is(T == void); 457 alias Mapper(alias BinderT) = ArgBinderMapper!(T, BinderT); 458 459 alias Binders = getSymbolsByUDAInModules!(ArgBinderFunc, Modules); 460 alias BindersForT = staticMap!(Mapper, Binders); 461 alias BindersFiltered = Filter!(isNotVoid, BindersForT); 462 463 // 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, 464 // and so it wouldn't produce our error message, but instead an index out of bounds one. 465 static if(BindersFiltered.length > 0) 466 { 467 enum ArgBinderFor = BindersFiltered[0]; 468 alias a = debugPragma!("Using arg binder `"~ArgBinderFor.FQN~"` for type `"~T.stringof~"`"); 469 } 470 else 471 static assert(false, "No arg binder found for type `"~T.stringof~"`"); 472 } 473 474 private template TryGetArgBindWith(UDAs...) 475 { 476 import std.traits : isInstanceOf; 477 import std.meta : Filter; 478 479 enum FilterFunc(alias T) = isInstanceOf!(ArgBindWith, T); 480 alias Filtered = Filter!(FilterFunc, UDAs); 481 482 static if(Filtered.length == 0) 483 alias TryGetArgBindWith = void; 484 else static if(Filtered.length > 1) 485 static assert(false, "Multiple `ArgBindWith` instances were found, only one can be used."); 486 else 487 alias TryGetArgBindWith = Filtered[0]; 488 } 489 490 /+ BUILT-IN BINDERS +/ 491 492 /// arg -> string. The result is the contents of `arg` as-is. 493 @ArgBinderFunc @safe @nogc 494 Result!string stringBinder(string arg) nothrow pure 495 { 496 return Result!string.success(arg); 497 } 498 499 /// arg -> numeric | enum | bool. The result is `arg` converted to `T`. 500 @ArgBinderFunc @safe 501 Result!T convBinder(T)(string arg) pure 502 if(isNumeric!T || is(T == bool) || is(T == enum)) 503 { 504 import std.conv : to, ConvException; 505 506 try return Result!T.success(arg.to!T); 507 catch(ConvException ex) 508 return Result!T.failure(ex.msg); 509 } 510 511 /+ BUILT-IN VALIDATORS +/ 512 513 /++ 514 + An `@ArgValidator` that runs the given `Func` during post-binding validation. 515 + 516 + Notes: 517 + This validator is loosely typed, so if your validator function doesn't compile or doesn't work with whatever 518 + type you attach this validator to, you might get some long-winded errors. 519 + 520 + Params: 521 + Func = The function that provides validation on a value. 522 + ++/ 523 @ArgValidator 524 struct PostValidate(alias Func) 525 { 526 // We don't do any static checking of the parameter type, as we can utilise a very interesting use case of anonymous lambdas here 527 // by allowing the compiler to perform the checks for us. 528 // 529 // 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. 530 Result!void onValidate(ParamT)(ParamT arg) 531 { 532 return Func(arg); 533 } 534 } 535 536 // I didn't *want* this to be templated, but when it's not templated and asks directly for a 537 // `Result!void function(string)`, I get a very very odd error message: "expression __lambda2 is not a valid template value argument" 538 /++ 539 + An `@ArgValidator` that runs the given `Func` during pre-binding validation. 540 + 541 + Params: 542 + Func = The function that provides validation on an argument. 543 + ++/ 544 @ArgValidator 545 struct PreValidate(alias Func) 546 { 547 Result!void onPreValidate(string arg) 548 { 549 return Func(arg); 550 } 551 } 552 /// 553 @("PostValidate and PreValidate") 554 unittest 555 { 556 static struct S 557 { 558 @PreValidate!(str => Result!void.failureIf(str.length != 3, "Number must be 3 digits long.")) 559 @PostValidate!(i => Result!void.failureIf(i <= 200, "Number must be larger than 200.")) 560 int arg; 561 } 562 563 alias Binder = ArgBinder!(jaster.cli.binder); 564 alias UDAs = __traits(getAttributes, S.arg); 565 566 assert(Binder.bind!(int, UDAs)("20").isFailure); 567 assert(Binder.bind!(int, UDAs)("199").isFailure); 568 assert(Binder.bind!(int, UDAs)("300").asSuccess.value == 300); 569 }