List Handling in API and CLI (#2584)

This commit is contained in:
Chris Hoffman
2017-04-18 16:02:31 -04:00
committed by GitHub
parent 0fa9b47d95
commit 8efdae67e5
8 changed files with 215 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ import (
"strings"
"github.com/hashicorp/vault/helper/jsonutil"
"github.com/mitchellh/mapstructure"
)
// Builder is a struct to build a key/value mapping based on a list
@@ -107,6 +108,17 @@ func (b *Builder) add(raw string) error {
}
}
// Repeated keys will be converted into a slice
if existingValue, ok := b.result[key]; ok {
var sliceValue []interface{}
if err := mapstructure.WeakDecode(existingValue, &sliceValue); err != nil {
return err
}
sliceValue = append(sliceValue, value)
b.result[key] = sliceValue
return nil
}
b.result[key] = value
return nil
}

View File

@@ -85,3 +85,36 @@ func TestBuilder_stdinTwice(t *testing.T) {
t.Fatal("should error")
}
}
func TestBuilder_sameKeyTwice(t *testing.T) {
var b Builder
err := b.Add("foo=bar", "foo=baz")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := map[string]interface{}{
"foo": []interface{}{"bar", "baz"},
}
actual := b.Map()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestBuilder_sameKeyMultipleTimes(t *testing.T) {
var b Builder
err := b.Add("foo=bar", "foo=baz", "foo=bay", "foo=bax", "bar=baz")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := map[string]interface{}{
"foo": []interface{}{"bar", "baz", "bay", "bax"},
"bar": "baz",
}
actual := b.Map()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}

View File

@@ -174,6 +174,16 @@ func ParseArbitraryStringSlice(input string, sep string) []string {
return ret
}
// TrimStrings takes a slice of strings and returns a slice of strings
// with trimmed spaces
func TrimStrings(items []string) []string {
ret := make([]string, len(items))
for i, item := range items {
ret[i] = strings.TrimSpace(item)
}
return ret
}
// Removes duplicate and empty elements from a slice of strings. This also may
// convert the items in the slice to lower case and returns a sorted slice.
func RemoveDuplicates(items []string, lowercase bool) []string {

View File

@@ -315,3 +315,12 @@ func TestGlobbedStringsMatch(t *testing.T) {
}
}
}
func TestTrimStrings(t *testing.T) {
input := []string{"abc", "123", "abcd ", "123 "}
expected := []string{"abc", "123", "abcd", "123"}
actual := TrimStrings(input)
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("Bad TrimStrings: expected:%#v, got:%#v", expected, actual)
}
}

View File

@@ -13,9 +13,9 @@ import (
log "github.com/mgutz/logxi/v1"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/helper/errutil"
"github.com/hashicorp/vault/helper/logformat"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/logical"
)
@@ -587,6 +587,10 @@ func (t FieldType) Zero() interface{} {
return map[string]interface{}{}
case TypeDurationSecond:
return 0
case TypeSlice:
return []interface{}{}
case TypeStringSlice, TypeCommaStringSlice:
return []string{}
default:
panic("unknown type: " + t.String())
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/helper/strutil"
"github.com/mitchellh/mapstructure"
)
@@ -30,7 +31,8 @@ func (d *FieldData) Validate() error {
}
switch schema.Type {
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString:
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString, TypeSlice,
TypeStringSlice, TypeCommaStringSlice:
_, _, err := d.getPrimitive(field, schema)
if err != nil {
return fmt.Errorf("Error converting input %v for field %s: %s", value, field, err)
@@ -105,7 +107,8 @@ func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) {
}
switch schema.Type {
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString:
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString,
TypeSlice, TypeStringSlice, TypeCommaStringSlice:
return d.getPrimitive(k, schema)
default:
return nil, false,
@@ -177,6 +180,36 @@ func (d *FieldData) getPrimitive(
}
return result, true, nil
case TypeSlice:
var result []interface{}
if err := mapstructure.WeakDecode(raw, &result); err != nil {
return nil, true, err
}
return result, true, nil
case TypeStringSlice:
var result []string
if err := mapstructure.WeakDecode(raw, &result); err != nil {
return nil, true, err
}
return strutil.TrimStrings(result), true, nil
case TypeCommaStringSlice:
var result []string
config := &mapstructure.DecoderConfig{
Result: &result,
WeaklyTypedInput: true,
DecodeHook: mapstructure.StringToSliceHookFunc(","),
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return nil, false, err
}
if err := decoder.Decode(raw); err != nil {
return nil, false, err
}
return strutil.TrimStrings(result), true, nil
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}

View File

@@ -146,6 +146,105 @@ func TestFieldDataGet(t *testing.T) {
"foo",
0,
},
"slice type, empty slice": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeSlice},
},
map[string]interface{}{
"foo": []interface{}{},
},
"foo",
[]interface{}{},
},
"slice type, filled, mixed slice": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeSlice},
},
map[string]interface{}{
"foo": []interface{}{123, "abc"},
},
"foo",
[]interface{}{123, "abc"},
},
"string slice type, filled slice": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeStringSlice},
},
map[string]interface{}{
"foo": []interface{}{123, "abc"},
},
"foo",
[]string{"123", "abc"},
},
"comma string slice type, comma string with one value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeCommaStringSlice},
},
map[string]interface{}{
"foo": "value1",
},
"foo",
[]string{"value1"},
},
"comma string slice type, comma string with multi value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeCommaStringSlice},
},
map[string]interface{}{
"foo": "value1,value2,value3",
},
"foo",
[]string{"value1", "value2", "value3"},
},
"comma string slice type, nil string slice value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeCommaStringSlice},
},
map[string]interface{}{
"foo": "",
},
"foo",
[]string{},
},
"commma string slice type, string slice with one value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeCommaStringSlice},
},
map[string]interface{}{
"foo": []interface{}{"value1"},
},
"foo",
[]string{"value1"},
},
"comma string slice type, string slice with multi value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeCommaStringSlice},
},
map[string]interface{}{
"foo": []interface{}{"value1", "value2", "value3"},
},
"foo",
[]string{"value1", "value2", "value3"},
},
"comma string slice type, empty string slice value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeCommaStringSlice},
},
map[string]interface{}{
"foo": []interface{}{},
},
"foo",
[]string{},
},
}
for name, tc := range cases {

View File

@@ -13,6 +13,16 @@ const (
// TypeDurationSecond represent as seconds, this can be either an
// integer or go duration format string (e.g. 24h)
TypeDurationSecond
// TypeSlice represents a slice of any type
TypeSlice
// TypeStringSlice is a helper for TypeSlice that returns a sanitized
// slice of strings
TypeStringSlice
// TypeCommaStringSlice is a helper for TypeSlice that returns a sanitized
// slice of strings and also supports parsing a comma-separated list in
// a string field
TypeCommaStringSlice
)
func (t FieldType) String() string {
@@ -27,6 +37,8 @@ func (t FieldType) String() string {
return "map"
case TypeDurationSecond:
return "duration (sec)"
case TypeSlice, TypeStringSlice, TypeCommaStringSlice:
return "slice"
default:
return "unknown type"
}