diff --git a/cli/completer.go b/cli/completer.go index 755cc7f..b8c6f82 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] } @@ -413,6 +466,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/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) + } +} diff --git a/config/cache.go b/config/cache.go index 510075a..a6b9d70 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 + Description string + Required bool + Length int + AllowedValues []string } // 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, }) }