1 module jcli.resolver.resolver;
2 
3 import std.conv : to;
4 
5 alias ResolveValueProvider = string[] delegate(string[] currArgs);
6 
7 struct ResolveResult(alias UserDataT)
8 {
9     enum Kind
10     {
11         full,
12         partial
13     }
14 
15     Kind kind;
16     ResolveNode!UserDataT*[] fullMatchChain;
17     ResolveNode!UserDataT*[] partialMatches;
18     ResolveValueProvider valueProvider;
19 }
20 
21 struct ResolveNode(alias UserDataT)
22 {
23     char letter;
24     bool isFullMatch;
25     string fullMatchString;
26     ResolveNode[] children;
27     UserDataT userData;
28     ResolveValueProvider valueProvider;
29 }
30 
31 alias t = Resolver!int;
32 
33 final class Resolver(alias UserDataT_)
34 {
35     alias UserDataT = UserDataT_;
36 
37     static struct ArgInfo
38     {
39         string arg;
40         ResolveValueProvider valueProvider;
41     }
42 
43     private
44     {
45         ResolveNode!UserDataT _root;
46     }
47 
48     @trusted nothrow
49     void add(
50         string[] command,
51         UserDataT userData,
52         ResolveValueProvider commandValueProvider
53     )
54     {
55         ResolveNode!UserDataT* node = &this._root;
56         bool _1;
57         foreach(i, word; command)
58         {
59             foreach(ch; word)
60                 node = &this.nextByChar(*node, ch, true, _1);
61             node.fullMatchString = word;
62             node.isFullMatch = true;
63             if(i+1 < command.length)
64                 node = &this.nextByChar(*node, ' ', true, _1);
65         }
66 
67         node.valueProvider = commandValueProvider;
68         node.userData = userData;
69     }
70 
71     ResolveResult!UserDataT resolve(
72         string[] command
73     )
74     {
75         ResolveResult!UserDataT ret;
76 
77         ResolveNode!UserDataT* node = &this._root;
78         bool found;
79         foreach(i, word; command)
80         {
81             foreach(ch; word)
82             {
83                 node = &this.nextByChar(*node, ch, false, found);
84                 if(!found)
85                     break;
86             }
87             if(node.isFullMatch)
88                 ret.fullMatchChain ~= node;
89             if(i+1 < command.length)
90             {
91                 node = &this.nextByChar(*node, ' ', false, found);
92                 if(!found)
93                     break;
94             }
95         }
96 
97         if(found && node.isFullMatch && node.userData != UserDataT.init)
98         {
99             ret.kind = ret.Kind.full;
100             if(node.valueProvider)
101                 ret.valueProvider = node.valueProvider;
102             return ret;
103         }
104 
105         void addToPartial(ResolveNode!UserDataT* node)
106         {
107             foreach(ref child; node.children)
108             {
109                 if(child.letter == ' ')
110                     continue;
111                 if(child.isFullMatch)
112                     ret.partialMatches ~= &child;
113                 addToPartial(&child);
114             }
115         }
116 
117         addToPartial(node);
118         ret.kind = ret.Kind.partial;
119         return ret;
120     }
121 
122     @safe nothrow
123     private ref NodeT nextByChar(NodeT)(ref NodeT parent, char ch, bool createIfNeeded, out bool found)
124     {
125         found = true;
126         foreach(ref node; parent.children)
127         {
128             if(node.letter == ch)
129                 return node;
130         }
131 
132         if(createIfNeeded)
133         {
134             parent.children ~= NodeT(ch);
135             return parent.children[$-1];
136         }
137         else
138         {
139             found = false;
140             static if(is(NodeT == ResolveNode!UserDataT))
141                 return this._root;
142             else
143             {
144                 static ResolveArgument _r;
145                 return _r;
146             }
147         }
148     }
149 }
150 
151 unittest
152 {
153     auto r = new Resolver!int();
154     r.add(["cloaca"], 20, null);
155     auto re = r.resolve(["cloaca"]);
156 
157     assert(re.kind == re.Kind.full);
158     assert(re.fullMatchChain.length == 1);
159     assert(re.fullMatchChain[0].fullMatchString == "cloaca");
160 }
161 
162 unittest
163 {
164     auto r = new Resolver!int();
165     r.add(["cloaca", "knuckles"], 20, null);
166     auto re = r.resolve(["cloaca", "knuckles"]);
167 
168     assert(re.kind == re.Kind.full);
169     assert(re.fullMatchChain.length == 2);
170     assert(re.fullMatchChain[0].fullMatchString == "cloaca");
171     assert(re.fullMatchChain[1].fullMatchString == "knuckles");
172 }
173 
174 unittest
175 {
176     auto r = new Resolver!int();
177     r.add(["cloaca"], 20, null);
178     auto re = r.resolve(["cloa"]);
179 
180     assert(re.kind == re.Kind.partial);
181     assert(re.partialMatches.length == 1);
182     assert(re.partialMatches[0].fullMatchString == "cloaca", re.partialMatches[0].fullMatchString);
183 }
184 
185 unittest
186 {
187     auto r = new Resolver!int();
188     r.add(["nodders"], 20, null);
189     auto re = r.resolve(["nop"]);
190 
191     assert(re.kind == re.Kind.partial, re.to!string);
192     assert(re.partialMatches.length == 1);
193     assert(re.partialMatches[0].fullMatchString == "nodders", re.partialMatches[0].fullMatchString);
194 }