1 /// Functionality for defining and resolving command "sentences".
2 module jaster.cli.resolver;
3 
4 import std.range;
5 import jaster.cli.parser;
6 
7 /// The type of a `CommandNode`.
8 enum CommandNodeType
9 {
10     /// Failsafe
11     ERROR,
12 
13     /// Used for the root `CommandNode`.
14     root,
15 
16     /// Used for `CommandNodes` that don't contain a command, but instead contain child `CommandNodes`.
17     ///
18     /// e.g. For "build all libraries", "build" and "all" would be `partialWords`.
19     partialWord,
20 
21     /// Used for `CommandNodes` that contain a command.
22     ///
23     /// e.g. For "build all libraries", "libraries" would be a `finalWord`.
24     finalWord
25 }
26 
27 /++
28  + The result of a command resolution attempt.
29  +
30  + Params:
31  +  UserDataT = See `CommandResolver`'s documentation.
32  + ++/
33 struct CommandResolveResult(UserDataT)
34 {
35     /// Whether the resolution was successful (true) or not (false).
36     bool success;
37 
38     /// The resolved `CommandNode`. This value is undefined when `success` is `false`.
39     CommandNode!UserDataT value;
40 }
41 
42 /++
43  + Contains a single "word" within a command "sentence", see `CommandResolver`'s documentation for more.
44  +
45  + Params:
46  +  UserDataT = See `CommandResolver`'s documentation.
47  + ++/
48 @safe
49 struct CommandNode(UserDataT)
50 {
51     /// The word this node contains.
52     string word;
53 
54     /// What type of node this is.
55     CommandNodeType type;
56 
57     /// The children of this node.
58     CommandNode!UserDataT[] children;
59 
60     /// User-provided data for this node. Note that partial words don't contain any user data.
61     UserDataT userData;
62 
63     /// A string of the entire sentence up to (and including) this word, please note that $(B currently only final words) have this field set.
64     string sentence;
65 
66     /// See_Also: `CommandResolver.resolve`
67     CommandResolveResult!UserDataT byCommandSentence(RangeOfStrings)(RangeOfStrings range)
68     {        
69         auto current = this;
70         for(; !range.empty; range.popFront())
71         {
72             auto commandWord = range.front;
73             auto currentBeforeChange = current;
74 
75             foreach(child; current.children)
76             {
77                 if(child.word == commandWord)
78                 {
79                     current = child;
80                     break;
81                 }
82             }
83 
84             // Above loop failed.
85             if(currentBeforeChange.word == current.word)
86             {
87                 current = this; // Makes result.success become false.
88                 break;
89             }
90         }
91 
92         typeof(return) result;
93         result.value   = current;
94         result.success = range.empty && current.word != this.word;
95         return result;
96     }
97     
98     /++
99      + Retrieves all child `CommandNodes` that are of type `CommandNodeType.finalWord`.
100      +
101      + Notes:
102      +  While similar to `CommandResolver.finalWords`, this function has one major difference.
103      +
104      +  It is less efficient, since `CommandResolver.finalWords` builds and caches its value whenever a sentence is defined, while
105      +  this function (currently) has to recreate its value each time.
106      +
107      +  Furthermore, as with `CommandResolver.finalWords`, the returned array of nodes are simply copies of the actual nodes used
108      +  and returned by `CommandResolver.resolve`. So don't expect any changes to be reflected anywhere. 
109      +
110      +  Technically the same could be done here, but I'm lazy, so for now you get extra GC garbage.
111      +
112      + Returns:
113      +  All child final words.
114      + ++/
115     CommandNode!UserDataT[] finalWords()
116     {
117         if(this.type != CommandNodeType.partialWord && this.type != CommandNodeType.root)
118             return null;
119 
120         typeof(return) nodes;
121 
122         void addFinalNodes(CommandNode!UserDataT repetitionNode)
123         {
124             foreach(childNode; repetitionNode.children)
125             {
126                 if(childNode.type == CommandNodeType.finalWord)
127                     nodes ~= childNode;
128                 else if(childNode.type == CommandNodeType.partialWord)
129                     addFinalNodes(childNode);
130                 else
131                     assert(false, "Malformed tree.");
132             }
133         }
134 
135         addFinalNodes(this);
136         return nodes;
137     }
138 }
139 
140 /++
141  + A helper class where you can define command "sentences", and then resolve (either partially or fully) commands
142  + from "sentences" provided by the user.
143  +
144  + Params:
145  +  UserDataT = User-provided data for each command (`CommandNodes` of type `CommandNodeType.finalWord`).
146  +
147  + Description:
148  +  In essence, this class is just an abstraction around a basic tree structure (`CommandNode`), to make it easy to
149  +  both define and search the tree.
150  +
151  +  First of all, JCLI supports commands having multiple "words" within them, such as "build all libs"; "remote get-url", etc.
152  +  This entire collection of "words" is referred to as a "sentence".
153  +
154  +  The tree for the resolver consists of words pointing to any following words (`CommandNodeType.partialWord`), ultimately ending each
155  +  branch with the final command word (`CommandNodeType.finalWord`).
156  +
157  +  For example, if we had the following commands "build libs"; "build apps", and "test libs", the tree would look like the following.
158  +
159  +  Legend = `[word] - partial word` and `<word> - final word`.
160  +
161  +```
162  +         root
163  +         /  \
164  +    [test]  [build]
165  +      |       |    \  
166  +    <libs>  <libs>  <apps>
167  +```
168  +
169  +  Because this class only handles resolving commands, and nothing more than that, the application can attach whatever data it wants (`UserDataT`)
170  +  so it can later perform its own processing (description; arg info; execution delegates, etc.)
171  +
172  +  I'd like to point out however, $(B only final words) are given user data as partial words aren't supposed to represent commands.
173  +
174  +  Finally, given the above point, if you tried to define "build release" and "build" at the same time, you'd fail an assert as "build" cannot be
175  +  a partial word and a final word at the same time. This does kind of suck in some cases, but there are workarounds e.g. defining "build", then passing "release"/"debug"
176  +  as arguments.
177  +
178  + Usage:
179  +  Build up your tree by using `CommandResolver.define`.
180  +
181  +  Resolve commands via `CommandResolver.resolve` or `CommandResolver.resolveAndAdvance`.
182  + ++/
183 @safe
184 final class CommandResolver(UserDataT)
185 {
186     /// The `CommandNode` instatiation for this resolver.
187     alias NodeT = CommandNode!UserDataT;
188 
189     private
190     {
191         CommandNode!UserDataT _rootNode;
192         string[]              _sentences;
193         NodeT[]               _finalWords;
194     }
195 
196     this()
197     {
198         this._rootNode.type = CommandNodeType.root;
199     }
200 
201     /++
202      + Defines a command sentence.
203      +
204      + Description:
205      +  A "sentence" consists of multiple "words". A "word" is a string of characters, each seperated by any amount of spaces.
206      +
207      +  For instance, `"build all libs"` contains the words `["build", "all", "libs"]`.
208      +
209      +  The last word within a sentence is known as the final word (`CommandNodeType.finalWord`), which is what defines the
210      +  actual command associated with this sentence. The final word is the only word that has the `userDataForFinalNode` associated with it.
211      +
212      +  The rest of the words are known as partial words (`CommandNodeType.partialWord`) as they are only a partial part of a full sentence.
213      +  (I hate all of this as well, don't worry).
214      +
215      +  So for example, if you wanted to define the command "build all libs" with some custom user data containing, for example, a description and
216      +  an execute function, you could do something such as.
217      +
218      +  `myResolver.define("build all libs", MyUserData("Builds all Libraries", &buildAllLibsCommand))`
219      +
220      +  You can then later use `CommandResolver.resolve` or `CommandResolver.resolveAndAdvance`, using a user-provided string, to try and resolve
221      +  to the final command.
222      +
223      + Params:
224      +  commandSentence      = The sentence to define.
225      +  userDataForFinalNode = The `UserDataT` to attach to the `CommandNode` for the sentence's final word.
226      + ++/
227     void define(string commandSentence, UserDataT userDataForFinalNode)
228     {
229         import std.algorithm : splitter, filter, any, countUntil;
230         import std.format    : format; // For errors.
231         import std.range     : walkLength;
232         import std.uni       : isWhite;
233 
234         auto words = commandSentence.splitter!(a => a == ' ').filter!(w => w.length > 0);
235         assert(!words.any!(w => w.any!isWhite), "Words inside a command sentence cannot contain whitespace.");
236 
237         const wordCount   = words.walkLength;
238         scope currentNode = &this._rootNode;
239         size_t wordIndex  = 0;
240         foreach(word; words)
241         {
242             const isLastWord = (wordIndex == wordCount - 1);
243             wordIndex++;
244 
245             const existingNodeIndex = currentNode.children.countUntil!(c => c.word == word);
246 
247             NodeT node;
248             node.word     = word;
249             node.type     = (isLastWord) ? CommandNodeType.finalWord : CommandNodeType.partialWord;
250             node.userData = (isLastWord) ? userDataForFinalNode : UserDataT.init;
251             node.sentence = (isLastWord) ? commandSentence : null;
252 
253             if(isLastWord)
254                 this._finalWords ~= node;
255 
256             if(existingNodeIndex == -1)
257             {
258                 currentNode.children ~= node;
259                 currentNode = &currentNode.children[$-1];
260                 continue;
261             }
262             
263             currentNode = &currentNode.children[existingNodeIndex];
264             assert(
265                 currentNode.type == CommandNodeType.partialWord, 
266                 "Cannot append word '%s' onto word '%s' as the latter word is not a partialWord, but instead a %s."
267                 .format(word, currentNode.word, currentNode.type)
268             );
269         }
270 
271         this._sentences ~= commandSentence;
272     }
273     
274     /++
275      + Attempts to resolve a range of words/a sentence into a `CommandNode`.
276      +
277      + Notes:
278      +  The overload taking a `string` will split the string by spaces, the same way `CommandResolver.define` works.
279      +
280      + Description:
281      +  There are three potential outcomes of this function.
282      +
283      +  1. The words provided fully match a command sentence. The value of `returnValue.value.type` will be `CommandNodeType.finalWord`.
284      +  2. The words provided a partial match of a command sentence. The value of `returnValue.value.type` will be `CommandNodeType.partialWord`.
285      +  3. Neither of the above. The value of `returnValue.success` will be `false`.
286      +
287      +  How you handle these outcomes, and which ones you handle, are entirely up to your application.
288      +
289      + Params:
290      +  words = The words to resolve.
291      +
292      + Returns:
293      +  A `CommandResolveResult`, specifying the result of the resolution.
294      + ++/
295     CommandResolveResult!UserDataT resolve(RangeOfStrings)(RangeOfStrings words)
296     {
297         return this._rootNode.byCommandSentence(words);
298     }
299 
300     /// ditto.
301     CommandResolveResult!UserDataT resolve(string sentence) pure
302     {
303         import std.algorithm : splitter, filter;
304         return this.resolve(sentence.splitter(' ').filter!(w => w.length > 0));
305     }
306 
307     /++
308      + Peforms the same task as `CommandResolver.resolve`, except that it will also advance the given `parser` to the
309      + next unparsed argument.
310      +
311      + Description:
312      +  For example, you've defined `"set verbose"` as a command, and you pass in an `ArgPullParser(["set", "verbose", "true"])`.
313      +
314      +  This function will match with the `"set verbose"` sentence, and will advance the parser so that it will now be `ArgPullParser(["true"])`, ready
315      +  for your application code to perform additional processing (e.g. arguments).
316      +
317      + Params:
318      +  parser = The `ArgPullParser` to use and advance.
319      +
320      + Returns:
321      +  Same thing as `CommandResolver.resolve`.
322      + ++/
323     CommandResolveResult!UserDataT resolveAndAdvance(ref ArgPullParser parser)
324     {
325         import std.algorithm : map;
326         import std.range     : take;
327 
328         typeof(return) lastSuccessfulResult;
329         
330         auto   parserCopy   = parser;
331         size_t amountToTake = 0;
332         while(true)
333         {
334             if(parser.empty || parser.front.type != ArgTokenType.Text)
335                 return lastSuccessfulResult;
336 
337             auto result = this.resolve(parserCopy.take(++amountToTake).map!(t => t.value));
338             if(!result.success)
339                 return lastSuccessfulResult;
340 
341             lastSuccessfulResult = result;
342             parser.popFront();
343         }
344     }
345     
346     /// Returns: The root `CommandNode`, for whatever you need it for.
347     @property
348     NodeT root()
349     {
350         return this._rootNode;
351     }
352 
353     /++
354      + Notes:
355      +  While the returned array is mutable, the nodes stored in this array are *not* the same nodes stored in the actual search tree.
356      +  This means that any changes made to this array will not be reflected by the results of `resolve` and `resolveAndAdvance`.
357      +
358      +  The reason this isn't marked `const` is because that'd mean that your user data would also be marked `const`, which, in D,
359      +  can be *very* annoying and limiting. Doubly so since your intentions can't be determined due to the nature of user data. So behave with this.
360      +
361      + Returns:
362      +  All of the final words currently defined.
363      + ++/
364     @property
365     NodeT[] finalWords()
366     {
367         return this._finalWords;
368     }
369 }
370 ///
371 @("Main test for CommandResolver")
372 @safe
373 unittest
374 {
375     import std.algorithm : map, equal;
376 
377     // Define UserData as a struct containing an execution method. Define a UserData which toggles a value.
378     static struct UserData
379     {
380         void delegate() @safe execute;
381     }
382 
383     bool executeValue;
384     void toggleValue() @safe
385     {
386         executeValue = !executeValue;
387     }
388 
389     auto userData = UserData(&toggleValue);
390 
391     // Create the resolver and define three command paths: "toggle", "please toggle", and "please tog".
392     // Tree should look like:
393     //       [root]
394     //      /      \
395     // toggle       please
396     //             /      \
397     //          toggle    tog
398     auto resolver = new CommandResolver!UserData;
399     resolver.define("toggle", userData);
400     resolver.define("please toggle", userData);
401     resolver.define("please tog", userData);
402 
403     // Resolve 'toggle' and call its execute function.
404     auto result = resolver.resolve("toggle");
405     assert(result.success);
406     assert(result.value.word == "toggle");
407     assert(result.value.sentence == "toggle");
408     assert(result.value.type == CommandNodeType.finalWord);
409     assert(result.value.userData.execute !is null);
410     result.value.userData.execute();
411     assert(executeValue == true);
412 
413     // Resolve 'please' and confirm that it's only a partial match.
414     result = resolver.resolve("please");
415     assert(result.success);
416     assert(result.value.word            == "please");
417     assert(result.value.sentence        is null);
418     assert(result.value.type            == CommandNodeType.partialWord);
419     assert(result.value.children.length == 2);
420     assert(result.value.userData        == UserData.init);
421     
422     // Resolve 'please toggle' and call its execute function.
423     result = resolver.resolve("please toggle");
424     assert(result.success);
425     assert(result.value.word == "toggle");
426     assert(result.value.sentence == "please toggle");
427     assert(result.value.type == CommandNodeType.finalWord);
428     result.value.userData.execute();
429     assert(executeValue == false);
430 
431     // Resolve 'please tog' and call its execute function. (to test nodes with multiple children).
432     result = resolver.resolve("please tog");
433     assert(result.success);
434     assert(result.value.word == "tog");
435     assert(result.value.sentence == "please tog");
436     assert(result.value.type == CommandNodeType.finalWord);
437     result.value.userData.execute();
438     assert(executeValue == true);
439 
440     // Resolve a few non-existing command sentences, and ensure that they were unsuccessful.
441     assert(!resolver.resolve(null).success);
442     assert(!resolver.resolve("toggle please").success);
443     assert(!resolver.resolve("He she we, wombo.").success);
444 
445     // Test that final words are properly tracked.
446     assert(resolver.finalWords.map!(w => w.word).equal(["toggle", "toggle", "tog"]));
447     assert(resolver.root.finalWords.equal(resolver.finalWords));
448 
449     auto node = resolver.resolve("please").value;
450     assert(node.finalWords().map!(w => w.word).equal(["toggle", "tog"]));
451 }
452 
453 @("Test CommandResolver.resolveAndAdvance")
454 @safe
455 unittest
456 {
457     // Resolution should stop once a non-Text argument is found "--c" in this case.
458     // Also the parser should be advanced, where .front is the argument that wasn't part of the resolved command.
459     auto resolver = new CommandResolver!int();
460     auto parser   = ArgPullParser(["a", "b", "--c", "-d", "e"]);
461 
462     resolver.define("a b e", 0);
463 
464     auto parserCopy = parser;
465     auto result     = resolver.resolveAndAdvance(parserCopy);
466     assert(result.success);
467     assert(result.value.type == CommandNodeType.partialWord);
468     assert(result.value.word == "b");
469     assert(parserCopy.front.value == "c", parserCopy.front.value);
470 }
471 
472 @("Test CommandResolver.resolve possible edge case")
473 @safe
474 unittest
475 {
476     auto resolver = new CommandResolver!int();
477     auto parser   = ArgPullParser(["set", "value", "true"]);
478     
479     resolver.define("set value", 0);
480 
481     auto result = resolver.resolveAndAdvance(parser);
482     assert(result.success);
483     assert(result.value.type == CommandNodeType.finalWord);
484     assert(result.value.word == "value");
485     assert(parser.front.value == "true");
486 
487     result = resolver.resolve("set verbose true");
488     assert(!result.success);
489 }