Merge pull request #764 from hashicorp/f-schema-typed-readers

Refactor of helper/schema to prepare for better state formats
This commit is contained in:
Mitchell Hashimoto 2015-01-10 15:21:33 -08:00
commit 12d20a8d52
18 changed files with 2937 additions and 1250 deletions

View File

@ -0,0 +1,269 @@
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) (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{}
ValueProcessed 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
}
// ValueOrZero returns the value of this result or the zero value of the
// schema type, ensuring a consistent non-nil return value.
func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
if r.Value != nil {
return r.Value
}
result := s.Type.Zero()
// The zero value of a set is nil, but we want it
// to actually be an empty set object...
if s.Type == TypeSet && result == nil {
result = &Set{F: s.Set}
}
return result
}
// addrToSchema finds the final element schema for the given address
// and the given schema. It returns all the schemas that led to the final
// schema. These are in order of the address (out to in).
func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
current := &Schema{
Type: typeObject,
Elem: schemaMap,
}
// If we aren't given an address, then the user is requesting the
// full object, so we return the special value which is the full object.
if len(addr) == 0 {
return []*Schema{current}
}
result := make([]*Schema, 0, len(addr))
for len(addr) > 0 {
k := addr[0]
addr = addr[1:]
REPEAT:
// We want to trim off the first "typeObject" since its not a
// real lookup that people do. i.e. []string{"foo"} in a structure
// isn't {typeObject, typeString}, its just a {typeString}.
if len(result) > 0 || current.Type != typeObject {
result = append(result, current)
}
switch t := current.Type; t {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
if len(addr) > 0 {
return nil
}
case TypeList:
fallthrough
case TypeSet:
switch v := current.Elem.(type) {
case *Resource:
current = &Schema{
Type: typeObject,
Elem: v.Schema,
}
case *Schema:
current = v
default:
return nil
}
// If we only have one more thing and the the next thing
// is a #, then we're accessing the index which is always
// an int.
if len(addr) > 0 && addr[0] == "#" {
current = &Schema{Type: TypeInt}
break
}
case TypeMap:
if len(addr) > 0 {
current = &Schema{Type: TypeString}
}
case typeObject:
// If we're already in the object, then we want to handle Sets
// and Lists specially. Basically, their next key is the lookup
// key (the set value or the list element). For these scenarios,
// we just want to skip it and move to the next element if there
// is one.
if len(result) > 0 {
lastType := result[len(result)-2].Type
if lastType == TypeSet || lastType == TypeList {
if len(addr) == 0 {
break
}
k = addr[0]
addr = addr[1:]
}
}
m := current.Elem.(map[string]*Schema)
val, ok := m[k]
if !ok {
return nil
}
current = val
goto REPEAT
}
}
return result
}
// 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, addr []string, schema *Schema) (FieldReadResult, error) {
addrPadded := make([]string, len(addr)+1)
copy(addrPadded, addr)
addrPadded[len(addrPadded)-1] = "#"
// Get the number of elements in the list
countResult, err := r.ReadField(addrPadded)
if err != nil {
return FieldReadResult{}, err
}
if !countResult.Exists {
// No count, means we have no list
countResult.Value = 0
}
// If we have an empty list, then return an empty list
if countResult.Computed || countResult.Value.(int) == 0 {
return FieldReadResult{
Value: []interface{}{},
Exists: countResult.Exists,
Computed: countResult.Computed,
}, nil
}
// Go through each count, and get the item value out of it
result := make([]interface{}, countResult.Value.(int))
for i, _ := range result {
is := strconv.FormatInt(int64(i), 10)
addrPadded[len(addrPadded)-1] = is
rawResult, err := r.ReadField(addrPadded)
if err != nil {
return FieldReadResult{}, err
}
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.
rawResult.Value = nil
}
result[i] = rawResult.Value
}
return FieldReadResult{
Value: result,
Exists: true,
}, 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,
addr []string,
schema map[string]*Schema) (FieldReadResult, error) {
result := make(map[string]interface{})
exists := false
for field, s := range schema {
addrRead := make([]string, len(addr), len(addr)+1)
copy(addrRead, addr)
addrRead = append(addrRead, field)
rawResult, err := r.ReadField(addrRead)
if err != nil {
return FieldReadResult{}, err
}
if rawResult.Exists {
exists = true
}
result[field] = rawResult.ValueOrZero(s)
}
return FieldReadResult{
Value: result,
Exists: exists,
}, 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
}

View File

