From 86a757d44ee81488dfea2d1e2abab379d20e1cc5 Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Sun, 4 Oct 2026 04:04:56 +0000 Subject: [PATCH] Add map argument autocomplete --- cli/completer.go | 61 +++++++++++++++ cli/completer_test.go | 168 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) diff --git a/cli/completer.go b/cli/completer.go index 9547f5a..1f450a7 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -19,6 +19,7 @@ package cli import ( "fmt" + "regexp" "sort" "strconv" "strings" @@ -224,6 +225,53 @@ func pluralizeNoun(noun string) string { } } +func mapFieldsFromDescription(description string) []string { + fields := []string{} + seen := make(map[string]bool) + + re := regexp.MustCompile(`([A-Za-z0-9_-]+\[\d+\]\.[A-Za-z0-9_.:-]+)=`) + + matches := re.FindAllStringSubmatch(description, -1) + for _, match := range matches { + if len(match) < 2 { + continue + } + + field := match[1] + "=" + if seen[field] { + continue + } + + seen[field] = true + fields = append(fields, field) + } + + return fields +} + +func mapFieldSuggestions(arg *config.APIArg) []string { + if arg == nil || arg.Type != "map" { + return nil + } + + fields := mapFieldsFromDescription(arg.Description) + if len(fields) > 0 { + return fields + } + + description := strings.ToLower(arg.Description) + if strings.Contains(description, "key/value") || + strings.Contains(description, "key-value") { + mapName := strings.TrimSuffix(arg.Name, "=") + return []string{ + mapName + "[0].key=", + mapName + "[0].value=", + } + } + + return nil +} + func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[string][]*config.API) *config.API { if arg.Type == "map" { return nil @@ -409,6 +457,19 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) return } + if arg.Type == "map" { + suggestions := mapFieldSuggestions(arg) + + for _, suggestion := range suggestions { + if strings.HasPrefix(suggestion, string(line)) { + options = append(options, []rune(suggestion[len(line):])) + } + } + + offset = len(line) + return + } + if arg.Type == config.FAKE && arg.Name == "filter=" { offset = 0 filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|") diff --git a/cli/completer_test.go b/cli/completer_test.go index 0aa73df..98b674e 100644 --- a/cli/completer_test.go +++ b/cli/completer_test.go @@ -18,6 +18,8 @@ package cli import ( + "reflect" + "strings" "testing" "github.com/apache/cloudstack-cloudmonkey/config" @@ -221,3 +223,169 @@ func TestFindAutocompleteAPIHeuristicWinsOverRelated(t *testing.T) { t.Fatalf("expected listProjects, got %s", result.Name) } } + +func TestMapFieldsFromDescription(t *testing.T) { + description := "Example: tags[0].key=value&tags[0].value=value" + + fields := mapFieldsFromDescription(description) + + expected := []string{ + "tags[0].key=", + "tags[0].value=", + } + + if len(fields) != len(expected) { + t.Fatalf("expected %d fields, got %d: %v", len(expected), len(fields), fields) + } + + for i := range expected { + if fields[i] != expected[i] { + t.Errorf("expected %q, got %q", expected[i], fields[i]) + } + } +} + +func TestMapFieldsFromDescriptionWithRealMap(t *testing.T) { + description := "VMware only: used to specify network mapping of a vApp VMware template registered \"as-is\". Example nicnetworklist[0].ip=Nic-101&nicnetworklist[0].network=uuid" + + fields := mapFieldsFromDescription(description) + + expected := []string{ + "nicnetworklist[0].ip=", + "nicnetworklist[0].network=", + } + + if len(fields) != len(expected) { + t.Fatalf("expected %d fields, got %d: %v", len(expected), len(fields), fields) + } + + for i := range expected { + if fields[i] != expected[i] { + t.Errorf("expected %q, got %q", expected[i], fields[i]) + } + } +} + +func TestMapFieldsFromDescriptionWithMultipleFields(t *testing.T) { + description := "Disk offering details. Example: datadisksdetails[0].diskofferingid=id&datadisksdetails[0].size=10&datadisksdetails[0].miniops=100&datadisksdetails[0].maxiops=200" + + fields := mapFieldsFromDescription(description) + + expected := []string{ + "datadisksdetails[0].diskofferingid=", + "datadisksdetails[0].size=", + "datadisksdetails[0].miniops=", + "datadisksdetails[0].maxiops=", + } + + if len(fields) != len(expected) { + t.Fatalf("expected %d fields, got %d: %v", len(expected), len(fields), fields) + } + + for i := range expected { + if fields[i] != expected[i] { + t.Errorf("expected %q, got %q", expected[i], fields[i]) + } + } +} + +func TestMapFieldSuggestions(t *testing.T) { + arg := &config.APIArg{ + Name: "tags=", + Type: "map", + Description: "Example: tags[0].key=value&tags[0].value=value", + } + + suggestions := mapFieldSuggestions(arg) + + expected := []string{ + "tags[0].key=", + "tags[0].value=", + } + + if len(suggestions) != len(expected) { + t.Fatalf("expected %d suggestions, got %d: %v", len(expected), len(suggestions), suggestions) + } + + for i := range expected { + if suggestions[i] != expected[i] { + t.Errorf("expected %q, got %q", expected[i], suggestions[i]) + } + } +} + +func TestMapFieldSuggestionsWithGenericMap(t *testing.T) { + arg := &config.APIArg{ + Name: "tags", + Type: "map", + Description: "List resources by tags (key/value pairs)", + } + + expected := []string{ + "tags[0].key=", + "tags[0].value=", + } + + got := mapFieldSuggestions(arg) + + if !reflect.DeepEqual(got, expected) { + t.Fatalf("expected %v, got %v", expected, got) + } +} + +func TestMapAutocompleteOptions(t *testing.T) { + arg := &config.APIArg{ + Name: "tags", + Type: "map", + Description: "List resources by tags (key/value pairs)", + } + + line := "tags" + + suggestions := mapFieldSuggestions(arg) + var options [][]rune + + for _, suggestion := range suggestions { + if strings.HasPrefix(suggestion, line) { + options = append(options, []rune(suggestion[len(line):])) + } + } + + expected := [][]rune{ + []rune("[0].key="), + []rune("[0].value="), + } + + if !reflect.DeepEqual(options, expected) { + t.Fatalf("expected %v, got %v", expected, options) + } +} + +func TestMapFieldsFromDescriptionWithMultipleIndexes(t *testing.T) { + description := "Example: details[0].key=accesskey&details[0].value=value&details[1].key=secretkey&details[1].value=value" + + expected := []string{ + "details[0].key=", + "details[0].value=", + "details[1].key=", + "details[1].value=", + } + + fields := mapFieldsFromDescription(description) + + if !reflect.DeepEqual(fields, expected) { + t.Fatalf("expected %v, got %v", expected, fields) + } +} + +func TestMapFieldSuggestionsWithNonKeyValueMap(t *testing.T) { + arg := &config.APIArg{ + Name: "storagecapabilities", + Type: "map", + Description: "the storage pool capabilities", + } + + if got := mapFieldSuggestions(arg); got != nil { + t.Fatalf("expected no suggestions, got %v", got) + } +}