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 }