From 3ff859d734cbb8a82e787cc74fe8ad6a74bc3635 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 Jan 2015 10:32:24 -0500 Subject: [PATCH] helper/schema: MultiLevelFieldReader --- helper/schema/field_reader_multi.go | 67 ++++++ helper/schema/field_reader_multi_test.go | 255 +++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 helper/schema/field_reader_multi.go create mode 100644 helper/schema/field_reader_multi_test.go diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go new file mode 100644 index 000000000..b8f9726cf --- /dev/null +++ b/helper/schema/field_reader_multi.go @@ -0,0 +1,67 @@ +package schema + +import ( + "fmt" +) + +// MultiLevelFieldReader reads from other field readers, +// merging their results along the way in a specific order. You can specify +// "levels" and name them in order to read only an exact level or up to +// a specific level. +// +// This is useful for saying things such as "read the field from the state +// and config and merge them" or "read the latest value of the field". +type MultiLevelFieldReader struct { + Readers map[string]FieldReader + Levels []string +} + +func (r *MultiLevelFieldReader) ReadField( + address []string, schema *Schema) (FieldReadResult, error) { + return r.ReadFieldMerge(address, schema, r.Levels[len(r.Levels)-1]) +} + +func (r *MultiLevelFieldReader) ReadFieldExact( + address []string, schema *Schema, level string) (FieldReadResult, error) { + reader, ok := r.Readers[level] + if !ok { + return FieldReadResult{}, fmt.Errorf( + "Unknown reader level: %s", level) + } + + result, err := reader.ReadField(address, schema) + if err != nil { + return FieldReadResult{}, fmt.Errorf( + "Error reading level %s: %s", level, err) + } + + return result, nil +} + +func (r *MultiLevelFieldReader) ReadFieldMerge( + address []string, schema *Schema, level string) (FieldReadResult, error) { + var result FieldReadResult + for _, l := range r.Levels { + r, ok := r.Readers[l] + if !ok { + continue + } + + out, err := r.ReadField(address, schema) + if err != nil { + return FieldReadResult{}, fmt.Errorf( + "Error reading level %s: %s", l, err) + } + + // TODO: computed + if out.Exists { + result = out + } + + if l == level { + break + } + } + + return result, nil +} diff --git a/helper/schema/field_reader_multi_test.go b/helper/schema/field_reader_multi_test.go new file mode 100644 index 000000000..0236b2849 --- /dev/null +++ b/helper/schema/field_reader_multi_test.go @@ -0,0 +1,255 @@ +package schema + +import ( + "reflect" + "strconv" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { + cases := map[string]struct { + Addr []string + Schema *Schema + Readers []FieldReader + Level string + Result FieldReadResult + }{ + "specific": { + Addr: []string{"foo"}, + + Schema: &Schema{ + Type: TypeString, + }, + + Readers: []FieldReader{ + &MapFieldReader{ + Map: map[string]string{ + "foo": "bar", + }, + }, + &MapFieldReader{ + Map: map[string]string{ + "foo": "baz", + }, + }, + &MapFieldReader{ + Map: map[string]string{}, + }, + }, + + Level: "1", + Result: FieldReadResult{ + Value: "baz", + Exists: true, + }, + }, + } + + for name, tc := range cases { + readers := make(map[string]FieldReader) + levels := make([]string, len(tc.Readers)) + for i, r := range tc.Readers { + is := strconv.FormatInt(int64(i), 10) + readers[is] = r + levels[i] = is + } + + r := &MultiLevelFieldReader{ + Readers: readers, + Levels: levels, + } + + out, err := r.ReadFieldExact(tc.Addr, tc.Schema, tc.Level) + if err != nil { + t.Fatalf("%s: err: %s", name, err) + } + + if !reflect.DeepEqual(tc.Result, out) { + t.Fatalf("%s: bad: %#v", name, out) + } + } +} + +func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { + cases := map[string]struct { + Addr []string + Schema *Schema + Readers []FieldReader + Result FieldReadResult + }{ + "stringInDiff": { + Addr: []string{"availability_zone"}, + + Schema: &Schema{ + Type: TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + Readers: []FieldReader{ + &DiffFieldReader{ + Source: &MapFieldReader{ + Map: map[string]string{ + "availability_zone": "foo", + }, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "availability_zone": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "bar", + RequiresNew: true, + }, + }, + }, + }, + }, + + Result: FieldReadResult{ + Value: "bar", + Exists: true, + }, + }, + + "lastLevelComputed": { + Addr: []string{"availability_zone"}, + + Schema: &Schema{ + Type: TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + Readers: []FieldReader{ + &MapFieldReader{ + Map: map[string]string{ + "availability_zone": "foo", + }, + }, + + &DiffFieldReader{ + Source: &MapFieldReader{ + Map: map[string]string{ + "availability_zone": "foo", + }, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "availability_zone": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "bar", + NewComputed: true, + }, + }, + }, + }, + }, + + Result: FieldReadResult{ + Value: "", + Exists: true, + Computed: true, + }, + }, + + "list of maps with removal in diff": { + Addr: []string{"config_vars"}, + + Schema: &Schema{ + Type: TypeList, + Optional: true, + Computed: true, + Elem: &Schema{ + Type: TypeMap, + }, + }, + + Readers: []FieldReader{ + &DiffFieldReader{ + Source: &MapFieldReader{ + Map: map[string]string{ + "config_vars.#": "2", + "config_vars.0.foo": "bar", + "config_vars.0.bar": "bar", + "config_vars.1.bar": "baz", + }, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "config_vars.0.bar": &terraform.ResourceAttrDiff{ + NewRemoved: true, + }, + }, + }, + }, + }, + + Result: FieldReadResult{ + Value: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "bar": "baz", + }, + }, + Exists: true, + }, + }, + + "first level only": { + Addr: []string{"foo"}, + + Schema: &Schema{ + Type: TypeString, + }, + + Readers: []FieldReader{ + &MapFieldReader{ + Map: map[string]string{ + "foo": "bar", + }, + }, + &MapFieldReader{ + Map: map[string]string{}, + }, + }, + + Result: FieldReadResult{ + Value: "bar", + Exists: true, + }, + }, + } + + for name, tc := range cases { + readers := make(map[string]FieldReader) + levels := make([]string, len(tc.Readers)) + for i, r := range tc.Readers { + is := strconv.FormatInt(int64(i), 10) + readers[is] = r + levels[i] = is + } + + r := &MultiLevelFieldReader{ + Readers: readers, + Levels: levels, + } + + out, err := r.ReadFieldMerge(tc.Addr, tc.Schema, levels[len(levels)-1]) + if err != nil { + t.Fatalf("%s: err: %s", name, err) + } + + if !reflect.DeepEqual(tc.Result, out) { + t.Fatalf("%s: bad: %#v", name, out) + } + } +}