From 944797301522b5784192fc2b05ffb4ca4695df03 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2014 22:52:53 +0530 Subject: [PATCH] helper/schema: ConfigFieldReader and generic helpers --- helper/schema/field_reader.go | 123 ++++++++++++ helper/schema/field_reader_config.go | 115 ++++++++++++ helper/schema/field_reader_config_test.go | 217 ++++++++++++++++++++++ helper/schema/field_reader_map.go | 107 +---------- 4 files changed, 460 insertions(+), 102 deletions(-) create mode 100644 helper/schema/field_reader_config.go create mode 100644 helper/schema/field_reader_config_test.go diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index dc99eaa70..187e79bee 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -1,8 +1,131 @@ package schema +import ( + "fmt" + "strconv" +) + // FieldReaders are responsible for decoding fields out of data into // the proper typed representation. ResourceData uses this to query data // out of multiple sources: config, state, diffs, etc. type FieldReader interface { ReadField([]string, *Schema) (interface{}, bool, bool, error) } + +// readListField is a generic method for reading a list field out of a +// a FieldReader. It does this based on the assumption that there is a key +// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc. +// after that point. +func readListField( + r FieldReader, k string, schema *Schema) (interface{}, bool, bool, error) { + // Get the number of elements in the list + countRaw, countOk, countComputed, err := r.ReadField( + []string{k + ".#"}, &Schema{Type: TypeInt}) + if err != nil { + return nil, false, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // If we have an empty list, then return an empty list + if countComputed || countRaw.(int) == 0 { + return []interface{}{}, true, countComputed, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countRaw.(int)) + for i, _ := range result { + is := strconv.FormatInt(int64(i), 10) + raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema) + if err != nil { + return nil, false, false, err + } + if !ok { + // This should never happen, because by the time the data + // gets to the FieldReaders, all the defaults should be set by + // Schema. + raw = nil + } + + result[i] = raw + } + + return result, true, false, nil +} + +// readObjectField is a generic method for reading objects out of FieldReaders +// based on the assumption that building an address of []string{k, FIELD} +// will result in the proper field data. +func readObjectField( + r FieldReader, + k string, + schema map[string]*Schema) (interface{}, bool, bool, error) { + result := make(map[string]interface{}) + for field, schema := range schema { + v, ok, _, err := r.ReadField([]string{k, field}, schema) + if err != nil { + return nil, false, false, err + } + if !ok { + continue + } + + result[field] = v + } + + return result, true, false, nil +} + +func stringToPrimitive( + value string, computed bool, schema *Schema) (interface{}, error) { + var returnVal interface{} + switch schema.Type { + case TypeBool: + if value == "" { + returnVal = false + break + } + + v, err := strconv.ParseBool(value) + if err != nil { + return nil, err + } + + returnVal = v + case TypeInt: + if value == "" { + returnVal = 0 + break + } + if computed { + break + } + + v, err := strconv.ParseInt(value, 0, 0) + if err != nil { + return nil, err + } + + returnVal = int(v) + case TypeString: + returnVal = value + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } + + return returnVal, nil +} diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go new file mode 100644 index 000000000..ff418287b --- /dev/null +++ b/helper/schema/field_reader_config.go @@ -0,0 +1,115 @@ +package schema + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/mapstructure" +) + +// ConfigFieldReader reads fields out of an untyped map[string]string to +// the best of its ability. +type ConfigFieldReader struct { + Config *terraform.ResourceConfig +} + +func (r *ConfigFieldReader) ReadField( + address []string, schema *Schema) (interface{}, bool, bool, error) { + k := strings.Join(address, ".") + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return r.readPrimitive(k, schema) + case TypeList: + return readListField(r, k, schema) + case TypeMap: + return r.readMap(k) + case TypeSet: + return r.readSet(k, schema) + case typeObject: + return readObjectField(r, k, schema.Elem.(map[string]*Schema)) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (r *ConfigFieldReader) readMap(k string) (interface{}, bool, bool, error) { + mraw, ok := r.Config.Get(k) + if !ok { + return nil, false, false, nil + } + + result := make(map[string]interface{}) + switch m := mraw.(type) { + case []interface{}: + for _, innerRaw := range m { + for k, v := range innerRaw.(map[string]interface{}) { + result[k] = v + } + } + case []map[string]interface{}: + for _, innerRaw := range m { + for k, v := range innerRaw { + result[k] = v + } + } + case map[string]interface{}: + result = m + default: + panic(fmt.Sprintf("unknown type: %#v", mraw)) + } + + return result, true, false, nil +} + +func (r *ConfigFieldReader) readPrimitive( + k string, schema *Schema) (interface{}, bool, bool, error) { + raw, ok := r.Config.Get(k) + if !ok { + return nil, false, false, nil + } + + var result string + if err := mapstructure.WeakDecode(raw, &result); err != nil { + return nil, false, false, err + } + + computed := r.Config.IsComputed(k) + returnVal, err := stringToPrimitive(result, computed, schema) + if err != nil { + return nil, false, false, err + } + + return returnVal, true, computed, nil +} + +func (r *ConfigFieldReader) readSet( + k string, schema *Schema) (interface{}, bool, bool, error) { + raw, ok, computed, err := readListField(r, k, schema) + if err != nil { + return nil, false, false, err + } + if !ok { + return nil, false, false, nil + } + + // Create the set that will be our result + set := &Set{F: schema.Set} + + // If the list is computed, the set is necessarilly computed + if computed { + return set, true, computed, nil + } + + // Build up the set from the list elements + for _, v := range raw.([]interface{}) { + set.Add(v) + } + + return set, true, false, nil +} diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go new file mode 100644 index 000000000..be266a57c --- /dev/null +++ b/helper/schema/field_reader_config_test.go @@ -0,0 +1,217 @@ +package schema + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +func TestConfigFieldReader_impl(t *testing.T) { + var _ FieldReader = new(ConfigFieldReader) +} + +func TestConfigFieldReader(t *testing.T) { + r := &ConfigFieldReader{ + Config: testConfig(t, map[string]interface{}{ + "bool": true, + "int": 42, + "string": "string", + + "list": []interface{}{"foo", "bar"}, + + "listInt": []interface{}{21, 42}, + + "map": map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + + "set": []interface{}{10, 50}, + + "setDeep": []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + }), + } + + cases := map[string]struct { + Addr []string + Schema *Schema + Out interface{} + OutOk bool + OutComputed bool + OutErr bool + }{ + "noexist": { + []string{"boolNOPE"}, + &Schema{Type: TypeBool}, + nil, + false, + false, + false, + }, + + "bool": { + []string{"bool"}, + &Schema{Type: TypeBool}, + true, + true, + false, + false, + }, + + "int": { + []string{"int"}, + &Schema{Type: TypeInt}, + 42, + true, + false, + false, + }, + + "string": { + []string{"string"}, + &Schema{Type: TypeString}, + "string", + true, + false, + false, + }, + + "list": { + []string{"list"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + []interface{}{ + "foo", + "bar", + }, + true, + false, + false, + }, + + "listInt": { + []string{"listInt"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + []interface{}{ + 21, + 42, + }, + true, + false, + false, + }, + + "map": { + []string{"map"}, + &Schema{Type: TypeMap}, + map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + true, + false, + false, + }, + + "mapelem": { + []string{"map", "foo"}, + &Schema{Type: TypeString}, + "bar", + true, + false, + false, + }, + + "set": { + []string{"set"}, + &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + []interface{}{10, 50}, + true, + false, + false, + }, + + "setDeep": { + []string{"setDeep"}, + &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + true, + false, + false, + }, + } + + for name, tc := range cases { + out, outOk, outComputed, outErr := r.ReadField(tc.Addr, tc.Schema) + if (outErr != nil) != tc.OutErr { + t.Fatalf("%s: err: %s", name, outErr) + } + if outComputed != tc.OutComputed { + t.Fatalf("%s: err: %#v", name, outComputed) + } + + if s, ok := out.(*Set); ok { + // If it is a set, convert to a list so its more easily checked. + out = s.List() + } + + if !reflect.DeepEqual(out, tc.Out) { + t.Fatalf("%s: out: %#v", name, out) + } + if outOk != tc.OutOk { + t.Fatalf("%s: outOk: %#v", name, outOk) + } + } +} + +func testConfig( + t *testing.T, raw map[string]interface{}) *terraform.ResourceConfig { + rc, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + return terraform.NewResourceConfig(rc) +} diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index df9391a16..190219c6a 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -2,7 +2,6 @@ package schema import ( "fmt" - "strconv" "strings" ) @@ -24,87 +23,18 @@ func (r *MapFieldReader) ReadField( case TypeString: return r.readPrimitive(k, schema) case TypeList: - return r.readList(k, schema) + return readListField(r, k, schema) case TypeMap: return r.readMap(k) case TypeSet: return r.readSet(k, schema) case typeObject: - return r.readObject(k, schema.Elem.(map[string]*Schema)) + return readObjectField(r, k, schema.Elem.(map[string]*Schema)) default: panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) } } -func (r *MapFieldReader) readObject( - k string, schema map[string]*Schema) (interface{}, bool, bool, error) { - result := make(map[string]interface{}) - for field, schema := range schema { - v, ok, _, err := r.ReadField([]string{k, field}, schema) - if err != nil { - return nil, false, false, err - } - if !ok { - continue - } - - result[field] = v - } - - return result, true, false, nil -} - -func (r *MapFieldReader) readList( - k string, schema *Schema) (interface{}, bool, bool, error) { - // Get the number of elements in the list - countRaw, countOk, countComputed, err := r.readPrimitive( - k+".#", &Schema{Type: TypeInt}) - if err != nil { - return nil, false, false, err - } - if !countOk { - // No count, means we have no list - countRaw = 0 - } - - // If we have an empty list, then return an empty list - if countComputed || countRaw.(int) == 0 { - return []interface{}{}, true, countComputed, nil - } - - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - - // Go through each count, and get the item value out of it - result := make([]interface{}, countRaw.(int)) - for i, _ := range result { - is := strconv.FormatInt(int64(i), 10) - raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema) - if err != nil { - return nil, false, false, err - } - if !ok { - // This should never happen, because by the time the data - // gets to the FieldReaders, all the defaults should be set by - // Schema. - raw = nil - } - - result[i] = raw - } - - return result, true, false, nil -} - func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) { result := make(map[string]interface{}) resultSet := false @@ -134,36 +64,9 @@ func (r *MapFieldReader) readPrimitive( return nil, false, false, nil } - var returnVal interface{} - switch schema.Type { - case TypeBool: - if result == "" { - returnVal = false - break - } - - v, err := strconv.ParseBool(result) - if err != nil { - return nil, false, false, err - } - - returnVal = v - case TypeInt: - if result == "" { - returnVal = 0 - break - } - - v, err := strconv.ParseInt(result, 0, 0) - if err != nil { - return nil, false, false, err - } - - returnVal = int(v) - case TypeString: - returnVal = result - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + returnVal, err := stringToPrimitive(result, false, schema) + if err != nil { + return nil, false, false, err } return returnVal, true, false, nil