Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cli/completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cli

import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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), "|")
Expand Down
168 changes: 168 additions & 0 deletions cli/completer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package cli

import (
"reflect"
"strings"
"testing"

"github.com/apache/cloudstack-cloudmonkey/config"
Expand Down Expand Up @@ -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)
}
}