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 }