helper/schema: DiffFieldReader for reading data from a diff

This commit is contained in:
Mitchell Hashimoto 2014-12-20 02:17:35 +05:30
parent 9447973015
commit 73726e83b2
7 changed files with 601 additions and 82 deletions

View File

@ -9,7 +9,26 @@ import (
// 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)
ReadField([]string, *Schema) (FieldReadResult, error)
}
// FieldReadResult encapsulates all the resulting data from reading
// a field.
type FieldReadResult struct {
// Value is the actual read value. NegValue is the _negative_ value
// or the items that should be removed (if they existed). NegValue
// doesn't make sense for primitives but is important for any
// container types such as maps, sets, lists.
Value interface{}
NegValue interface{}
// Exists is true if the field was found in the data. False means
// it wasn't found if there was no error.
Exists bool
// Computed is true if the field was found but the value
// is computed.
Computed bool
}
// readListField is a generic method for reading a list field out of a
@ -17,21 +36,24 @@ type FieldReader interface {
// "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) {
r FieldReader, k string, schema *Schema) (FieldReadResult, error) {
// Get the number of elements in the list
countRaw, countOk, countComputed, err := r.ReadField(
[]string{k + ".#"}, &Schema{Type: TypeInt})
countResult, err := r.ReadField([]string{k + ".#"}, &Schema{Type: TypeInt})
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
if !countOk {
if !countResult.Exists {
// No count, means we have no list
countRaw = 0
countResult.Value = 0
}
// If we have an empty list, then return an empty list
if countComputed || countRaw.(int) == 0 {
return []interface{}{}, true, countComputed, nil
if countResult.Computed || countResult.Value.(int) == 0 {
return FieldReadResult{
Value: []interface{}{},
Exists: true,
Computed: countResult.Computed,
}, nil
}
// Get the schema for the elements
@ -47,24 +69,27 @@ func readListField(
}
// Go through each count, and get the item value out of it
result := make([]interface{}, countRaw.(int))
result := make([]interface{}, countResult.Value.(int))
for i, _ := range result {
is := strconv.FormatInt(int64(i), 10)
raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema)
rawResult, err := r.ReadField([]string{k, is}, elemSchema)
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
if !ok {
if !rawResult.Exists {
// This should never happen, because by the time the data
// gets to the FieldReaders, all the defaults should be set by
// Schema.
raw = nil
rawResult.Value = nil
}
result[i] = raw
result[i] = rawResult.Value
}
return result, true, false, nil
return FieldReadResult{
Value: result,
Exists: true,
}, nil
}
// readObjectField is a generic method for reading objects out of FieldReaders
@ -73,21 +98,24 @@ func readListField(
func readObjectField(
r FieldReader,
k string,
schema map[string]*Schema) (interface{}, bool, bool, error) {
schema map[string]*Schema) (FieldReadResult, error) {
result := make(map[string]interface{})
for field, schema := range schema {
v, ok, _, err := r.ReadField([]string{k, field}, schema)
rawResult, err := r.ReadField([]string{k, field}, schema)
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
if !ok {
if !rawResult.Exists {
continue
}
result[field] = v
result[field] = rawResult.Value
}
return result, true, false, nil
return FieldReadResult{
Value: result,
Exists: true,
}, nil
}
func stringToPrimitive(

View File

@ -15,7 +15,7 @@ type ConfigFieldReader struct {
}
func (r *ConfigFieldReader) ReadField(
address []string, schema *Schema) (interface{}, bool, bool, error) {
address []string, schema *Schema) (FieldReadResult, error) {
k := strings.Join(address, ".")
switch schema.Type {
@ -38,10 +38,10 @@ func (r *ConfigFieldReader) ReadField(
}
}
func (r *ConfigFieldReader) readMap(k string) (interface{}, bool, bool, error) {
func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
mraw, ok := r.Config.Get(k)
if !ok {
return nil, false, false, nil
return FieldReadResult{}, nil
}
result := make(map[string]interface{})
@ -64,52 +64,66 @@ func (r *ConfigFieldReader) readMap(k string) (interface{}, bool, bool, error) {
panic(fmt.Sprintf("unknown type: %#v", mraw))
}
return result, true, false, nil
return FieldReadResult{
Value: result,
Exists: true,
}, nil
}
func (r *ConfigFieldReader) readPrimitive(
k string, schema *Schema) (interface{}, bool, bool, error) {
k string, schema *Schema) (FieldReadResult, error) {
raw, ok := r.Config.Get(k)
if !ok {
return nil, false, false, nil
return FieldReadResult{}, nil
}
var result string
if err := mapstructure.WeakDecode(raw, &result); err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
computed := r.Config.IsComputed(k)
returnVal, err := stringToPrimitive(result, computed, schema)
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
return returnVal, true, computed, nil
return FieldReadResult{
Value: returnVal,
Exists: true,
Computed: computed,
}, nil
}
func (r *ConfigFieldReader) readSet(
k string, schema *Schema) (interface{}, bool, bool, error) {
raw, ok, computed, err := readListField(r, k, schema)
k string, schema *Schema) (FieldReadResult, error) {
raw, err := readListField(r, k, schema)
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
if !ok {
return nil, false, false, nil
if !raw.Exists {
return FieldReadResult{}, 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
if raw.Computed {
return FieldReadResult{
Value: set,
Exists: true,
Computed: raw.Computed,
}, nil
}
// Build up the set from the list elements
for _, v := range raw.([]interface{}) {
for _, v := range raw.Value.([]interface{}) {
set.Add(v)
}
return set, true, false, nil
return FieldReadResult{
Value: set,
Exists: true,
}, nil
}

View File

@ -184,24 +184,24 @@ func TestConfigFieldReader(t *testing.T) {
}
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)
out, err := r.ReadField(tc.Addr, tc.Schema)
if (err != nil) != tc.OutErr {
t.Fatalf("%s: err: %s", name, err)
}
if outComputed != tc.OutComputed {
t.Fatalf("%s: err: %#v", name, outComputed)
if out.Computed != tc.OutComputed {
t.Fatalf("%s: err: %#v", name, out.Computed)
}
if s, ok := out.(*Set); ok {
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out = s.List()
out.Value = s.List()
}
if !reflect.DeepEqual(out, tc.Out) {
t.Fatalf("%s: out: %#v", name, out)
if !reflect.DeepEqual(out.Value, tc.Out) {
t.Fatalf("%s: out: %#v", name, out.Value)
}
if outOk != tc.OutOk {
t.Fatalf("%s: outOk: %#v", name, outOk)
if out.Exists != tc.OutOk {
t.Fatalf("%s: outOk: %#v", name, out.Exists)
}
}
}

View File

@ -0,0 +1,152 @@
package schema
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
// DiffFieldReader reads fields out of a diff structures.
type DiffFieldReader struct {
Diff *terraform.InstanceDiff
}
func (r *DiffFieldReader) ReadField(
address []string, schema *Schema) (FieldReadResult, 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 *DiffFieldReader) readMap(k string) (FieldReadResult, error) {
result := make(map[string]interface{})
negresult := make(map[string]interface{})
resultSet := false
prefix := k + "."
for k, v := range r.Diff.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
resultSet = true
k = k[len(prefix):]
if v.NewRemoved {
negresult[k] = ""
continue
}
result[k] = v.New
}
var resultVal interface{}
if resultSet {
resultVal = result
}
return FieldReadResult{
Value: resultVal,
NegValue: negresult,
Exists: resultSet,
}, nil
}
func (r *DiffFieldReader) readPrimitive(
k string, schema *Schema) (FieldReadResult, error) {
attrD, ok := r.Diff.Attributes[k]
if !ok {
return FieldReadResult{}, nil
}
if attrD.NewComputed {
return FieldReadResult{
Exists: true,
Computed: true,
}, nil
}
result := attrD.New
if attrD.NewExtra != nil {
if err := mapstructure.WeakDecode(attrD.NewExtra, &result); err != nil {
return FieldReadResult{}, err
}
}
returnVal, err := stringToPrimitive(result, false, schema)
if err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: returnVal,
Exists: true,
}, nil
}
func (r *DiffFieldReader) readSet(
k string, schema *Schema) (FieldReadResult, error) {
// Create the set that will be our result
set := &Set{F: schema.Set}
// 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 the map and find all the set items
prefix := k + "."
for k, _ := range r.Diff.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
if strings.HasPrefix(k, prefix+"#") {
// Ignore the count field
continue
}
// Split the key, since it might be a sub-object like "idx.field"
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
raw, err := r.ReadField([]string{prefix + idx}, elemSchema)
if err != nil {
return FieldReadResult{}, err
}
if !raw.Exists {
// This shouldn't happen because we just verified it does exist
panic("missing field in set: " + k + "." + idx)
}
set.Add(raw.Value)
}
return FieldReadResult{
Value: set,
Exists: true,
}, nil
}

View File

@ -0,0 +1,313 @@
package schema
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffFieldReader_impl(t *testing.T) {
var _ FieldReader = new(DiffFieldReader)
}
func TestDiffFieldReader(t *testing.T) {
r := &DiffFieldReader{
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"bool": &terraform.ResourceAttrDiff{
Old: "",
New: "true",
},
"int": &terraform.ResourceAttrDiff{
Old: "",
New: "42",
},
"string": &terraform.ResourceAttrDiff{
Old: "",
New: "string",
},
"list.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"list.0": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"list.1": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
"listInt.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"listInt.0": &terraform.ResourceAttrDiff{
Old: "",
New: "21",
},
"listInt.1": &terraform.ResourceAttrDiff{
Old: "",
New: "42",
},
"map.foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
"map.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
"mapRemove.foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
"mapRemove.bar": &terraform.ResourceAttrDiff{
NewRemoved: true,
},
"set.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"set.10": &terraform.ResourceAttrDiff{
Old: "",
New: "10",
},
"set.50": &terraform.ResourceAttrDiff{
Old: "",
New: "50",
},
"setDeep.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"setDeep.10.index": &terraform.ResourceAttrDiff{
Old: "",
New: "10",
},
"setDeep.10.value": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"setDeep.50.index": &terraform.ResourceAttrDiff{
Old: "",
New: "50",
},
"setDeep.50.value": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
}
cases := map[string]struct {
Addr []string
Schema *Schema
Result FieldReadResult
Err bool
}{
"noexist": {
[]string{"boolNOPE"},
&Schema{Type: TypeBool},
FieldReadResult{
Value: nil,
Exists: false,
Computed: false,
},
false,
},
"bool": {
[]string{"bool"},
&Schema{Type: TypeBool},
FieldReadResult{
Value: true,
Exists: true,
Computed: false,
},
false,
},
"int": {
[]string{"int"},
&Schema{Type: TypeInt},
FieldReadResult{
Value: 42,
Exists: true,
Computed: false,
},
false,
},
"string": {
[]string{"string"},
&Schema{Type: TypeString},
FieldReadResult{
Value: "string",
Exists: true,
Computed: false,
},
false,
},
"list": {
[]string{"list"},
&Schema{
Type: TypeList,
Elem: &Schema{Type: TypeString},
},
FieldReadResult{
Value: []interface{}{
"foo",
"bar",
},
Exists: true,
Computed: false,
},
false,
},
"listInt": {
[]string{"listInt"},
&Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
FieldReadResult{
Value: []interface{}{
21,
42,
},
Exists: true,
Computed: false,
},
false,
},
"map": {
[]string{"map"},
&Schema{Type: TypeMap},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
NegValue: map[string]interface{}{},
Exists: true,
Computed: false,
},
false,
},
"mapelem": {
[]string{"map", "foo"},
&Schema{Type: TypeString},
FieldReadResult{
Value: "bar",
Exists: true,
Computed: false,
},
false,
},
"mapRemove": {
[]string{"mapRemove"},
&Schema{Type: TypeMap},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
},
NegValue: map[string]interface{}{
"bar": "",
},
Exists: true,
Computed: false,
},
false,
},
"set": {
[]string{"set"},
&Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
FieldReadResult{
Value: []interface{}{10, 50},
Exists: true,
Computed: 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)
},
},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
Exists: true,
Computed: false,
},
false,
},
}
for name, tc := range cases {
out, err := r.ReadField(tc.Addr, tc.Schema)
if (err != nil) != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out.Value = s.List()
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}

