helper/schema: handle TypeMap elem consistently with other collection types

For historical reasons, the handling of element types for maps is inconsistent with other collection types.

Here we begin a multi-step process to make it consistent, starting by supporting both the "consistent" form of using a schema.Schema and an existing erroneous form of using a schema.Type directly. In subsequent commits we will phase out the erroneous form and require the schema.Schema approach, the same as we do for TypeList and TypeSet.
This commit is contained in:
James McGill 2018-03-14 17:50:41 -04:00 committed by Martin Atkins
parent 04899393b0
commit 035d56409f
10 changed files with 90 additions and 14 deletions

View File

@ -126,6 +126,8 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
switch v := current.Elem.(type) { switch v := current.Elem.(type) {
case ValueType: case ValueType:
current = &Schema{Type: v} current = &Schema{Type: v}
case *Schema:
current, _ = current.Elem.(*Schema)
default: default:
// maps default to string values. This is all we can have // maps default to string values. This is all we can have
// if this is nested in another list or map. // if this is nested in another list or map.
@ -249,11 +251,10 @@ func readObjectField(
} }
// convert map values to the proper primitive type based on schema.Elem // convert map values to the proper primitive type based on schema.Elem
func mapValuesToPrimitive(m map[string]interface{}, schema *Schema) error { func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error {
elemType, err := getValueType(k, schema)
elemType := TypeString if err != nil {
if et, ok := schema.Elem.(ValueType); ok { return err
elemType = et
} }
switch elemType { switch elemType {

View File

@ -206,7 +206,7 @@ func (r *ConfigFieldReader) readMap(k string, schema *Schema) (FieldReadResult,
panic(fmt.Sprintf("unknown type: %#v", mraw)) panic(fmt.Sprintf("unknown type: %#v", mraw))
} }
err := mapValuesToPrimitive(result, schema) err := mapValuesToPrimitive(k, result, schema)
if err != nil { if err != nil {
return FieldReadResult{}, nil return FieldReadResult{}, nil
} }

View File

@ -39,6 +39,10 @@ func TestConfigFieldReader(t *testing.T) {
"one": "1", "one": "1",
"two": "2", "two": "2",
}, },
"mapIntNestedSchema": map[string]interface{}{
"one": "1",
"two": "2",
},
"mapFloat": map[string]interface{}{ "mapFloat": map[string]interface{}{
"oneDotTwo": "1.2", "oneDotTwo": "1.2",
}, },

View File

@ -122,7 +122,8 @@ func (r *DiffFieldReader) readMap(
result[k] = v.New result[k] = v.New
} }
err = mapValuesToPrimitive(result, schema) key := address[len(address)-1]
err = mapValuesToPrimitive(key, result, schema)
if err != nil { if err != nil {
return FieldReadResult{}, nil return FieldReadResult{}, nil
} }

View File

@ -433,6 +433,19 @@ func TestDiffFieldReader(t *testing.T) {
New: "2", New: "2",
}, },
"mapIntNestedSchema.%": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapIntNestedSchema.one": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"mapIntNestedSchema.two": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapFloat.%": &terraform.ResourceAttrDiff{ "mapFloat.%": &terraform.ResourceAttrDiff{
Old: "", Old: "",
New: "1", New: "1",

View File

@ -61,7 +61,7 @@ func (r *MapFieldReader) readMap(k string, schema *Schema) (FieldReadResult, err
return true return true
}) })
err := mapValuesToPrimitive(result, schema) err := mapValuesToPrimitive(k, result, schema)
if err != nil { if err != nil {
return FieldReadResult{}, nil return FieldReadResult{}, nil
} }

View File

@ -46,6 +46,10 @@ func TestMapFieldReader(t *testing.T) {
"mapInt.one": "1", "mapInt.one": "1",
"mapInt.two": "2", "mapInt.two": "2",
"mapIntNestedSchema.%": "2",
"mapIntNestedSchema.one": "1",
"mapIntNestedSchema.two": "2",
"mapFloat.%": "1", "mapFloat.%": "1",
"mapFloat.oneDotTwo": "1.2", "mapFloat.oneDotTwo": "1.2",

View File

@ -215,6 +215,13 @@ func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) {
Type: TypeMap, Type: TypeMap,
Elem: TypeInt, Elem: TypeInt,
}, },
// This is used to verify that the type of a Map can be specified using the
// same syntax as for lists (as a nested *Schema passed to Elem)
"mapIntNestedSchema": &Schema{
Type: TypeMap,
Elem: &Schema{Type: TypeInt},
},
"mapFloat": &Schema{ "mapFloat": &Schema{
Type: TypeMap, Type: TypeMap,
Elem: TypeFloat, Elem: TypeFloat,
@ -360,6 +367,19 @@ func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) {
false, false,
}, },
"mapIntNestedSchema": {
[]string{"mapIntNestedSchema"},
FieldReadResult{
Value: map[string]interface{}{
"one": 1,
"two": 2,
},
Exists: true,
Computed: false,
},
false,
},
"mapFloat": { "mapFloat": {
[]string{"mapFloat"}, []string{"mapFloat"},
FieldReadResult{ FieldReadResult{

View File

@ -1461,13 +1461,10 @@ func getValueType(k string, schema *Schema) (ValueType, error) {
return vt, nil return vt, nil
} }
// If a Schema is provided to a Map, we use the Type of that schema
// as the type for each element in the Map.
if s, ok := schema.Elem.(*Schema); ok { if s, ok := schema.Elem.(*Schema); ok {
if s.Elem == nil { return s.Type, nil
return TypeString, nil
}
if vt, ok := s.Elem.(ValueType); ok {
return vt, nil
}
} }
if _, ok := schema.Elem.(*Resource); ok { if _, ok := schema.Elem.(*Resource); ok {

View File

@ -4528,6 +4528,42 @@ func TestSchemaMap_Validate(t *testing.T) {
}, },
}, },
"Map with type specified as value type": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
Elem: TypeBool,
},
},
Config: map[string]interface{}{
"user_data": map[string]interface{}{
"foo": "not_a_bool",
},
},
Err: true,
},
"Map with type specified as nested Schema": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
Elem: &Schema{Type: TypeBool},
},
},
Config: map[string]interface{}{
"user_data": map[string]interface{}{
"foo": "not_a_bool",
},
},
Err: true,
},
"Bad map: just a slice": { "Bad map: just a slice": {
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"user_data": &Schema{ "user_data": &Schema{