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 }