@ -0,0 +1,236 @@
package schema
import (
"fmt"
"strconv"
"strings"
"sync"
"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
Schema map[string]*Schema
lock sync.Mutex
}
func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) {
return r.readField(address, false)
}
func (r *ConfigFieldReader) readField(
address []string, nested bool) (FieldReadResult, error) {
schemaList := addrToSchema(address, r.Schema)
if len(schemaList) == 0 {
return FieldReadResult{}, nil
}
if !nested {
// If we have a set anywhere in the address, then we need to
// read that set out in order and actually replace that part of
// the address with the real list index. i.e. set.50 might actually
// map to set.12 in the config, since it is in list order in the
// config, not indexed by set value.
for i, v := range schemaList {
// Sets are the only thing that cause this issue.
if v.Type != TypeSet {
continue
}
// If we're at the end of the list, then we don't have to worry
// about this because we're just requesting the whole set.
if i == len(schemaList)-1 {
continue
}
// If we're looking for the count, then ignore...
if address[i+1] == "#" {
continue
}
// Get the code
code, err := strconv.ParseInt(address[i+1], 0, 0)
if err != nil {
return FieldReadResult{}, err
}
// Get the set so we can get the index map that tells us the
// mapping of the hash code to the list index
_, indexMap, err := r.readSet(address[:i+1], v)
if err != nil {
return FieldReadResult{}, err
}
index, ok := indexMap[int(code)]
if !ok {
return FieldReadResult{}, nil
}
address[i+1] = strconv.FormatInt(int64(index), 10)
}
}
k := strings.Join(address, ".")
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
return r.readPrimitive(k, schema)
case TypeList:
return readListField(&nestedConfigFieldReader{r}, address, schema)
case TypeMap:
return r.readMap(k)
case TypeSet:
result, _, err := r.readSet(address, schema)
return result, err
case typeObject:
return readObjectField(
&nestedConfigFieldReader{r},
address, schema.Elem.(map[string]*Schema))
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
}
func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
mraw, ok := r.Config.Get(k)
if !ok {
return FieldReadResult{}, 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 FieldReadResult{
Value: result,
Exists: true,
}, nil
}
func (r *ConfigFieldReader) readPrimitive(
k string, schema *Schema) (FieldReadResult, error) {
raw, ok := r.Config.Get(k)
if !ok {
return FieldReadResult{}, nil
}
var result string
if err := mapstructure.WeakDecode(raw, &result); err != nil {
return FieldReadResult{}, err
}
computed := r.Config.IsComputed(k)
returnVal, err := stringToPrimitive(result, computed, schema)
if err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: returnVal,
Exists: true,
Computed: computed,
}, nil
}
func (r *ConfigFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, map[int]int, error) {
indexMap := make(map[int]int)
// Create the set that will be our result
set := &Set{F: schema.Set}
raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
if err != nil {
return FieldReadResult{}, indexMap, err
}
if !raw.Exists {
return FieldReadResult{Value: set}, indexMap, nil
}
// If the list is computed, the set is necessarilly computed
if raw.Computed {
return FieldReadResult{
Value: set,
Exists: true,
Computed: raw.Computed,
}, indexMap, nil
}
// Build up the set from the list elements
for i, v := range raw.Value.([]interface{}) {
// Check if any of the keys in this item are computed
computed := r.hasComputedSubKeys(
fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema)
code := set.add(v)
indexMap[code] = i
if computed {
set.m[-code] = set.m[code]
delete(set.m, code)
code = -code
}
}
return FieldReadResult{
Value: set,
Exists: true,
}, indexMap, nil
}
// hasComputedSubKeys walks through a schema and returns whether or not the
// given key contains any subkeys that are computed.
func (r *ConfigFieldReader) hasComputedSubKeys(key string, schema *Schema) bool {
prefix := key + "."
switch t := schema.Elem.(type) {
case *Resource:
for k, schema := range t.Schema {
if r.Config.IsComputed(prefix + k) {
return true
}
if r.hasComputedSubKeys(prefix+k, schema) {
return true
}
}
}
return false
}
// nestedConfigFieldReader is a funny little thing that just wraps a
// ConfigFieldReader to call readField when ReadField is called so that
// we don't recalculate the set rewrites in the address, which leads to
// an infinite loop.
type nestedConfigFieldReader struct {
Reader *ConfigFieldReader
}
func (r *nestedConfigFieldReader) ReadField(
address []string) (FieldReadResult, error) {
return r.Reader.readField(address, true)
}

View File

@ -0,0 +1,57 @@
package schema
import (
"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) {
testFieldReader(t, func(s map[string]*Schema) FieldReader {
return &ConfigFieldReader{
Schema: s,
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",
},
},
}),
}
})
}
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)
}

View File

@ -0,0 +1,174 @@
package schema
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
// DiffFieldReader reads fields out of a diff structures.
//
// It also requires access to a Reader that reads fields from the structure
// that the diff was derived from. This is usually the state. This is required
// because a diff on its own doesn't have complete data about full objects
// such as maps.
//
// The Source MUST be the data that the diff was derived from. If it isn't,
// the behavior of this struct is undefined.
//
// Reading fields from a DiffFieldReader is identical to reading from
// Source except the diff will be applied to the end result.
//
// The "Exists" field on the result will be set to true if the complete
// field exists whether its from the source, diff, or a combination of both.
// It cannot be determined whether a retrieved value is composed of
// diff elements.
type DiffFieldReader struct {
Diff *terraform.InstanceDiff
Source FieldReader
Schema map[string]*Schema
}
func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) {
schemaList := addrToSchema(address, r.Schema)
if len(schemaList) == 0 {
return FieldReadResult{}, nil
}
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
return r.readPrimitive(address, schema)
case TypeList:
return readListField(r, address, schema)
case TypeMap:
return r.readMap(address, schema)
case TypeSet:
return r.readSet(address, schema)
case typeObject:
return readObjectField(r, address, schema.Elem.(map[string]*Schema))
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
}
func (r *DiffFieldReader) readMap(
address []string, schema *Schema) (FieldReadResult, error) {
result := make(map[string]interface{})
resultSet := false
// First read the map from the underlying source
source, err := r.Source.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
if source.Exists {
result = source.Value.(map[string]interface{})
resultSet = true
}
// Next, read all the elements we have in our diff, and apply
// the diff to our result.
prefix := strings.Join(address, ".") + "."
for k, v := range r.Diff.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
resultSet = true
k = k[len(prefix):]
if v.NewRemoved {
delete(result, k)
continue
}
result[k] = v.New
}
var resultVal interface{}
if resultSet {
resultVal = result
}
return FieldReadResult{
Value: resultVal,
Exists: resultSet,
}, nil
}
func (r *DiffFieldReader) readPrimitive(
address []string, schema *Schema) (FieldReadResult, error) {
result, err := r.Source.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
attrD, ok := r.Diff.Attributes[strings.Join(address, ".")]
if !ok {
return result, nil
}
var resultVal string
if !attrD.NewComputed {
resultVal = attrD.New
if attrD.NewExtra != nil {
result.ValueProcessed = resultVal
if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil {
return FieldReadResult{}, err
}
}
}
result.Computed = attrD.NewComputed
result.Exists = true
result.Value, err = stringToPrimitive(resultVal, false, schema)
if err != nil {
return FieldReadResult{}, err
}
return result, nil
}
func (r *DiffFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, error) {
// Create the set that will be our result
set := &Set{F: schema.Set}
// Go through the map and find all the set items
prefix := strings.Join(address, ".") + "."
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(append(address, idx))
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: set.Len() > 0,
}, nil
}