View File

@ -12,7 +12,7 @@ type MapFieldReader struct {
}
func (r *MapFieldReader) ReadField(
address []string, schema *Schema) (interface{}, bool, bool, error) {
address []string, schema *Schema) (FieldReadResult, error) {
k := strings.Join(address, ".")
switch schema.Type {
@ -35,7 +35,7 @@ func (r *MapFieldReader) ReadField(
}
}
func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) {
func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) {
result := make(map[string]interface{})
resultSet := false
@ -54,43 +54,52 @@ func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) {
resultVal = result
}
return resultVal, resultSet, false, nil
return FieldReadResult{
Value: resultVal,
Exists: resultSet,
}, nil
}
func (r *MapFieldReader) readPrimitive(
k string, schema *Schema) (interface{}, bool, bool, error) {
k string, schema *Schema) (FieldReadResult, error) {
result, ok := r.Map[k]
if !ok {
return nil, false, false, nil
return FieldReadResult{}, nil
}
returnVal, err := stringToPrimitive(result, false, schema)
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
return returnVal, true, false, nil
return FieldReadResult{
Value: returnVal,
Exists: true,
}, nil
}
func (r *MapFieldReader) readSet(
k string, schema *Schema) (interface{}, bool, bool, error) {
k string, schema *Schema) (FieldReadResult, error) {
// Get the number of elements in the list
countRaw, countOk, countComputed, err := r.readPrimitive(
k+".#", &Schema{Type: TypeInt})
countRaw, err := r.readPrimitive(k+".#", &Schema{Type: TypeInt})
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
if !countOk {
if !countRaw.Exists {
// No count, means we have no list
countRaw = 0
countRaw.Value = 0
}
// Create the set that will be our result
set := &Set{F: schema.Set}
// If we have an empty list, then return an empty list
if countComputed || countRaw.(int) == 0 {
return set, true, countComputed, nil
if countRaw.Computed || countRaw.Value.(int) == 0 {
return FieldReadResult{
Value: set,
Exists: true,
Computed: countRaw.Computed,
}, nil
}
// Get the schema for the elements
@ -120,17 +129,20 @@ func (r *MapFieldReader) readSet(
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
v, ok, _, err := r.ReadField([]string{prefix + idx}, elemSchema)
raw, err := r.ReadField([]string{prefix + idx}, elemSchema)
if err != nil {
return nil, false, false, err
return FieldReadResult{}, err
}
if !ok {
if !raw.Exists {
// This shouldn't happen because we just verified it does exist
panic("missing field in set: " + k + "." + idx)
}
set.Add(v)
set.Add(raw.Value)
}
return set, true, false, nil
return FieldReadResult{
Value: set,
Exists: true,
}, nil
}

View File

@ -180,24 +180,24 @@ func TestMapFieldReader(t *testing.T) {
}
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)
out, err := r.ReadField(tc.Addr, tc.Schema)
if (err != nil) != tc.OutErr {
t.Fatalf("%s: err: %s", name, err)
}
if outComputed != tc.OutComputed {
t.Fatalf("%s: err: %#v", name, outComputed)
if out.Computed != tc.OutComputed {
t.Fatalf("%s: err: %#v", name, out.Computed)
}
if s, ok := out.(*Set); ok {
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out = s.List()
out.Value = s.List()
}
if !reflect.DeepEqual(out, tc.Out) {
t.Fatalf("%s: out: %#v", name, out)
if !reflect.DeepEqual(out.Value, tc.Out) {
t.Fatalf("%s: out: %#v", name, out.Value)
}
if outOk != tc.OutOk {
t.Fatalf("%s: outOk: %#v", name, outOk)
if out.Exists != tc.OutOk {
t.Fatalf("%s: outOk: %#v", name, out.Exists)
}
}
}