Skip to content

Commit 9a04d5b

Browse files
Use Related metadata for autocomplete API discovery (#214)
* feat: use Related metadata for autocomplete API discovery * test: add autocomplete API discovery coverage * Use Related metadata only when the noun heuristics find nothing Checking Related before the heuristics changed 47 existing completions against the bundled API cache, many of them wrong: registerIso projectid resolved to listProjectAccounts instead of listProjects, acquirePodIpAddress podid to listZones instead of listPods, and executeWebhookDelivery webhookid to listWebhookDeliveries instead of listWebhooks. The loose Related match picks the first list API in the array, whatever its noun. Move the Related lookup after the noun heuristics and run it only when they found no API. That keeps all 142 new completions Related metadata adds and leaves every existing completion untouched. Add a test pinning the ordering. --------- Co-authored-by: Boris Stoyanov <bss.stoyanov@gmail.com>
1 parent ae66522 commit 9a04d5b

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

cli/completer.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,22 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st
287287
relatedNoun = relatedNoun[:len(relatedNoun)-1]
288288
}
289289

290+
// Prefer the API's own Related metadata when the noun heuristics found
291+
// nothing, so entity-reference args still get completions.
292+
if autocompleteAPI == nil {
293+
for _, relatedAPI := range arg.Related {
294+
if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") {
295+
continue
296+
}
297+
for _, listAPI := range apiMap["list"] {
298+
if strings.EqualFold(listAPI.Name, relatedAPI) {
299+
config.Debug("Autocomplete: API found using Related metadata: ", listAPI.Name)
300+
return listAPI
301+
}
302+
}
303+
}
304+
}
305+
290306
// Heuristic: find any list API that contains the arg name
291307
if autocompleteAPI == nil {
292308
config.Debug("Finding possible API that have: ", argName, " related APIs: ", arg.Related)

cli/completer_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/apache/cloudstack-cloudmonkey/config"
7+
)
8+
9+
func TestFindAutocompleteAPIRelatedNounMatch(t *testing.T) {
10+
arg := &config.APIArg{
11+
Name: "domainid=",
12+
Related: []string{
13+
"createDomain",
14+
"listDomains",
15+
"updateDomain",
16+
},
17+
}
18+
19+
apiFound := &config.API{
20+
Name: "listVirtualMachines",
21+
Verb: "list",
22+
Noun: "virtualmachines",
23+
}
24+
25+
apiMap := map[string][]*config.API{
26+
"list": {
27+
{
28+
Name: "listDomains",
29+
Noun: "domains",
30+
},
31+
},
32+
}
33+
34+
result := findAutocompleteAPI(arg, apiFound, apiMap)
35+
36+
if result == nil {
37+
t.Fatal("expected API, got nil")
38+
}
39+
40+
if result.Name != "listDomains" {
41+
t.Fatalf("expected listDomains, got %s", result.Name)
42+
}
43+
}
44+
45+
func TestFindAutocompleteAPIRelatedFallback(t *testing.T) {
46+
arg := &config.APIArg{
47+
Name: "domainid=",
48+
Related: []string{
49+
"listDomainChildren",
50+
},
51+
}
52+
53+
apiFound := &config.API{
54+
Name: "listVirtualMachines",
55+
Verb: "list",
56+
Noun: "virtualmachines",
57+
}
58+
59+
apiMap := map[string][]*config.API{
60+
"list": {
61+
{
62+
Name: "listDomainChildren",
63+
Noun: "domainchildren",
64+
},
65+
},
66+
}
67+
68+
result := findAutocompleteAPI(arg, apiFound, apiMap)
69+
70+
if result == nil {
71+
t.Fatal("expected API, got nil")
72+
}
73+
74+
if result.Name != "listDomainChildren" {
75+
t.Fatalf("expected listDomainChildren, got %s", result.Name)
76+
}
77+
}
78+
79+
func TestFindAutocompleteAPIEmptyRelatedFallsBackToHeuristic(t *testing.T) {
80+
arg := &config.APIArg{
81+
Name: "zoneid=",
82+
}
83+
84+
apiFound := &config.API{
85+
Name: "listVirtualMachines",
86+
Verb: "list",
87+
Noun: "virtualmachines",
88+
}
89+
90+
apiMap := map[string][]*config.API{
91+
"list": {
92+
{
93+
Name: "listZones",
94+
Noun: "zones",
95+
},
96+
},
97+
}
98+
99+
result := findAutocompleteAPI(arg, apiFound, apiMap)
100+
101+
if result == nil {
102+
t.Fatal("expected API, got nil")
103+
}
104+
105+
if result.Name != "listZones" {
106+
t.Fatalf("expected listZones, got %s", result.Name)
107+
}
108+
}
109+
110+
func TestFindAutocompleteAPINonListRelatedFallsBackToHeuristic(t *testing.T) {
111+
arg := &config.APIArg{
112+
Name: "zoneid=",
113+
Related: []string{
114+
"createZone",
115+
"updateZone",
116+
},
117+
}
118+
119+
apiFound := &config.API{
120+
Name: "listVirtualMachines",
121+
Verb: "list",
122+
Noun: "virtualmachines",
123+
}
124+
125+
apiMap := map[string][]*config.API{
126+
"list": {
127+
{
128+
Name: "listZones",
129+
Noun: "zones",
130+
},
131+
},
132+
}
133+
134+
result := findAutocompleteAPI(arg, apiFound, apiMap)
135+
136+
if result == nil {
137+
t.Fatal("expected API, got nil")
138+
}
139+
140+
if result.Name != "listZones" {
141+
t.Fatalf("expected listZones, got %s", result.Name)
142+
}
143+
}
144+
145+
func TestFindAutocompleteAPIMapTypeReturnsNil(t *testing.T) {
146+
arg := &config.APIArg{
147+
Type: "map",
148+
}
149+
150+
apiFound := &config.API{
151+
Name: "listVirtualMachines",
152+
Verb: "list",
153+
Noun: "virtualmachines",
154+
}
155+
156+
apiMap := map[string][]*config.API{
157+
"list": {},
158+
}
159+
160+
result := findAutocompleteAPI(arg, apiFound, apiMap)
161+
162+
if result != nil {
163+
t.Fatalf("expected nil, got %v", result)
164+
}
165+
}
166+
167+
func TestFindAutocompleteAPIHeuristicWinsOverRelated(t *testing.T) {
168+
// registerIso's projectid arg lists many related APIs; the noun heuristic
169+
// must keep winning so the completion stays listProjects.
170+
arg := &config.APIArg{
171+
Name: "projectid=",
172+
Related: []string{
173+
"listProjectAccounts",
174+
"listProjects",
175+
},
176+
}
177+
178+
apiFound := &config.API{
179+
Name: "registerIso",
180+
Verb: "register",
181+
Noun: "iso",
182+
}
183+
184+
apiMap := map[string][]*config.API{
185+
"list": {
186+
{
187+
Name: "listProjectAccounts",
188+
Noun: "projectaccounts",
189+
},
190+
{
191+
Name: "listProjects",
192+
Noun: "projects",
193+
},
194+
},
195+
}
196+
197+
result := findAutocompleteAPI(arg, apiFound, apiMap)
198+
199+
if result == nil {
200+
t.Fatal("expected API, got nil")
201+
}
202+
203+
if result.Name != "listProjects" {
204+
t.Fatalf("expected listProjects, got %s", result.Name)
205+
}
206+
}

0 commit comments

Comments
 (0)