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] } 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) + } +}