View File

@ -0,0 +1,286 @@
package schema
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffFieldReader_impl(t *testing.T) {
var _ FieldReader = new(DiffFieldReader)
}
func TestDiffFieldReader_extra(t *testing.T) {
schema := map[string]*Schema{
"stringComputed": &Schema{Type: TypeString},
"listMap": &Schema{
Type: TypeList,
Elem: &Schema{
Type: TypeMap,
},
},
"mapRemove": &Schema{Type: TypeMap},
"setChange": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"value": &Schema{
Type: TypeString,
Required: true,
},
},
},
Set: func(a interface{}) int {
m := a.(map[string]interface{})
return m["index"].(int)
},
},
}
r := &DiffFieldReader{
Schema: schema,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"stringComputed": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
NewComputed: true,
},
"listMap.0.bar": &terraform.ResourceAttrDiff{
NewRemoved: true,
},
"mapRemove.bar": &terraform.ResourceAttrDiff{
NewRemoved: true,
},
"setChange.10.value": &terraform.ResourceAttrDiff{
Old: "50",
New: "80",
},
},
},
Source: &MapFieldReader{
Schema: schema,
Map: BasicMapReader(map[string]string{
"listMap.#": "2",
"listMap.0.foo": "bar",
"listMap.0.bar": "baz",
"listMap.1.baz": "baz",
"mapRemove.foo": "bar",
"mapRemove.bar": "bar",
"setChange.#": "1",
"setChange.10.index": "10",
"setChange.10.value": "50",
}),
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Err bool
}{
"stringComputed": {
[]string{"stringComputed"},
FieldReadResult{
Value: "",
Exists: true,
Computed: true,
},
false,
},
"listMapRemoval": {
[]string{"listMap"},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"baz": "baz",
},
},
Exists: true,
},
false,
},
"mapRemove": {
[]string{"mapRemove"},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
},
Exists: true,
Computed: false,
},
false,
},
"setChange": {
[]string{"setChange"},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"index": 10,
"value": "80",
},
},
Exists: true,
},
false,
},
}
for name, tc := range cases {
out, err := r.ReadField(tc.Addr)
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)
}
}
}
func TestDiffFieldReader(t *testing.T) {
testFieldReader(t, func(s map[string]*Schema) FieldReader {
return &DiffFieldReader{
Schema: s,
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",
},
"stringComputed": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
NewComputed: true,
},
"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",
},
"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",
},
},
},
Source: &MapFieldReader{
Schema: s,
Map: BasicMapReader(map[string]string{
"listMap.#": "2",
"listMap.0.foo": "bar",
"listMap.0.bar": "baz",
"listMap.1.baz": "baz",
}),
},
}
})
}

View File

@ -0,0 +1,227 @@
package schema
import (
"fmt"
"strings"
)
// MapFieldReader reads fields out of an untyped map[string]string to
// the best of its ability.
type MapFieldReader struct {
Map MapReader
Schema map[string]*Schema
}
func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) {
k := strings.Join(address, ".")
schemaList := addrToSchema(address, r.Schema)
if len(schemaList) == 0 {
return FieldReadResult{}, nil
}
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
return r.readPrimitive(address, schema)
case TypeList:
return readListField(r, address, schema)
case TypeMap:
return r.readMap(k)
case TypeSet:
return r.readSet(address, schema)
case typeObject:
return readObjectField(r, address, schema.Elem.(map[string]*Schema))
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
}
func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) {
result := make(map[string]interface{})
resultSet := false
// If the name of the map field is directly in the map with an
// empty string, it means that the map is being deleted, so mark
// that is is set.
if v, ok := r.Map.Access(k); ok && v == "" {
resultSet = true
}
prefix := k + "."
r.Map.Range(func(k, v string) bool {
if strings.HasPrefix(k, prefix) {
result[k[len(prefix):]] = v
resultSet = true
}
return true
})
var resultVal interface{}
if resultSet {
resultVal = result
}
return FieldReadResult{
Value: resultVal,
Exists: resultSet,
}, nil
}
func (r *MapFieldReader) readPrimitive(
address []string, schema *Schema) (FieldReadResult, error) {
k := strings.Join(address, ".")
result, ok := r.Map.Access(k)
if !ok {
return FieldReadResult{}, nil
}
returnVal, err := stringToPrimitive(result, false, schema)
if err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: returnVal,
Exists: true,
}, nil
}
func (r *MapFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, error) {
// Get the number of elements in the list
countRaw, err := r.readPrimitive(
append(address, "#"), &Schema{Type: TypeInt})
if err != nil {
return FieldReadResult{}, err
}
if !countRaw.Exists {
// No count, means we have no list
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 countRaw.Computed || countRaw.Value.(int) == 0 {
return FieldReadResult{
Value: set,
Exists: countRaw.Exists,
Computed: countRaw.Computed,
}, nil
}
// Go through the map and find all the set items
prefix := strings.Join(address, ".") + "."
countExpected := countRaw.Value.(int)
countActual := make(map[string]struct{})
completed := r.Map.Range(func(k, _ string) bool {
if !strings.HasPrefix(k, prefix) {
return true
}
if strings.HasPrefix(k, prefix+"#") {
// Ignore the count field
return true
}
// Split the key, since it might be a sub-object like "idx.field"
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
var raw FieldReadResult
raw, err = r.ReadField(append(address, idx))
if err != nil {
return false
}
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)
// Due to the way multimap readers work, if we've seen the number
// of fields we expect, then exit so that we don't read later values.
// For example: the "set" map might have "ports.#", "ports.0", and
// "ports.1", but the "state" map might have those plus "ports.2".
// We don't want "ports.2"
countActual[idx] = struct{}{}
if len(countActual) >= countExpected {
return false
}
return true
})
if !completed && err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: set,
Exists: true,
}, nil
}
// MapReader is an interface that is given to MapFieldReader for accessing
// a "map". This can be used to have alternate implementations. For a basic
// map[string]string, use BasicMapReader.
type MapReader interface {
Access(string) (string, bool)
Range(func(string, string) bool) bool
}
// BasicMapReader implements MapReader for a single map.
type BasicMapReader map[string]string
func (r BasicMapReader) Access(k string) (string, bool) {
v, ok := r[k]
return v, ok
}
func (r BasicMapReader) Range(f func(string, string) bool) bool {
for k, v := range r {
if cont := f(k, v); !cont {
return false
}
}
return true
}
// MultiMapReader reads over multiple maps, preferring keys that are
// founder earlier (lower number index) vs. later (higher number index)
type MultiMapReader []map[string]string
func (r MultiMapReader) Access(k string) (string, bool) {
for _, m := range r {
if v, ok := m[k]; ok {
return v, ok
}
}
return "", false
}
func (r MultiMapReader) Range(f func(string, string) bool) bool {
done := make(map[string]struct{})
for _, m := range r {
for k, v := range m {
if _, ok := done[k]; ok {
continue
}
if cont := f(k, v); !cont {
return false
}
done[k] = struct{}{}
}
}
return true
}

