1 module jcli.argbinder.binder;
2 
3 import jcli.introspect, jcli.core;
4 
5 // import std.algorithm;
6 import std.meta;
7 import std.traits;
8 import std.range : ElementType;
9 
10 enum Binder;
11 
12 // Note: these have to be types, because you cannot 
13 // pattern-match templates in an is expression.
14 struct UseConverter(alias _conversionFunction)
15 { 
16     alias conversionFunction = _conversionFunction;
17 }
18 struct PreValidate(_validationFunctions...)
19 {
20     alias validationFunctions = _validationFunctions;
21 }
22 struct PostValidate(_validationFunctions...)
23 {
24     alias validationFunctions = _validationFunctions;
25 }
26 
27 alias PreValidator = PreValidate;
28 alias PostValidator = PostValidate;
29 
30 alias bindArgumentSimple = bindArgument!();
31 
32 template bindArgument(Binders...)
33 {
34     static foreach (Binder; Binders)
35     {
36         static if (is(typeof(&Binder)))
37         {
38             static assert(Parameters!Binder.length == 1
39                 && is(Parameters!Binder[0] : string),
40                 "The binder " ~ Binder.stringof ~ " cannot be invoked with a string argument.");
41         }
42     }
43 
44     Result bindArgument
45     (
46         alias /* Common or Named or Positional info */ argumentInfo,
47         TCommand
48     )
49     (
50         ref TCommand command,
51         string stringValue
52     )
53     {
54         alias argumentFieldSymbol = getArgumentFieldSymbol!(TCommand, argumentInfo);
55         alias preValidators       = getValidators!(argumentFieldSymbol, PreValidate);
56         alias postValidators      = getValidators!(argumentFieldSymbol, PostValidate);
57 
58         static if (argumentInfo.flags.has(ArgFlags._aggregateBit))
59             alias ArgumentType = ElementType!(typeof(argumentFieldSymbol));
60         else
61             alias ArgumentType = typeof(argumentFieldSymbol);
62         
63         alias conversionFunction = getConversionFunction!(argumentFieldSymbol, ArgumentType, Binders);
64 
65         static foreach (v; preValidators)
66         {{
67             const validationResult = v(stringValue);
68             if (!validationResult.isOk)
69                 return fail!void(validationResult.error, validationResult.errorCode);
70         }}
71 
72         ResultOf!ArgumentType conversionResult; // Declared here first in order for opAssign to be called.
73         conversionResult = conversionFunction(stringValue);
74         if (!conversionResult.isOk)
75             return fail!void(conversionResult.error, conversionResult.errorCode);
76 
77         static foreach (v; postValidators)
78         {{
79             const validationResult = v(conversionResult.value);
80             if (!validationResult.isOk)
81                 return fail!void(validationResult.error, validationResult.errorCode);
82         }}
83 
84         static if (argumentInfo.flags.has(ArgFlags._aggregateBit))
85             command.getArgumentFieldRef!argumentInfo ~= conversionResult.value;
86         else
87             command.getArgumentFieldRef!argumentInfo = conversionResult.value;
88 
89         return ok();
90     }
91 }
92 unittest
93 {
94     alias Dummy = ArgNamed;
95     
96     // no binders
97     alias bind = bindArgument!();
98     
99     {
100         struct S
101         {
102             @Dummy
103             int a;
104         }
105         S s;
106         enum a = getCommonArgumentInfo!(S.a);
107         {
108             const result = bind!a(s, "1");
109             assert(result.isOk);
110             assert(s.a == 1);
111         }
112         {
113             const result = bind!a(s, "b");
114             assert(result.isError);
115             assert(s.a == 1);
116         }
117     }
118     {
119         struct S
120         {
121             @Dummy
122             @(ArgConfig.aggregate)
123             int[] a;
124         }
125         S s;
126         enum a = getCommonArgumentInfo!(S.a);
127         {
128             const result = bind!a(s, "1");
129             assert(result.isOk);
130             assert(s.a == [1]);
131         }
132         {
133             const result = bind!a(s, "2");
134             assert(result.isOk);
135             assert(s.a == [1, 2]);
136         }
137         {
138             const result = bind!a(s, "b");
139             assert(result.isError);
140             assert(s.a == [1, 2]);
141         }
142     }
143     {
144         struct S
145         {
146             @Dummy
147             Nullable!bool a;
148         }
149         S s;
150         enum a = getCommonArgumentInfo!(S.a);
151         {
152             s.a = false;
153             const result = bind!a(s, "null");
154             assert(result.isError);
155             assert(s.a == false);
156         }
157         {
158             const result = bind!a(s, "true");
159             assert(result.isOk);
160             assert(s.a == true);
161         }
162         {
163             const result = bind!a(s, "false");
164             assert(result.isOk);
165             assert(s.a == false);
166         }
167         {
168             s.a = true;
169             const result = bind!a(s, "kek");
170             assert(result.isError);
171             assert(s.a == true);
172         }
173     }
174     {
175         struct S
176         {
177             @Dummy
178             @(PreValidate!(a => fail!void("")))
179             int a;
180         }
181         S s;
182         enum a = getCommonArgumentInfo!(S.a);
183         {
184             const result = bind!a(s, "1");
185             assert(result.isError);
186         }
187     }
188     {
189         struct S
190         {
191             @Dummy
192             @(PostValidate!(a => fail!void("")))
193             int a;
194         }
195         S s;
196         enum a = getCommonArgumentInfo!(S.a);
197         {
198             s.a = 9;
199             const result = bind!a(s, "1");
200             assert(result.isError);
201             assert(s.a == 9);
202         }
203     }
204     {
205         struct S
206         {
207             @Dummy
208             @(UseConverter!((string b) => ok("nope")))
209             int a;
210         }
211         enum a = getCommonArgumentInfo!(S.a);
212         static assert(!__traits(compiles, bind!(a, S)));
213     }
214     {
215         struct S
216         {
217             @Dummy
218             @(UseConverter!(b => ok(b ~ "lol")))
219             string a;
220         }
221         enum a = getCommonArgumentInfo!(S.a);
222         S s;
223         {
224             const result = bind!a(s, "1");
225             assert(result.isOk);
226             assert(s.a == "1lol");
227         }
228     }
229 }
230 unittest
231 {
232     alias Dummy = ArgNamed;
233     {
234 
235         struct S
236         {
237             @Dummy
238             string a;
239         }
240 
241         enum a = getCommonArgumentInfo!(S.a);
242         S s;
243         {
244             static ResultOf!string binder(string input) { return ok(input ~ "kek"); }
245             alias bind = bindArgument!(binder);
246             const result = bind!a(s, "1");
247             assert(result.isOk);
248             assert(s.a == "1kek");
249         }
250         {
251             static ResultOf!string binder() { return ok("kek"); }
252             static assert(!__traits(compiles, bindArgument!(binder)));
253         }
254     }
255     {
256         static ResultOf!T test(T)(string arg) { return ok(T.init); }
257         struct S
258         {
259             @Dummy
260             string a;
261         }
262         // Currently, validation is not done for temples at all.
263         static assert(__traits(compiles, bindArgument!(test)));
264     }
265 }
266 
267 template bindArgumentAcrossModules(Modules...)
268 {
269     alias ToBinder(alias M)         = getSymbolsByUDA!(M, Binder);
270     alias Binders                   = staticMap!(ToBinder, Modules);
271     alias bindArgumentAcrossModules = bindArgument!(Binders);
272 }
273 
274 // template GetArgumentBinderInfo(
275 //     alias /* Common or Named or Positional info */ argumentInfo,
276 //     TCommand,
277 //     Binders...)
278 // {
279     
280 // }
281 
282 // This function should be used to convert the string 
283 // to the given value when all other options have failed.
284 import std.conv : to, ConvException;
285 ResultOf!T universalFallbackConverter(T)(string value)
286     if (__traits(compiles, to!T))
287 {
288     try 
289         return ok(to!T(value));
290     catch (ConvException exc)
291         return fail!T(exc.msg); 
292 }
293 
294 private:
295 
296 template getValidators(alias ArgSymbol, alias ValidatorUDAType)
297 {
298     alias result = AliasSeq!();
299     static foreach (alias ValidatorUDA; getUDAs!(ArgSymbol, ValidatorUDAType))
300         result = AliasSeq!(result, ValidatorUDA.validationFunctions);
301     alias getValidators = result;
302 }
303 
304 /// Binders must be functions returning ResultOf
305 template getConversionFunction(
306     alias argumentFieldSymbol,
307     
308     // The argument type may be different from the actual field type
309     // (currently only in the case when the argument has the aggregate flag). 
310     ArgumentType,
311     
312     Binders...)
313 {
314     import std.traits;
315     alias FoundExplicitConverters = getUDAs!(argumentFieldSymbol, UseConverter);
316 
317     static assert(FoundExplicitConverters.length <= 1, "Only one @UseConverter may exist.");
318     static if (FoundExplicitConverters.length == 0)
319     {
320         // There is no such thing as a to!(Nullable!int), for example,
321         // but a Nullable!int can be created implicitly from an int.
322         //
323         // The whole point is, we need to convert into the underlying type 
324         // and not into the outer Nullable type, because then to!ThatType will fail,
325         // but the Nullable!T construction from a T won't.
326         // 
327         // So here we extract the inner type in case it is a Nullable.
328         //
329         // Note:
330         // Nullable-like user types can be handled in user code via the use of Binders.
331         // User-defined binders will match before the universal fallback converter (aka to!T).
332         // 
333         static if (is(ArgumentType : Nullable!T, T))
334             alias ConversionType = T;
335         else
336             alias ConversionType = ArgumentType;
337 
338         enum isValidConversionFunction(alias f) = 
339             __traits(compiles, { ArgumentType a = f!(ConversionType)("").value; })
340             || __traits(compiles, { ArgumentType a = f("").value; });
341         alias validConversionFunctions = Filter!(isValidConversionFunction, Binders);
342 
343         static if (validConversionFunctions.length == 0)
344             alias getConversionFunction = universalFallbackConverter!ConversionType;
345         else static if(__traits(compiles, Instantiate!(validConversionFunctions[0], ConversionType)))
346             alias getConversionFunction = Instantiate!(validConversionFunctions[0], ConversionType);
347         else
348             alias getConversionFunction = validConversionFunctions[0];
349     }
350     else
351     {
352         alias getConversionFunction = FoundExplicitConverters[0].conversionFunction;
353     }
354 }