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 }