View File

@ -0,0 +1,95 @@
package schema
import (
"reflect"
"testing"
)
func TestMapFieldReader_impl(t *testing.T) {
var _ FieldReader = new(MapFieldReader)
}
func TestMapFieldReader(t *testing.T) {
testFieldReader(t, func(s map[string]*Schema) FieldReader {
return &MapFieldReader{
Schema: s,
Map: BasicMapReader(map[string]string{
"bool": "true",
"int": "42",
"string": "string",
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
"listInt.#": "2",
"listInt.0": "21",
"listInt.1": "42",
"map.foo": "bar",
"map.bar": "baz",
"set.#": "2",
"set.10": "10",
"set.50": "50",
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
}),
}
})
}
func TestMapFieldReader_extra(t *testing.T) {
r := &MapFieldReader{
Schema: map[string]*Schema{
"mapDel": &Schema{Type: TypeMap},
},
Map: BasicMapReader(map[string]string{
"mapDel": "",
}),
}
cases := map[string]struct {
Addr []string
Out interface{}
OutOk bool
OutComputed bool
OutErr bool
}{
"mapDel": {
[]string{"mapDel"},
map[string]interface{}{},
true,
false,
false,
},
}
for name, tc := range cases {
out, err := r.ReadField(tc.Addr)
if (err != nil) != tc.OutErr {
t.Fatalf("%s: err: %s", name, err)
}
if out.Computed != tc.OutComputed {
t.Fatalf("%s: err: %#v", name, out.Computed)
}
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(out.Value, tc.Out) {
t.Fatalf("%s: out: %#v", name, out.Value)
}
if out.Exists != tc.OutOk {
t.Fatalf("%s: outOk: %#v", name, out.Exists)
}
}
}

View File

