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 }