1 /// Templates for generating information about commands. 2 module jaster.cli.infogen.gen; 3 4 import std.traits, std.meta, std.typecons; 5 import jaster.cli.infogen, jaster.cli.udas, jaster.cli.binder; 6 7 /++ 8 + Generates a `CommandInfo!CommandT` populated with all the information about the command and its arguments. 9 + 10 + Description: 11 + This template can be useful to gather information about a command and its argument, without having any extra baggage attached. 12 + 13 + This allows you to introspect information about the command in the same way as JCLI does. 14 + 15 + Params: 16 + CommandT = The Command to generate the information for. 17 + ArgBinderInstance = The `ArgBinder` to use when generating argument setter functions. 18 + ++/ 19 template getCommandInfoFor(alias CommandT, alias ArgBinderInstance) 20 { 21 static assert(isSomeCommand!CommandT, "Type "~CommandT.stringof~" is not marked with @Command or @CommandDefault."); 22 23 static if(hasUDA!(CommandT, Command)) 24 { 25 enum CommandPattern = getSingleUDA!(CommandT, Command).pattern; 26 enum CommandDescription = getSingleUDA!(CommandT, Command).description; 27 28 static assert(CommandPattern.pattern !is null, "Null pattern names are deprecated, use `@CommandDefault` instead."); 29 } 30 else 31 { 32 enum CommandPattern = Pattern.init; 33 enum CommandDescription = getSingleUDA!(CommandT, CommandDefault).description; 34 } 35 36 enum ArgInfoTuple = toArgInfoArray!(CommandT, ArgBinderInstance); 37 38 enum getCommandInfoFor = CommandInfo!CommandT( 39 CommandPattern, 40 CommandDescription, 41 ArgInfoTuple[0], 42 ArgInfoTuple[1], 43 ArgInfoTuple[2] 44 ); 45 46 enum _dummy = assertGroupDescriptionConsistency!CommandT(getCommandInfoFor); 47 } 48 /// 49 unittest 50 { 51 import std.typecons : Nullable; 52 53 @Command("test", "doe") 54 static struct C 55 { 56 @CommandNamedArg("abc", "123") string arg; 57 @CommandPositionalArg(20, "ray", "me") @CommandArgGroup("nam") Nullable!bool pos; 58 @CommandNamedArg @(CommandArgAction.count) int b; 59 } 60 61 enum info = getCommandInfoFor!(C, ArgBinder!()); 62 static assert(info.pattern.matchSpaceless("test")); 63 static assert(info.description == "doe"); 64 static assert(info.namedArgs.length == 2); 65 static assert(info.positionalArgs.length == 1); 66 67 static assert(info.namedArgs[0].identifier == "arg"); 68 static assert(info.namedArgs[0].uda.pattern.matchSpaceless("abc")); 69 static assert(info.namedArgs[0].action == CommandArgAction.default_); 70 static assert(info.namedArgs[0].group == CommandArgGroup.init); 71 static assert(info.namedArgs[0].existence == CommandArgExistence.default_); 72 static assert(info.namedArgs[0].parseScheme == CommandArgParseScheme.default_); 73 74 static assert(info.positionalArgs[0].identifier == "pos"); 75 static assert(info.positionalArgs[0].uda.position == 20); 76 static assert(info.positionalArgs[0].action == CommandArgAction.default_); 77 static assert(info.positionalArgs[0].group.name == "nam"); 78 static assert(info.positionalArgs[0].existence == CommandArgExistence.optional); 79 static assert(info.positionalArgs[0].parseScheme == CommandArgParseScheme.bool_); 80 81 static assert(info.namedArgs[1].action == CommandArgAction.count); 82 } 83 84 private auto toArgInfoArray(alias CommandT, alias ArgBinderInstance)() 85 { 86 import std.typecons : tuple; 87 88 alias NamedArgs = getNamedArguments!CommandT; 89 alias PositionalArgs = getPositionalArguments!CommandT; 90 91 auto namedArgs = new NamedArgumentInfo!CommandT[NamedArgs.length]; 92 auto positionalArgs = new PositionalArgumentInfo!CommandT[PositionalArgs.length]; 93 typeof(CommandInfo!CommandT.rawListArg) rawListArg; 94 95 static foreach(i, ArgT; NamedArgs) 96 namedArgs[i] = getArgInfoFor!(CommandT, ArgT, ArgBinderInstance); 97 static foreach(i, ArgT; PositionalArgs) 98 positionalArgs[i] = getArgInfoFor!(CommandT, ArgT, ArgBinderInstance); 99 100 alias RawListArgSymbols = getSymbolsByUDA!(CommandT, CommandRawListArg); 101 static if(RawListArgSymbols.length > 0) 102 { 103 static assert(RawListArgSymbols.length == 1, "Only one argument can be marked with @CommandRawListArg"); 104 static assert(!isSomeArgument!(RawListArgSymbols[0]), "@CommandRawListArg is mutually exclusive to the other command UDAs."); 105 rawListArg = getArgInfoFor!(CommandT, RawListArgSymbols[0], ArgBinderInstance); 106 } 107 108 return tuple(namedArgs, positionalArgs, rawListArg); 109 } 110 111 private template getArgInfoFor(alias CommandT, alias ArgT, alias ArgBinderInstance) 112 { 113 // Determine argument info type. 114 static if(isNamedArgument!ArgT) 115 alias ArgInfoT = NamedArgumentInfo!CommandT; 116 else static if(isPositionalArgument!ArgT) 117 alias ArgInfoT = PositionalArgumentInfo!CommandT; 118 else static if(isRawListArgument!ArgT) 119 alias ArgInfoT = RawListArgumentInfo!CommandT; 120 else 121 static assert(false, "Type "~ArgT~" cannot be recognised as an argument."); 122 123 // Find what action to use. 124 enum isActionUDA(alias UDA) = is(typeof(UDA) == CommandArgAction); 125 enum ActionUDAs = Filter!(isActionUDA, __traits(getAttributes, ArgT)); 126 static if(ActionUDAs.length == 0) 127 enum Action = CommandArgAction.default_; 128 else static if(ActionUDAs.length == 1) 129 enum Action = ActionUDAs[0]; 130 else 131 static assert(false, "Multiple `CommandArgAction` UDAs detected for argument "~ArgT.stringof); 132 alias ActionFunc = actionFuncFromAction!(Action, CommandT, ArgT, ArgBinderInstance); 133 134 // Get the arg group if one is assigned. 135 static if(hasUDA!(ArgT, CommandArgGroup)) 136 enum Group = getSingleUDA!(ArgT, CommandArgGroup); 137 else 138 enum Group = CommandArgGroup.init; 139 140 // Determine existence and parse scheme traits. 141 enum Existence = determineExistence!(CommandT, typeof(ArgT), Action); 142 enum Scheme = determineParseScheme!(CommandT, ArgT, Action); 143 144 enum getArgInfoFor = ArgInfoT( 145 __traits(identifier, ArgT), 146 getSingleUDA!(ArgT, typeof(ArgInfoT.uda)), 147 Action, 148 Group, 149 Existence, 150 Scheme, 151 &ActionFunc 152 ); 153 } 154 155 private template actionFuncFromAction(CommandArgAction Action, alias CommandT, alias ArgT, alias ArgBinderInstance) 156 { 157 import std.conv; 158 159 static if(isRawListArgument!ArgT) 160 alias actionFuncFromAction = dummyAction!CommandT; 161 else static if(Action == CommandArgAction.default_) 162 alias actionFuncFromAction = actionValueBind!(CommandT, ArgT, ArgBinderInstance); 163 else static if(Action == CommandArgAction.count) 164 alias actionFuncFromAction = actionCount!(CommandT, ArgT, ArgBinderInstance); 165 else 166 { 167 pragma(msg, Action.to!string); 168 pragma(msg, CommandT); 169 pragma(msg, __traits(identifier, ArgT)); 170 pragma(msg, ArgBinderInstance); 171 static assert(false, "No suitable action found."); 172 } 173 } 174 175 private CommandArgExistence determineExistence(alias CommandT, alias ArgTType, CommandArgAction Action)() 176 { 177 import std.typecons : Nullable; 178 179 CommandArgExistence value; 180 181 static if(isInstanceOf!(Nullable, ArgTType)) 182 value |= CommandArgExistence.optional; 183 static if(Action == CommandArgAction.count) 184 { 185 value |= CommandArgExistence.multiple; 186 value |= CommandArgExistence.optional; 187 } 188 189 return value; 190 } 191 192 private template determineParseScheme(alias CommandT, alias ArgT, CommandArgAction Action) 193 { 194 import std.typecons : Nullable; 195 196 static if(is(typeof(ArgT) == bool) || is(typeof(ArgT) == Nullable!bool)) 197 enum determineParseScheme = CommandArgParseScheme.bool_; 198 else static if(Action == CommandArgAction.count) 199 enum determineParseScheme = CommandArgParseScheme.allowRepeatedName; 200 else 201 enum determineParseScheme = CommandArgParseScheme.default_; 202 } 203 204 private bool assertGroupDescriptionConsistency(alias CommandT)(CommandInfo!CommandT info) 205 { 206 import std.algorithm : map, filter, all, uniq, joiner; 207 import std.range : chain; 208 import std.conv : to; 209 210 auto groups = info.namedArgs 211 .map!(a => a.group).chain( 212 info.positionalArgs 213 .map!(a => a.group) 214 ); 215 auto uniqueGroupNames = groups.map!(g => g.name).uniq; 216 217 foreach(name; uniqueGroupNames) 218 { 219 auto descriptionsForGroupName = groups.filter!(g => g.name == name).map!(g => g.description); 220 auto firstNonNullDescription = descriptionsForGroupName.filter!(d => d !is null); 221 if(firstNonNullDescription.empty) 222 continue; 223 224 const canonDescription = firstNonNullDescription.front; 225 assert( 226 descriptionsForGroupName.all!(d => d is null || d == canonDescription), 227 "Group '"~name~"' has multiple conflicting descriptions. Canon description is '"~canonDescription~"' but the following conflicts were found: " 228 ~"[" 229 ~descriptionsForGroupName.filter!(d => d !is null && d != canonDescription).map!(d => `"`~d~`"`).joiner(" <-> ").to!string 230 ~"]" 231 ); 232 } 233 234 return true; // Dummy value, just so I can do enum assignment 235 }