@ -0,0 +1,63 @@
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) (FieldReadResult, error) {
return r.ReadFieldMerge(address, r.Levels[len(r.Levels)-1])
}
func (r *MultiLevelFieldReader) ReadFieldExact(
address []string, 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)
if err != nil {
return FieldReadResult{}, fmt.Errorf(
"Error reading level %s: %s", level, err)
}
return result, nil
}
func (r *MultiLevelFieldReader) ReadFieldMerge(
address []string, level string) (FieldReadResult, error) {
var result FieldReadResult
for _, l := range r.Levels {
if r, ok := r.Readers[l]; ok {
out, err := r.ReadField(address)
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
}

View File

@ -0,0 +1,270 @@
package schema
import (
"reflect"
"strconv"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) {
cases := map[string]struct {
Addr []string
Readers []FieldReader
Level string
Result FieldReadResult
}{
"specific": {
Addr: []string{"foo"},
Readers: []FieldReader{
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"foo": "bar",
}),
},
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"foo": "baz",
}),
},
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(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.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
Readers []FieldReader
Result FieldReadResult
}{
"stringInDiff": {
Addr: []string{"availability_zone"},
Readers: []FieldReader{
&DiffFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Source: &MapFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Map: BasicMapReader(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"},
Readers: []FieldReader{
&MapFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"availability_zone": "foo",
}),
},
&DiffFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Source: &MapFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Map: BasicMapReader(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"},
Readers: []FieldReader{
&DiffFieldReader{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
Source: &MapFieldReader{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
Map: BasicMapReader(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"},
Readers: []FieldReader{
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"foo": "bar",
}),
},
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(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, 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)
}
}
}

View File

@ -0,0 +1,390 @@
package schema
import (
"reflect"
"testing"
)
func TestAddrToSchema(t *testing.T) {
cases := map[string]struct {
Addr []string
Schema map[string]*Schema
Result []ValueType
}{
"full object": {
[]string{},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{typeObject},
},
"list": {
[]string{"list"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{TypeList},
},
"list.#": {
[]string{"list", "#"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{TypeList, TypeInt},
},
"list.0": {
[]string{"list", "0"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{TypeList, TypeInt},
},
"list.0 with resource": {
[]string{"list", "0"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"field": &Schema{Type: TypeString},
},
},
},
},
[]ValueType{TypeList, typeObject},
},
"list.0.field": {
[]string{"list", "0", "field"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"field": &Schema{Type: TypeString},
},
},
},
},
[]ValueType{TypeList, typeObject, TypeString},
},
"set": {
[]string{"set"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
[]ValueType{TypeSet},
},
"set.#": {
[]string{"set", "#"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
[]ValueType{TypeSet, TypeInt},
},
"set.0": {
[]string{"set", "0"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
[]ValueType{TypeSet, TypeInt},
},
"set.0 with resource": {
[]string{"set", "0"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"field": &Schema{Type: TypeString},
},
},
},
},
[]ValueType{TypeSet, typeObject},
},
"mapElem": {
[]string{"map", "foo"},
map[string]*Schema{
"map": &Schema{Type: TypeMap},
},
[]ValueType{TypeMap, TypeString},
},
"setDeep": {
[]string{"set", "50", "index"},
map[string]*Schema{
"set": &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)
},
},
},
[]ValueType{TypeSet, typeObject, TypeInt},
},
}
for name, tc := range cases {
result := addrToSchema(tc.Addr, tc.Schema)
types := make([]ValueType, len(result))
for i, v := range result {
types[i] = v.Type
}
if !reflect.DeepEqual(types, tc.Result) {
t.Fatalf("%s: %#v", name, types)
}
}
}
// testFieldReader is a helper that should be used to verify that
// a FieldReader behaves properly in all the common cases.
func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) {
schema := map[string]*Schema{
// Primitives
"bool": &Schema{Type: TypeBool},
"int": &Schema{Type: TypeInt},
"string": &Schema{Type: TypeString},
// Lists
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeString},
},
"listInt": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
"listMap": &Schema{
Type: TypeList,
Elem: &Schema{
Type: TypeMap,
},
},
// Maps
"map": &Schema{Type: TypeMap},
// Sets
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
"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)
},
},
"setEmpty": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Err bool
}{
"noexist": {
[]string{"boolNOPE"},
FieldReadResult{
Value: nil,
Exists: false,
Computed: false,
},
false,
},
"bool": {
[]string{"bool"},
FieldReadResult{
Value: true,
Exists: true,
Computed: false,
},
false,
},
"int": {
[]string{"int"},
FieldReadResult{
Value: 42,
Exists: true,
Computed: false,
},
false,
},
"string": {
[]string{"string"},
FieldReadResult{
Value: "string",
Exists: true,
Computed: false,
},
false,
},
"list": {
[]string{"list"},
FieldReadResult{
Value: []interface{}{
"foo",
"bar",
},
Exists: true,
Computed: false,
},
false,
},
"listInt": {
[]string{"listInt"},
FieldReadResult{
Value: []interface{}{
21,
42,
},
Exists: true,
Computed: false,
},
false,
},
"map": {
[]string{"map"},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
Exists: true,
Computed: false,
},
false,
},
"mapelem": {
[]string{"map", "foo"},
FieldReadResult{
Value: "bar",
Exists: true,
Computed: false,
},
false,
},
"set": {
[]string{"set"},
FieldReadResult{
Value: []interface{}{10, 50},
Exists: true,
Computed: false,
},
false,
},
"setDeep": {
[]string{"setDeep"},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
Exists: true,
Computed: false,
},
false,
},
"setEmpty": {
[]string{"setEmpty"},
FieldReadResult{
Value: []interface{}{},
Exists: false,
},
false,
},
}
for name, tc := range cases {
r := f(schema)
out, err := r.ReadField(tc.Addr)
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

@ -0,0 +1,8 @@
package schema
// FieldWriters are responsible for writing fields by address into
// a proper typed representation. ResourceData uses this to write new data
// into existing sources.
type FieldWriter interface {
WriteField([]string, interface{}) error
}

View File

@ -0,0 +1,303 @@
package schema
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/mitchellh/mapstructure"
)
// MapFieldWriter writes data into a single map[string]string structure.
type MapFieldWriter struct {
Schema map[string]*Schema
lock sync.Mutex
result map[string]string
}
// Map returns the underlying map that is being written to.
func (w *MapFieldWriter) Map() map[string]string {
w.lock.Lock()
defer w.lock.Unlock()
if w.result == nil {
w.result = make(map[string]string)
}
return w.result
}
func (w *MapFieldWriter) WriteField(addr []string, value interface{}) error {
w.lock.Lock()
defer w.lock.Unlock()
if w.result == nil {
w.result = make(map[string]string)
}
schemaList := addrToSchema(addr, w.Schema)
if len(schemaList) == 0 {
return fmt.Errorf("Invalid address to set: %#v", addr)
}
// If we're setting anything other than a list root or set root,
// then disallow it.
for _, schema := range schemaList[:len(schemaList)-1] {
if schema.Type == TypeList {
return fmt.Errorf(
"%s: can only set full list",
strings.Join(addr, "."))
}
if schema.Type == TypeMap {
return fmt.Errorf(
"%s: can only set full map",
strings.Join(addr, "."))
}
if schema.Type == TypeSet {
return fmt.Errorf(
"%s: can only set full set",
strings.Join(addr, "."))
}
}
return w.set(addr, value)
}
func (w *MapFieldWriter) set(addr []string, value interface{}) error {
schemaList := addrToSchema(addr, w.Schema)
if len(schemaList) == 0 {
return fmt.Errorf("Invalid address to set: %#v", addr)
}
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
return w.setPrimitive(addr, value, schema)
case TypeList:
return w.setList(addr, value, schema)
case TypeMap:
return w.setMap(addr, value, schema)
case TypeSet:
return w.setSet(addr, value, schema)
case typeObject:
return w.setObject(addr, value, schema)
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
}
func (w *MapFieldWriter) setList(
addr []string,
v interface{},
schema *Schema) error {
k := strings.Join(addr, ".")
setElement := func(idx string, value interface{}) error {
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
return w.set(append(addrCopy, idx), value)
}
var vs []interface{}
if err := mapstructure.Decode(v, &vs); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
// Set the entire list.
var err error
for i, elem := range vs {
is := strconv.FormatInt(int64(i), 10)
err = setElement(is, elem)
if err != nil {
break
}
}
if err != nil {
for i, _ := range vs {
is := strconv.FormatInt(int64(i), 10)
setElement(is, nil)
}
return err
}
w.result[k+".#"] = strconv.FormatInt(int64(len(vs)), 10)
return nil
}
func (w *MapFieldWriter) setMap(
addr []string,
value interface{},
schema *Schema) error {
k := strings.Join(addr, ".")
v := reflect.ValueOf(value)
vs := make(map[string]interface{})
if value != nil {
if v.Kind() != reflect.Map {
return fmt.Errorf("%s: must be a map", k)
}
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("%s: keys must strings", k)
}
for _, mk := range v.MapKeys() {
mv := v.MapIndex(mk)
vs[mk.String()] = mv.Interface()
}
}
if len(vs) == 0 {
// The empty string here means the map is removed.
w.result[k] = ""
return nil
}
// Remove the pure key since we're setting the full map value
delete(w.result, k)
// Set each subkey
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
for subKey, v := range vs {
if err := w.set(append(addrCopy, subKey), v); err != nil {
return err
}
}
return nil
}
func (w *MapFieldWriter) setObject(
addr []string,
value interface{},
schema *Schema) error {
// Set the entire object. First decode into a proper structure
var v map[string]interface{}
if err := mapstructure.Decode(value, &v); err != nil {
return fmt.Errorf("%s: %s", strings.Join(addr, "."), err)
}
// Make space for additional elements in the address
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
// Set each element in turn
var err error
for k1, v1 := range v {
if err = w.set(append(addrCopy, k1), v1); err != nil {
break
}
}
if err != nil {
for k1, _ := range v {
w.set(append(addrCopy, k1), nil)
}
}
return err
}
func (w *MapFieldWriter) setPrimitive(
addr []string,
v interface{},
schema *Schema) error {
k := strings.Join(addr, ".")
if v == nil {
delete(w.result, k)
return nil
}
var set string
switch schema.Type {
case TypeBool:
var b bool
if err := mapstructure.Decode(v, &b); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
set = strconv.FormatBool(b)
case TypeString:
if err := mapstructure.Decode(v, &set); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
case TypeInt:
var n int
if err := mapstructure.Decode(v, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
set = strconv.FormatInt(int64(n), 10)
default:
return fmt.Errorf("Unknown type: %#v", schema.Type)
}
w.result[k] = set
return nil
}
func (w *MapFieldWriter) setSet(
addr []string,
value interface{},
schema *Schema) error {
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
// If it is a slice, then we have to turn it into a *Set so that
// we get the proper order back based on the hash code.
if v := reflect.ValueOf(value); v.Kind() == reflect.Slice {
// Build a temp *ResourceData to use for the conversion
tempSchema := *schema
tempSchema.Type = TypeList
tempSchemaMap := map[string]*Schema{addr[0]: &tempSchema}
tempW := &MapFieldWriter{Schema: tempSchemaMap}
// Set the entire list, this lets us get sane values out of it
if err := tempW.WriteField(addr, value); err != nil {
return err
}
// Build the set by going over the list items in order and
// hashing them into the set. The reason we go over the list and
// not the `value` directly is because this forces all types
// to become []interface{} (generic) instead of []string, which
// most hash functions are expecting.
s := &Set{F: schema.Set}
tempR := &MapFieldReader{
Map: BasicMapReader(tempW.Map()),
Schema: tempSchemaMap,
}
for i := 0; i < v.Len(); i++ {
is := strconv.FormatInt(int64(i), 10)
result, err := tempR.ReadField(append(addrCopy, is))
if err != nil {
return err
}
if !result.Exists {
panic("set item just set doesn't exist")
}
s.Add(result.Value)
}
value = s
}
k := strings.Join(addr, ".")
for code, elem := range value.(*Set).m {
codeStr := strconv.FormatInt(int64(code), 10)
if err := w.set(append(addrCopy, codeStr), elem); err != nil {
return err
}
}
w.result[k+".#"] = strconv.Itoa(value.(*Set).Len())
return nil
}

View File

@ -0,0 +1,199 @@
package schema
import (
"reflect"
"testing"
)
func TestMapFieldWriter_impl(t *testing.T) {
var _ FieldWriter = new(MapFieldWriter)
}
func TestMapFieldWriter(t *testing.T) {
schema := map[string]*Schema{
"bool": &Schema{Type: TypeBool},
"int": &Schema{Type: TypeInt},
"string": &Schema{Type: TypeString},
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeString},
},
"listInt": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
"map": &Schema{Type: TypeMap},
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
"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)
},
},
}
cases := map[string]struct {
Addr []string
Value interface{}
Err bool
Out map[string]string
}{
"noexist": {
[]string{"noexist"},
42,
true,
map[string]string{},
},
"bool": {
[]string{"bool"},
false,
false,
map[string]string{
"bool": "false",
},
},
"int": {
[]string{"int"},
42,
false,
map[string]string{
"int": "42",
},
},
"string": {
[]string{"string"},
"42",
false,
map[string]string{
"string": "42",
},
},
"list of strings": {
[]string{"list"},
[]interface{}{"foo", "bar"},
false,
map[string]string{
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
},
},
"list element": {
[]string{"list", "0"},
"string",
true,
map[string]string{},
},
"map": {
[]string{"map"},
map[string]interface{}{"foo": "bar"},
false,
map[string]string{
"map.foo": "bar",
},
},
"map delete": {
[]string{"map"},
nil,
false,
map[string]string{
"map": "",
},
},
"map element": {
[]string{"map", "foo"},
"bar",
true,
map[string]string{},
},
"set": {
[]string{"set"},
[]interface{}{1, 2, 5},
false,
map[string]string{
"set.#": "3",
"set.1": "1",
"set.2": "2",
"set.5": "5",
},
},
"set resource": {
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
false,
map[string]string{
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
},
},
"set element": {
[]string{"set", "5"},
5,
true,
map[string]string{},
},
"full object": {
nil,
map[string]interface{}{
"string": "foo",
"list": []interface{}{"foo", "bar"},
},
false,
map[string]string{
"string": "foo",
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
},
},
}
for name, tc := range cases {
w := &MapFieldWriter{Schema: schema}
err := w.WriteField(tc.Addr, tc.Value)
if (err != nil) != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%s: bad: %#v", name, actual)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -592,6 +592,29 @@ func TestResourceDataGet(t *testing.T) {
},
},
},
// #19 Empty Set
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Diff: nil,
Key: "ports",
Value: []interface{}{},
},
}
for i, tc := range cases {
@ -606,7 +629,7 @@ func TestResourceDataGet(t *testing.T) {
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("Bad: %d\n\n%#v", i, v)
t.Fatalf("Bad: %d\n\n%#v\n\nExpected: %#v", i, v, tc.Value)
}
}
}
@ -975,8 +998,12 @@ func TestResourceDataSet(t *testing.T) {
Err bool
GetKey string
GetValue interface{}
// GetPreProcess can be set to munge the return value before being
// compared to GetValue
GetPreProcess func(interface{}) interface{}
}{
// Basic good
// #0: Basic good
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -998,7 +1025,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: "foo",
},
// Basic int
// #1: Basic int
{
Schema: map[string]*Schema{
"port": &Schema{
@ -1020,7 +1047,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: 80,
},
// Basic bool
// #2: Basic bool
{
Schema: map[string]*Schema{
"vpc": &Schema{
@ -1040,6 +1067,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: true,
},
// #3
{
Schema: map[string]*Schema{
"vpc": &Schema{
@ -1059,7 +1087,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: false,
},
// Invalid type
// #4: Invalid type
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1082,35 +1110,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: "",
},
// List of primitives, set element
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Computed: true,
Elem: &Schema{Type: TypeInt},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "3",
"ports.0": "1",
"ports.1": "2",
"ports.2": "5",
},
},
Diff: nil,
Key: "ports.1",
Value: 3,
GetKey: "ports",
GetValue: []interface{}{1, 3, 5},
},
// List of primitives, set list
// #5: List of primitives, set list
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1131,7 +1131,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: []interface{}{1, 2, 5},
},
// List of primitives, set list with error
// #6: List of primitives, set list with error
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1153,140 +1153,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: []interface{}{},
},
// List of resource, set element
{
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
},
},
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ingress.#": "2",
"ingress.0.from": "80",
"ingress.1.from": "8080",
},
},
Diff: nil,
Key: "ingress.1.from",
Value: 9000,
GetKey: "ingress",
GetValue: []interface{}{
map[string]interface{}{
"from": 80,
},
map[string]interface{}{
"from": 9000,
},
},
},
// List of resource, set full resource element
{
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
},
},
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ingress.#": "2",
"ingress.0.from": "80",
"ingress.1.from": "8080",
},
},
Diff: nil,
Key: "ingress.1",
Value: map[string]interface{}{
"from": 9000,
},
GetKey: "ingress",
GetValue: []interface{}{
map[string]interface{}{
"from": 80,
},
map[string]interface{}{
"from": 9000,
},
},
},
// List of resource, set full resource element, with error
{
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
},
"to": &Schema{
Type: TypeInt,
},
},
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ingress.#": "2",
"ingress.0.from": "80",
"ingress.0.to": "10",
"ingress.1.from": "8080",
"ingress.1.to": "8080",
},
},
Diff: nil,
Key: "ingress.1",
Value: map[string]interface{}{
"from": 9000,
"to": "bar",
},
Err: true,
GetKey: "ingress",
GetValue: []interface{}{
map[string]interface{}{
"from": 80,
"to": 10,
},
map[string]interface{}{
"from": 8080,
"to": 8080,
},
},
},
// Set a list of maps
// #7: Set a list of maps
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@ -1325,46 +1192,7 @@ func TestResourceDataSet(t *testing.T) {
},
},
// Set a list of maps
{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Schema{
Type: TypeMap,
},
},
},
State: nil,
Diff: nil,
Key: "config_vars",
Value: []interface{}{
map[string]string{
"foo": "bar",
},
map[string]string{
"bar": "baz",
},
},
Err: false,
GetKey: "config_vars",
GetValue: []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"bar": "baz",
},
},
},
// Set, with list
// #8: Set, with list
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1394,7 +1222,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: []interface{}{100, 125},
},
// Set, with Set
// #9: Set, with Set
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1429,7 +1257,7 @@ func TestResourceDataSet(t *testing.T) {
GetValue: []interface{}{1, 2},
},
// Set single item
// #10: Set single item
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1458,6 +1286,74 @@ func TestResourceDataSet(t *testing.T) {
GetKey: "ports",
GetValue: []interface{}{80, 100},
},
// #11: Set with nested set
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
},
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["port"].(int)
},
},
},
State: nil,
Key: "ports",
Value: []interface{}{
map[string]interface{}{
"port": 80,
},
},
GetKey: "ports",
GetValue: []interface{}{
map[string]interface{}{
"port": 80,
"set": []interface{}{},
},
},
GetPreProcess: func(v interface{}) interface{} {
if v == nil {
return v
}
s, ok := v.([]interface{})
if !ok {
return v
}
for _, v := range s {
m, ok := v.(map[string]interface{})
if !ok {
continue
}
if m["set"] == nil {
continue
}
if s, ok := m["set"].(*Set); ok {
m["set"] = s.List()
}
}
return v
},
},
}
for i, tc := range cases {
@ -1475,6 +1371,11 @@ func TestResourceDataSet(t *testing.T) {
if s, ok := v.(*Set); ok {
v = s.List()
}
if tc.GetPreProcess != nil {
v = tc.GetPreProcess(v)
}
if !reflect.DeepEqual(v, tc.GetValue) {
t.Fatalf("Get Bad: %d\n\n%#v", i, v)
}
@ -1490,7 +1391,7 @@ func TestResourceDataState(t *testing.T) {
Result *terraform.InstanceState
Partial []string
}{
// Basic primitive in diff
// #0 Basic primitive in diff
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1520,7 +1421,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Basic primitive set override
// #1 Basic primitive set override
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1554,6 +1455,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #2
{
Schema: map[string]*Schema{
"vpc": &Schema{
@ -1577,7 +1479,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Basic primitive with StateFunc set
// #3 Basic primitive with StateFunc set
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1607,7 +1509,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List
// #4 List
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1646,7 +1548,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of resources
// #5 List of resources
{
Schema: map[string]*Schema{
"ingress": &Schema{
@ -1696,7 +1598,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of maps
// #6 List of maps
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@ -1727,10 +1629,15 @@ func TestResourceDataState(t *testing.T) {
},
Set: map[string]interface{}{
"config_vars.1": map[string]interface{}{
"config_vars": []map[string]interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"baz": "bang",
},
},
},
Result: &terraform.InstanceState{
Attributes: map[string]string{
@ -1741,7 +1648,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of maps with removal in diff
// #7 List of maps with removal in diff
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@ -1781,7 +1688,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Basic state with other keys
// #8 Basic state with other keys
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1818,7 +1725,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Sets
// #9 Sets
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1853,6 +1760,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #10
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1883,6 +1791,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #11
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1944,7 +1853,9 @@ func TestResourceDataState(t *testing.T) {
"ports.10.order": "10",
"ports.10.a.#": "1",
"ports.10.a.0": "80",
"ports.10.b.#": "0",
"ports.20.order": "20",
"ports.20.a.#": "0",
"ports.20.b.#": "1",
"ports.20.b.0": "100",
},
@ -1955,7 +1866,7 @@ func TestResourceDataState(t *testing.T) {
* PARTIAL STATES
*/
// Basic primitive
// #12 Basic primitive
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1981,11 +1892,13 @@ func TestResourceDataState(t *testing.T) {
Partial: []string{},
Result: &terraform.InstanceState{
Attributes: map[string]string{},
Attributes: map[string]string{
"availability_zone": "",
},
},
},
// List
// #13 List
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -2025,6 +1938,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #14
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -2059,7 +1973,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of resources
// #15 List of resources
{
Schema: map[string]*Schema{
"ingress": &Schema{
@ -2110,7 +2024,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of maps
// #16 List of maps
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@ -2141,15 +2055,21 @@ func TestResourceDataState(t *testing.T) {
},
Set: map[string]interface{}{
"config_vars.1": map[string]interface{}{
"config_vars": []map[string]interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"baz": "bang",
},
},
},
Partial: []string{},
Result: &terraform.InstanceState{
Attributes: map[string]string{
// TODO: broken, shouldn't bar be removed?
"config_vars.#": "2",
"config_vars.0.foo": "bar",
"config_vars.0.bar": "bar",
@ -2158,7 +2078,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Sets
// #17 Sets
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -2201,6 +2121,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #18
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -2234,7 +2155,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Maps
// #19 Maps
{
Schema: map[string]*Schema{
"tags": &Schema{
@ -2262,6 +2183,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #20
{
Schema: map[string]*Schema{
"tags": &Schema{

View File

@ -33,8 +33,33 @@ const (
TypeList
TypeMap
TypeSet
typeObject
)
// Zero returns the zero value for a type.
func (t ValueType) Zero() interface{} {
switch t {
case TypeInvalid:
return nil
case TypeBool:
return false
case TypeInt:
return 0
case TypeString:
return ""
case TypeList:
return []interface{}{}
case TypeMap:
return map[string]interface{}{}
case TypeSet:
return nil
case typeObject:
return map[string]interface{}{}
default:
panic(fmt.Sprintf("unknown type %#v", t))
}
}
// Schema is used to describe the structure of a value.
//
// Read the documentation of the struct elements for important details.
@ -201,7 +226,6 @@ func (m schemaMap) Diff(
schema: m,
state: s,
config: c,
diffing: true,
}
for k, schema := range m {
@ -220,8 +244,10 @@ func (m schemaMap) Diff(
result2 := new(terraform.InstanceDiff)
result2.Attributes = make(map[string]*terraform.ResourceAttrDiff)
// Reset the data to not contain state
// Reset the data to not contain state. We have to call init()
// again in order to reset the FieldReaders.
d.state = nil
d.init()
// Perform the diff again
for k, schema := range m {
@ -457,6 +483,9 @@ func (m schemaMap) diffList(
d *ResourceData,
all bool) error {
o, n, _, computedList := d.diffChange(k)
if computedList {
n = nil
}
nSet := n != nil
// If we have an old value and no new value is set or will be
@ -654,6 +683,9 @@ func (m schemaMap) diffSet(
d *ResourceData,
all bool) error {
o, n, _, computedSet := d.diffChange(k)
if computedSet {
n = nil
}
nSet := n != nil
// If we have an old value and no new value is set or will be

View File

@ -8,6 +8,27 @@ import (
"github.com/hashicorp/terraform/terraform"
)
func TestValueType_Zero(t *testing.T) {
cases := []struct {
Type ValueType
Value interface{}
}{
{TypeBool, false},
{TypeInt, 0},
{TypeString, ""},
{TypeList, []interface{}{}},
{TypeMap, map[string]interface{}{}},
{TypeSet, nil},
}
for i, tc := range cases {
actual := tc.Type.Zero()
if !reflect.DeepEqual(actual, tc.Value) {
t.Fatalf("%d: %#v != %#v", i, actual, tc.Value)
}
}
}
func TestSchemaMap_Diff(t *testing.T) {
cases := []struct {
Schema map[string]*Schema
@ -844,7 +865,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "",
Old: "0",
New: "",
NewComputed: true,
},
@ -1664,7 +1685,7 @@ func TestSchemaMap_Diff(t *testing.T) {
New: "1",
},
"route.~1.gateway.#": &terraform.ResourceAttrDiff{
Old: "",
Old: "0",
NewComputed: true,
},
},

View File

@ -1,6 +1,7 @@
package schema
import (
"fmt"
"sort"
"sync"
)
@ -100,6 +101,10 @@ func (s *Set) Union(other *Set) *Set {
return result
}
func (s *Set) GoString() string {
return fmt.Sprintf("*Set(%#v)", s.m)
}
func (s *Set) init() {
s.m = make(map[int]interface{})
}