Skip to content

Commit 86a757d

Browse files
committed
Add map argument autocomplete
1 parent efbb895 commit 86a757d

2 files changed

Lines changed: 229 additions & 0 deletions

File tree

‎cli/completer.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cli
1919

2020
import (
2121
"fmt"
22+
"regexp"
2223
"sort"
2324
"strconv"
2425
"strings"
@@ -224,6 +225,53 @@ func pluralizeNoun(noun string) string {
224225
}
225226
}
226227

228+
func mapFieldsFromDescription(description string) []string {
229+
fields := []string{}
230+
seen := make(map[string]bool)
231+
232+
re := regexp.MustCompile(`([A-Za-z0-9_-]+\[\d+\]\.[A-Za-z0-9_.:-]+)=`)
233+
234+
matches := re.FindAllStringSubmatch(description, -1)
235+
for _, match := range matches {
236+
if len(match) < 2 {
237+
continue
238+
}
239+
240+
field := match[1] + "="
241+
if seen[field] {
242+
continue
243+
}
244+
245+
seen[field] = true
246+
fields = append(fields, field)
247+
}
248+
249+
return fields
250+
}
251+
252+
func mapFieldSuggestions(arg *config.APIArg) []string {
253+
if arg == nil || arg.Type != "map" {
254+
return nil
255+
}
256+
257+
fields := mapFieldsFromDescription(arg.Description)
258+
if len(fields) > 0 {
259+
return fields
260+
}
261+
262+
description := strings.ToLower(arg.Description)
263+
if strings.Contains(description, "key/value") ||
264+
strings.Contains(description, "key-value") {
265+
mapName := strings.TrimSuffix(arg.Name, "=")
266+
return []string{
267+
mapName + "[0].key=",
268+
mapName + "[0].value=",
269+
}
270+
}
271+
272+
return nil
273+
}
274+
227275
func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[string][]*config.API) *config.API {
228276
if arg.Type == "map" {
229277
return nil
@@ -409,6 +457,19 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int)
409457
return
410458
}
411459

