1 /// The various datatypes provided by infogen.
2 module jaster.cli.infogen.datatypes;
3 
4 import std.typecons : Flag, Nullable;
5 import jaster.cli.parser, jaster.cli.infogen, jaster.cli.result;
6 
7 /// Used with `Pattern.matchSpacefull`.
8 alias AllowPartialMatch = Flag!"partialMatch";
9 
10 /++
11  + Attach any value from this enum onto an argument to specify what parsing action should be performed on it.
12  + ++/
13 enum CommandArgAction
14 {
15     /// Perform the default parsing action.
16     default_,
17 
18     /++
19      + Increments an argument for every time it is defined inside the parameters.
20      +
21      + Arg Type: Named
22      + Value Type: Any type that supports `++`.
23      + Arg becomes optional: true
24      + ++/
25     count,
26 }
27 
28 /++
29  + Describes the existence of a command argument. i.e., how many times can it appear; is it optional, etc.
30  + ++/
31 enum CommandArgExistence
32 {
33     /// Can only appear once, and is mandatory.
34     default_ = 0,
35 
36     /// Argument can be omitted.
37     optional = 1 << 0,
38 
39     /// Argument can be redefined.
40     multiple = 1 << 1,
41 }
42 
43 /++
44  + Describes the parsing scheme used when parsing the argument's value.
45  + ++/
46 enum CommandArgParseScheme
47 {
48     /// Default parsing scheme.
49     default_,
50 
51     /// Parsing scheme that special cases bools.
52     bool_,
53 
54     /// Allows: -v, -vvvv(n+1). Special case: -vsome_value ignores the "some_value" and leaves it for the next parse cycle.
55     allowRepeatedName
56 }
57 
58 /++
59  + Describes a command and its parameters.
60  +
61  + Params:
62  +  CommandT = The command that this information belongs to.
63  +
64  + See_Also:
65  +  `jaster.cli.infogen.gen.getCommandInfoFor` for generating instances of this struct.
66  + ++/
67 struct CommandInfo(CommandT)
68 {
69     /// The command's `Pattern`, if it has one.
70     Pattern pattern;
71 
72     /// The command's description.
73     string description;
74 
75     /// Information about all of this command's named arguments.
76     NamedArgumentInfo!CommandT[] namedArgs;
77 
78     /// Information about all of this command's positional arguments.
79     PositionalArgumentInfo!CommandT[] positionalArgs;
80 
81     /// Information about this command's raw list argument, if it has one.
82     Nullable!(RawListArgumentInfo!CommandT) rawListArg;
83 }
84 
85 /// The function used to perform an argument's setter action.
86 alias ArgumentActionFunc(CommandT) = Result!void function(string value, ref CommandT commandInstance);
87 
88 /++
89  + Contains information about command's argument.
90  +
91  + Params:
92  +  UDA = The UDA that defines the argument (e.g. `@CommandNamedArg`, `@CommandPositionalArg`)
93  +  CommandT = The command type that this argument belongs to.
94  +
95  + See_Also:
96  +  `jaster.cli.infogen.gen.getCommandInfoFor` for generating instances of this struct.
97  + ++/
98 struct ArgumentInfo(UDA, CommandT)
99 {
100     // NOTE: Do not use Nullable in this struct as it causes compile-time errors.
101     //       It hits a code path that uses memcpy, which of course doesn't work in CTFE.
102 
103     /// The result of `__traits(identifier)` on the argument's symbol.
104     string identifier;
105 
106     /// The UDA attached to the argument's symbol.
107     UDA uda;
108 
109     /// The binding action performed to create the argument's value.
110     CommandArgAction action;
111 
112     /// The user-defined `CommandArgGroup`, this is `.init` for the default group.
113     CommandArgGroup group;
114 
115     /// Describes the existence properties for this argument.
116     CommandArgExistence existence;
117 
118     /// Describes how this argument is to be parsed.
119     CommandArgParseScheme parseScheme;
120 
121     // I wish I could defer this to another part of the library instead of here.
122     // However, any attempt I've made to keep around aliases to parameters has resulted
123     // in a dreaded "Cannot infer type from template arguments CommandInfo!CommandType".
124     // 
125     // My best guesses are: 
126     //  1. More weird behaviour with the hidden context pointer D inserts.
127     //  2. I might've hit some kind of internal template limit that the compiler is just giving a bad message for.
128 
129     /// The function used to perform the binding action for this argument.
130     ArgumentActionFunc!CommandT actionFunc;
131 }
132 
133 alias NamedArgumentInfo(CommandT) = ArgumentInfo!(CommandNamedArg, CommandT);
134 alias PositionalArgumentInfo(CommandT) = ArgumentInfo!(CommandPositionalArg, CommandT);
135 alias RawListArgumentInfo(CommandT) = ArgumentInfo!(CommandRawListArg, CommandT);
136 
137 /++
138  + A pattern is a simple string format for describing multiple "patterns" that can be matched to user provided input.
139  +
140  + Description:
141  +  A simple pattern of "hello" would match, and only match "hello".
142  +
143  +  A pattern of "hello|world" would match either "hello" or "world".
144  +
145  +  Some patterns may contain spaces, other may not, it should be documented if possible.
146  + ++/
147 struct Pattern
148 {
149     import std.algorithm : all;
150     import std.ascii : isWhite;
151 
152     /// The raw pattern string.
153     string pattern;
154 
155     //invariant(pattern.length > 0, "Attempting to use null pattern.");
156 
157     /// Asserts that there is no whitespace within the pattern.
158     void assertNoWhitespace() const
159     {
160         assert(this.pattern.all!(c => !c.isWhite), "The pattern '"~this.pattern~"' is not allowed to contain whitespace.");
161     }
162 
163     /// Returns: An input range consisting of every subpattern within this pattern.
164     auto byEach()
165     {
166         import std.algorithm : splitter;
167         return this.pattern.splitter('|');
168     }
169 
170     /++
171      + The default subpattern can be used as the default 'user-facing' name to display to the user.
172      +
173      + Returns:
174      +  Either the first subpattern, or "DEFAULT" if this pattern is null.
175      + ++/
176     string defaultPattern()
177     {
178         return (this.pattern is null) ? "DEFAULT" : this.byEach.front;
179     }
180 
181     /++
182      + Matches the given input string without splitting up by spaces.
183      +
184      + Params:
185      +  toTestAgainst = The string to test for.
186      +
187      + Returns:
188      +  `true` if there was a match for the given string, `false` otherwise.
189      + ++/
190     bool matchSpaceless(string toTestAgainst)
191     {
192         import std.algorithm : any;
193         return this.byEach.any!(str => str == toTestAgainst);
194     }
195     ///
196     unittest
197     {
198         assert(Pattern("v|verbose").matchSpaceless("v"));
199         assert(Pattern("v|verbose").matchSpaceless("verbose"));
200         assert(!Pattern("v|verbose").matchSpaceless("lalafell"));
201     }
202 
203     /++
204      + Advances the given token parser in an attempt to match with any of this pattern's subpatterns.
205      +
206      + Description:
207      +  On successful or partial match (if `allowPartial` is `yes`) the given `parser` will be advanced to the first
208      +  token that is not part of the match.
209      +
210      +  e.g. For the pattern ("hey there"), if you matched it with the tokens ["hey", "there", "me"], the resulting parser
211      +  would only have ["me"] left.
212      +
213      +  On a failed match, the given parser is left unmodified.
214      +
215      + Bugs:
216      +  If a partial match is allowed, and a partial match is found before a valid full match is found, then only the
217      +  partial match is returned.
218      +
219      + Params:
220      +  parser = The parser to match against.
221      +  allowPartial = If `yes` then allow partial matches, otherwise only allow full matches.
222      +
223      + Returns:
224      +  `true` if there was a full or partial (if allowed) match, otherwise `false`.
225      + ++/
226     bool matchSpacefull(ref ArgPullParser parser, AllowPartialMatch allowPartial = AllowPartialMatch.no)
227     {
228         import std.algorithm : splitter;
229 
230         foreach(subpattern; this.byEach)
231         {
232             auto savedParser = parser.save();
233             bool isAMatch = true;
234             bool isAPartialMatch = false;
235             foreach(split; subpattern.splitter(" "))
236             {
237                 if(savedParser.empty
238                 || !(savedParser.front.type == ArgTokenType.Text && savedParser.front.value == split))
239                 {
240                     isAMatch = false;
241                     break;
242                 }
243 
244                 isAPartialMatch = true;
245                 savedParser.popFront();
246             }
247 
248             if(isAMatch || (isAPartialMatch && allowPartial))
249             {
250                 parser = savedParser;
251                 return true;
252             }
253         }
254 
255         return false;
256     }
257     ///
258     unittest
259     {
260         // Test empty parsers.
261         auto parser = ArgPullParser([]);
262         assert(!Pattern("v").matchSpacefull(parser));
263 
264         // Test that the parser's position is moved forward correctly.
265         parser = ArgPullParser(["v", "verbose"]);
266         assert(Pattern("v").matchSpacefull(parser));
267         assert(Pattern("verbose").matchSpacefull(parser));
268         assert(parser.empty);
269 
270         // Test that a parser that fails to match isn't moved forward at all.
271         parser = ArgPullParser(["v", "verbose"]);
272         assert(!Pattern("lel").matchSpacefull(parser));
273         assert(parser.front.value == "v");
274 
275         // Test that a pattern with spaces works.
276         parser = ArgPullParser(["give", "me", "chocolate"]);
277         assert(Pattern("give me").matchSpacefull(parser));
278         assert(parser.front.value == "chocolate");
279 
280         // Test that multiple patterns work.
281         parser = ArgPullParser(["v", "verbose"]);
282         assert(Pattern("lel|v|verbose").matchSpacefull(parser));
283         assert(Pattern("lel|v|verbose").matchSpacefull(parser));
284         assert(parser.empty);
285     }
286 }