From 33e214afc80dbe8587c9f0b5e26176f91affe2ab Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Mon, 8 Jun 2026 19:55:24 +0000 Subject: [PATCH 1/4] feat: use Related metadata for autocomplete API discovery --- cli/completer.go | 85 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/cli/completer.go b/cli/completer.go index 755cc7f..bf730cd 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -214,29 +214,33 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st } var autocompleteAPI *config.API + argName := strings.Replace(arg.Name, "=", "", -1) - relatedNoun := argName + + // Build the noun we expect from the existing heuristic rules. + expectedNoun := argName switch { case argName == "id" || argName == "ids": - // Heuristic: user is trying to autocomplete for id/ids arg for a list API - relatedNoun = apiFound.Noun + expectedNoun = apiFound.Noun if apiFound.Verb != "list" { - relatedNoun += "s" + expectedNoun += "s" } + case argName == "account": - // Heuristic: user is trying to autocomplete for accounts - relatedNoun = "accounts" + expectedNoun = "accounts" + case argName == "ipaddressid": - // Heuristic: user is trying to autocomplete for ip addresses - relatedNoun = "publicipaddresses" + expectedNoun = "publicipaddresses" + case argName == "storageid": - relatedNoun = "storagepools" + expectedNoun = "storagepools" + case argName == "associatednetworkid": - relatedNoun = "networks" + expectedNoun = "networks" + default: - // Heuristic: autocomplete for the arg for which a lists API exists - // For example, for zoneid arg, listZones API exists base := argName + if strings.HasSuffix(argName, "id") { base = strings.TrimSuffix(argName, "id") } else if strings.HasSuffix(argName, "ids") { @@ -249,15 +253,61 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st } } } - // Handle common cases where base ends with a vowel and needs "es" - if strings.HasSuffix(base, "s") || strings.HasSuffix(base, "x") || strings.HasSuffix(base, "z") || strings.HasSuffix(base, "ch") || strings.HasSuffix(base, "sh") { - relatedNoun = base + "es" + + if strings.HasSuffix(base, "s") || + strings.HasSuffix(base, "x") || + strings.HasSuffix(base, "z") || + strings.HasSuffix(base, "ch") || + strings.HasSuffix(base, "sh") { + expectedNoun = base + "es" } else { - relatedNoun = base + "s" + expectedNoun = base + "s" + } + } + + // FIRST: prefer Related APIs whose noun matches the expected noun. + for _, relatedAPI := range arg.Related { + if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { + continue + } + + for _, listAPI := range apiMap["list"] { + if strings.EqualFold(listAPI.Name, relatedAPI) && + strings.EqualFold(listAPI.Noun, expectedNoun) { + + config.Debug( + "Autocomplete: API found using Related metadata: ", + listAPI.Name, + ) + + return listAPI + } + } + } + + // SECOND: fallback to any list API from Related. + for _, relatedAPI := range arg.Related { + if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { + continue + } + + for _, listAPI := range apiMap["list"] { + if strings.EqualFold(listAPI.Name, relatedAPI) { + + config.Debug( + "Autocomplete: API found using Related metadata fallback: ", + listAPI.Name, + ) + + return listAPI + } } } + relatedNoun := expectedNoun + config.Debug("Possible related noun for the arg: ", relatedNoun, " and type: ", arg.Type) + autocompleteAPI = findAPI(apiMap, relatedNoun) if autocompleteAPI == nil { @@ -278,13 +328,16 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st // Heuristic: find any list API that contains the arg name if autocompleteAPI == nil { config.Debug("Finding possible API that have: ", argName, " related APIs: ", arg.Related) + possibleAPIs := []*config.API{} + for _, listAPI := range apiMap["list"] { if strings.Contains(listAPI.Noun, argName) { config.Debug("Found possible API: ", listAPI.Name) possibleAPIs = append(possibleAPIs, listAPI) } } + if len(possibleAPIs) == 1 { autocompleteAPI = possibleAPIs[0] } From 1b33bad316fdf6b6eb0bdc65fadd8ccbd68c36d1 Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Fri, 19 Jun 2026 08:45:10 +0000 Subject: [PATCH 2/4] test: add autocomplete API discovery coverage --- cli/completer_test.go | 165 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 cli/completer_test.go diff --git a/cli/completer_test.go b/cli/completer_test.go new file mode 100644 index 0000000..c404e66 --- /dev/null +++ b/cli/completer_test.go @@ -0,0 +1,165 @@ +package cli + +import ( + "testing" + + "github.com/apache/cloudstack-cloudmonkey/config" +) + +func TestFindAutocompleteAPIRelatedNounMatch(t *testing.T) { + arg := &config.APIArg{ + Name: "domainid=", + Related: []string{ + "createDomain", + "listDomains", + "updateDomain", + }, + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listDomains", + Noun: "domains", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listDomains" { + t.Fatalf("expected listDomains, got %s", result.Name) + } +} + +func TestFindAutocompleteAPIRelatedFallback(t *testing.T) { + arg := &config.APIArg{ + Name: "domainid=", + Related: []string{ + "listDomainChildren", + }, + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listDomainChildren", + Noun: "domainchildren", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listDomainChildren" { + t.Fatalf("expected listDomainChildren, got %s", result.Name) + } +} + +func TestFindAutocompleteAPIEmptyRelatedFallsBackToHeuristic(t *testing.T) { + arg := &config.APIArg{ + Name: "zoneid=", + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listZones", + Noun: "zones", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listZones" { + t.Fatalf("expected listZones, got %s", result.Name) + } +} + +func TestFindAutocompleteAPINonListRelatedFallsBackToHeuristic(t *testing.T) { + arg := &config.APIArg{ + Name: "zoneid=", + Related: []string{ + "createZone", + "updateZone", + }, + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listZones", + Noun: "zones", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listZones" { + t.Fatalf("expected listZones, got %s", result.Name) + } +} + +func TestFindAutocompleteAPIMapTypeReturnsNil(t *testing.T) { + arg := &config.APIArg{ + Type: "map", + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": {}, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result != nil { + t.Fatalf("expected nil, got %v", result) + } +} From fdd8a90ba269fa8d9f2ad1a49945fb8db08a3a7a Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Thu, 23 Jul 2026 10:50:15 +0000 Subject: [PATCH 3/4] Add autocomplete support for API allowedValues --- cli/completer.go | 12 ++++++++++++ config/cache.go | 34 +++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/cli/completer.go b/cli/completer.go index 755cc7f..5984d32 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -413,6 +413,18 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) return } + if len(arg.AllowedValues) > 0 { + offset = 0 + for _, value := range arg.AllowedValues { + option := value + " " + if strings.HasPrefix(value, argInput) { + options = append(options, []rune(option[len(argInput):])) + offset = len(argInput) + } + } + return + } + autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap) if autocompleteAPI == nil { return nil, 0 diff --git a/config/cache.go b/config/cache.go index 510075a..fc083eb 100644 --- a/config/cache.go +++ b/config/cache.go @@ -39,12 +39,13 @@ var bundledAPICache []byte // APIArg are the args passable to an API type APIArg struct { - Name string - Type string - Related []string - Description string - Required bool - Length int + Name string + Type string + Related []string + AllowedValues []string + Description string + Required bool + Length int } // API describes a CloudStack API @@ -143,12 +144,23 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} { related = strings.Split(apiArg["related"].(string), ",") sort.Strings(related) } + allowedValues := []string{} + if apiArg["allowedvalues"] != nil { + if rawValues, ok := apiArg["allowedvalues"].([]interface{}); ok { + for _, value := range rawValues { + if str, ok := value.(string); ok { + allowedValues = append(allowedValues, str) + } + } + } + } apiArgs = append(apiArgs, &APIArg{ - Name: apiArg["name"].(string) + "=", - Type: apiArg["type"].(string), - Required: apiArg["required"].(bool), - Related: related, - Description: apiArg["description"].(string), + Name: apiArg["name"].(string) + "=", + Type: apiArg["type"].(string), + Required: apiArg["required"].(bool), + Related: related, + Description: apiArg["description"].(string), + AllowedValues: allowedValues, }) } From 6718ed561377f3219b48a7b704f23a30e7bd4333 Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Mon, 27 Jul 2026 15:17:07 +0000 Subject: [PATCH 4/4] style: move AllowedValues to end of APIArg --- config/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cache.go b/config/cache.go index fc083eb..a6b9d70 100644 --- a/config/cache.go +++ b/config/cache.go @@ -42,10 +42,10 @@ type APIArg struct { Name string Type string Related []string - AllowedValues []string Description string Required bool Length int + AllowedValues []string } // API describes a CloudStack API