460+
if arg.Type == "map" {
461+
suggestions := mapFieldSuggestions(arg)
462+
463+
for _, suggestion := range suggestions {
464+
if strings.HasPrefix(suggestion, string(line)) {
465+
options = append(options, []rune(suggestion[len(line):]))
466+
}
467+
}
468+
469+
offset = len(line)
470+
return
471+
}
472+
412473
if arg.Type == config.FAKE && arg.Name == "filter=" {
413474
offset = 0
414475
filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|")

‎cli/completer_test.go‎

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package cli
1919

2020
import (
21+
"reflect"
22+
"strings"
2123
"testing"
2224

2325
"github.com/apache/cloudstack-cloudmonkey/config"
@@ -221,3 +223,169 @@ func TestFindAutocompleteAPIHeuristicWinsOverRelated(t *testing.T) {
221223
t.Fatalf("expected listProjects, got %s", result.Name)
222224
}
223225
}
226+
227+
func TestMapFieldsFromDescription(t *testing.T) {
228+
description := "Example: tags[0].key=value&tags[0].value=value"
229+
230+
fields := mapFieldsFromDescription(description)
231+
232+
expected := []string{
233+
"tags[0].key=",
234+
"tags[0].value=",
235+
}
236+
237+
if len(fields) != len(expected) {
238+
t.Fatalf("expected %d fields, got %d: %v", len(expected), len(fields), fields)
239+
}
240+
241+
for i := range expected {
242+
if fields[i] != expected[i] {
243+
t.Errorf("expected %q, got %q", expected[i], fields[i])
244+
}
245+
}
246+
}
247+
248+
func TestMapFieldsFromDescriptionWithRealMap(t *testing.T) {
249+
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"
250+
251+
fields := mapFieldsFromDescription(description)
252+
253+
expected := []string{
254+
"nicnetworklist[0].ip=",
255+
"nicnetworklist[0].network=",
256+
}
257+
258+
if len(fields) != len(expected) {
259+
t.Fatalf("expected %d fields, got %d: %v", len(expected), len(fields), fields)
260+
}
261+
262+
for i := range expected {
263+
if fields[i] != expected[i] {
264+
t.Errorf("expected %q, got %q", expected[i], fields[i])
265+
}
266+
}
267+
}
268+
269+
func TestMapFieldsFromDescriptionWithMultipleFields(t *testing.T) {
270+
description := "Disk offering details. Example: datadisksdetails[0].diskofferingid=id&datadisksdetails[0].size=10&datadisksdetails[0].miniops=100&datadisksdetails[0].maxiops=200"
271+
272+
fields := mapFieldsFromDescription(description)
273+
274+
expected := []string{
275+
"datadisksdetails[0].diskofferingid=",
276+
"datadisksdetails[0].size=",
277+
"datadisksdetails[0].miniops=",
278+
"datadisksdetails[0].maxiops=",
279+
}
280+
281+
if len(fields) != len(expected) {
282+
t.Fatalf("expected %d fields, got %d: %v", len(expected), len(fields), fields)
283+
}
284+
285+
for i := range expected {
286+
if fields[i] != expected[i] {
287+
t.Errorf("expected %q, got %q", expected[i], fields[i])
288+
}
289+
}
290+
}
291+
292+
func TestMapFieldSuggestions(t *testing.T) {
293+
arg := &config.APIArg{
294+
Name: "tags=",
295+
Type: "map",
296+
Description: "Example: tags[0].key=value&tags[0].value=value",
297+
}
298+
299+
suggestions := mapFieldSuggestions(arg)
300+
301+
expected := []string{
302+
"tags[0].key=",
303+
"tags[0].value=",
304+
}
305+
306+
if len(suggestions) != len(expected) {
307+
t.Fatalf("expected %d suggestions, got %d: %v", len(expected), len(suggestions), suggestions)
308+
}
309+
310+
for i := range expected {
311+
if suggestions[i] != expected[i] {
312+
t.Errorf("expected %q, got %q", expected[i], suggestions[i])
313+
}
314+
}
315+
}
316+
317+
func TestMapFieldSuggestionsWithGenericMap(t *testing.T) {
318+
arg := &config.APIArg{
319+
Name: "tags",
320+
Type: "map",
321+
Description: "List resources by tags (key/value pairs)",
322+
}
323+
324+
expected := []string{
325+
"tags[0].key=",
326+
"tags[0].value=",
327+
}
328+
329+
got := mapFieldSuggestions(arg)
330+
331+
if !reflect.DeepEqual(got, expected) {
332+
t.Fatalf("expected %v, got %v", expected, got)
333+
}
334+
}
335+
336+
func TestMapAutocompleteOptions(t *testing.T) {
337+
arg := &config.APIArg{
338+
Name: "tags",
339+
Type: "map",
340+
Description: "List resources by tags (key/value pairs)",
341+
}
342+
343+
line := "tags"
344+
345+
suggestions := mapFieldSuggestions(arg)
346+
var options [][]rune
347+
348+
for _, suggestion := range suggestions {
349+
if strings.HasPrefix(suggestion, line) {
350+
options = append(options, []rune(suggestion[len(line):]))
351+
}
352+
}
353+
354+
expected := [][]rune{
355+
[]rune("[0].key="),
356+
[]rune("[0].value="),
357+
}
358+
359+
if !reflect.DeepEqual(options, expected) {
360+
t.Fatalf("expected %v, got %v", expected, options)
361+
}
362+
}
363+
364+
func TestMapFieldsFromDescriptionWithMultipleIndexes(t *testing.T) {
365+
description := "Example: details[0].key=accesskey&details[0].value=value&details[1].key=secretkey&details[1].value=value"
366+
367+
expected := []string{
368+
"details[0].key=",
369+
"details[0].value=",
370+
"details[1].key=",
371+
"details[1].value=",
372+
}
373+
374+
fields := mapFieldsFromDescription(description)
375+
376+
if !reflect.DeepEqual(fields, expected) {
377+
t.Fatalf("expected %v, got %v", expected, fields)
378+
}
379+
}
380+
381+
func TestMapFieldSuggestionsWithNonKeyValueMap(t *testing.T) {
382+
arg := &config.APIArg{
383+
Name: "storagecapabilities",
384+
Type: "map",
385+
Description: "the storage pool capabilities",
386+
}
387+
388+
if got := mapFieldSuggestions(arg); got != nil {
389+
t.Fatalf("expected no suggestions, got %v", got)
390+
}
391+
}

0 commit comments

Comments
 (0)