helper/schema: empty maps, support reading objects directly

This commit is contained in:
Mitchell Hashimoto 2015-01-09 15:07:02 -08:00
parent f0af1c36f5
commit e57f3f69b1
6 changed files with 60 additions and 527 deletions

View File

@ -57,8 +57,13 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
Type: typeObject,
Elem: schemaMap,
}
result := make([]*Schema, 0, len(addr))
// TODO: test
if len(addr) == 0 {
return []*Schema{current}
}
result := make([]*Schema, 0, len(addr))
for len(addr) > 0 {
k := addr[0]
addr = addr[1:]

View File

@ -44,6 +44,13 @@ 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) {

View File

@ -24,6 +24,7 @@ func TestMapFieldReader(t *testing.T) {
Elem: &Schema{Type: TypeInt},
},
"map": &Schema{Type: TypeMap},
"mapDel": &Schema{Type: TypeMap},
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
@ -68,6 +69,8 @@ func TestMapFieldReader(t *testing.T) {
"map.foo": "bar",
"map.bar": "baz",
"mapDel": "",
"set.#": "2",
"set.10": "10",
"set.50": "50",
@ -152,6 +155,14 @@ func TestMapFieldReader(t *testing.T) {
false,
},
"mapDel": {
[]string{"mapDel"},
map[string]interface{}{},
true,
false,
false,
},
"mapelem": {
[]string{"map", "foo"},
"bar",

View File

@ -42,7 +42,10 @@ func (r *MultiLevelFieldReader) ReadFieldMerge(
var result FieldReadResult
for _, l := range r.Levels {
if r, ok := r.Readers[l]; ok {
println(fmt.Sprintf("GET %#v %s %#v", address, l, r))
out, err := r.ReadField(address)
println(fmt.Sprintf("%#v", out))
println("======================================")
if err != nil {
return FieldReadResult{}, fmt.Errorf(
"Error reading level %s: %s", l, err)

View File

@ -105,7 +105,8 @@ func (d *ResourceData) getRaw(key string, level getSource) getResult {
parts = strings.Split(key, ".")
}
return d.getObject("", parts, d.schema, level)
schema := &Schema{Type: typeObject, Elem: d.schema}
return d.get("", parts, schema, level)
}
// HasChange returns whether or not the given key has been changed.
@ -312,8 +313,9 @@ func (d *ResourceData) getChange(
parts2 = strings.Split(key, ".")
}
o := d.getObject("", parts, d.schema, oldLevel)
n := d.getObject("", parts2, d.schema, newLevel)
schema := &Schema{Type: typeObject, Elem: d.schema}
o := d.get("", parts, schema, oldLevel)
n := d.get("", parts2, schema, newLevel)
return o, n
}
@ -340,7 +342,11 @@ func (d *ResourceData) get(
}
// Build the address of the key we're looking for and ask the FieldReader
addr := append(strings.Split(k, "."), parts...)
var addr []string
if k != "" {
addr = strings.Split(k, ".")
}
addr = append(addr, parts...)
for i, v := range addr {
if v[0] == '~' {
addr[i] = v[1:]
@ -377,511 +383,6 @@ func (d *ResourceData) get(
}
}
func (d *ResourceData) getSet(
k string,
parts []string,
schema *Schema,
source getSource) getResult {
s := &Set{F: schema.Set}
result := getResult{Schema: schema, Value: s}
prefix := k + "."
// Get the set. For sets, the entire source must be exact: the
// entire set must come from set, diff, state, etc. So we go backwards
// and once we get a result, we take it. Or, we never get a result.
var indexMap map[int]int
codes := make(map[string]int)
sourceLevel := source & getSourceLevelMask
sourceFlags := source & ^getSourceLevelMask
sourceDiff := sourceFlags&getSourceDiff != 0
for setSource := sourceLevel; setSource > 0; setSource >>= 1 {
// If we're already asking for an exact source and it doesn't
// match, then leave since the original source was the match.
if sourceFlags&getSourceExact != 0 && setSource != sourceLevel {
break
}
if d.config != nil && setSource == getSourceConfig {
raw := d.getList(k, nil, schema, setSource)
// If the entire list is computed, then the entire set is
// necessarilly computed.
if raw.Computed {
result.Computed = true
if len(parts) > 0 {
break
}
return result
}
if raw.Exists {
result.Exists = true
list := raw.Value.([]interface{})
indexMap = make(map[int]int, len(list))
// Build the set from all the items using the given hash code
for i, v := range list {
code := s.add(v)
// Check if any of the keys in this item are computed
computed := false
if len(d.config.ComputedKeys) > 0 {
prefix := fmt.Sprintf("%s.%d", k, i)
computed = d.hasComputedSubKeys(prefix, schema)
}
// Check if we are computed and if so negatate the hash to
// this is a approximate hash
if computed {
s.m[-code] = s.m[code]
delete(s.m, code)
code = -code
}
indexMap[code] = i
}
break
}
}
if d.state != nil && setSource == getSourceState {
for k, _ := range d.state.Attributes {
if !strings.HasPrefix(k, prefix) || strings.HasPrefix(k, prefix+"#") {
continue
}
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
if _, ok := codes[idx]; ok {
continue
}
code, err := strconv.Atoi(strings.Replace(parts[0], "~", "-", -1))
if err != nil {
panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err))
}
codes[idx] = code
}
}
if d.setMap != nil && setSource == getSourceSet {
for k, _ := range d.setMap {
if !strings.HasPrefix(k, prefix) || strings.HasPrefix(k, prefix+"#") {
continue
}
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
if _, ok := codes[idx]; ok {
continue
}
code, err := strconv.Atoi(strings.Replace(parts[0], "~", "-", -1))
if err != nil {
panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err))
}
codes[idx] = code
}
}
if d.diff != nil && sourceDiff {
for k, _ := range d.diff.Attributes {
if !strings.HasPrefix(k, prefix) || strings.HasPrefix(k, prefix+"#") {
continue
}
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
if _, ok := codes[idx]; ok {
continue
}
code, err := strconv.Atoi(strings.Replace(parts[0], "~", "-", -1))
if err != nil {
panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err))
}
codes[idx] = code
}
}
if len(codes) > 0 {
break
}
}
if indexMap == nil {
s.m = make(map[int]interface{})
for idx, code := range codes {
switch t := schema.Elem.(type) {
case *Resource:
// Get the entire object
m := make(map[string]interface{})
for field, _ := range t.Schema {
m[field] = d.getObject(prefix+idx, []string{field}, t.Schema, source).Value
}
s.m[code] = m
result.Exists = true
case *Schema:
// Get a single value
s.m[code] = d.get(prefix+idx, nil, t, source).Value
result.Exists = true
}
}
}
if len(parts) > 0 {
// We still have parts left over meaning we're accessing an
// element of this set.
idx := parts[0]
parts = parts[1:]
// Special case if we're accessing the count of the set
if idx == "#" {
schema := &Schema{Type: TypeInt}
return d.get(prefix+"#", parts, schema, source)
}
if source&getSourceLevelMask == getSourceConfig {
i, err := strconv.Atoi(strings.Replace(idx, "~", "-", -1))
if err != nil {
panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err))
}
if i, ok := indexMap[i]; ok {
idx = strconv.Itoa(i)
}
}
switch t := schema.Elem.(type) {
case *Resource:
return d.getObject(prefix+idx, parts, t.Schema, source)
case *Schema:
return d.get(prefix+idx, parts, t, source)
}
}
return result
}
func (d *ResourceData) getMap(
k string,
parts []string,
schema *Schema,
source getSource) getResult {
elemSchema := &Schema{Type: TypeString}
result := make(map[string]interface{})
resultSet := false
prefix := k + "."
flags := source & ^getSourceLevelMask
level := source & getSourceLevelMask
exact := flags&getSourceExact != 0
diff := flags&getSourceDiff != 0
if !exact || level == getSourceState {
if d.state != nil && level >= getSourceState {
for k, _ := range d.state.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
single := k[len(prefix):]
result[single] = d.getPrimitive(k, nil, elemSchema, source).Value
resultSet = true
}
}
}
if d.config != nil && level == getSourceConfig {
// For config, we always set the result to exactly what was requested
if mraw, ok := d.config.Get(k); ok {
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
}
}
resultSet = true
case []map[string]interface{}:
for _, innerRaw := range m {
for k, v := range innerRaw {
result[k] = v
}
}
resultSet = true
case map[string]interface{}:
result = m
resultSet = true
default:
panic(fmt.Sprintf("unknown type: %#v", mraw))
}
} else {
result = nil
}
}
if d.diff != nil && diff {
for k, v := range d.diff.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
resultSet = true
single := k[len(prefix):]
if v.NewRemoved {
delete(result, single)
} else {
result[single] = d.getPrimitive(k, nil, elemSchema, source).Value
}
}
}
if !exact || level == getSourceSet {
if d.setMap != nil && level >= getSourceSet {
cleared := false
if v, ok := d.setMap[k]; ok && v == "" {
// We've cleared the map
result = make(map[string]interface{})
resultSet = true
} else {
for k, _ := range d.setMap {
if !strings.HasPrefix(k, prefix) {
continue
}
resultSet = true
if !cleared {
// We clear the results if they are in the set map
result = make(map[string]interface{})
cleared = true
}
single := k[len(prefix):]
result[single] = d.getPrimitive(
k, nil, elemSchema, source).Value
}
}
}
}
// If we're requesting a specific element, return that
var resultValue interface{} = result
if len(parts) > 0 {
resultValue = result[parts[0]]
}
return getResult{
Value: resultValue,
Exists: resultSet,
Schema: schema,
}
}
func (d *ResourceData) getObject(
k string,
parts []string,
schema map[string]*Schema,
source getSource) getResult {
if len(parts) > 0 {
// We're requesting a specific key in an object
key := parts[0]
parts = parts[1:]
s, ok := schema[key]
if !ok {
return getResultEmpty
}
if k != "" {
// If we're not at the root, then we need to append
// the key to get the full key path.
key = fmt.Sprintf("%s.%s", k, key)
}
return d.get(key, parts, s, source)
}
// Get the entire object
result := make(map[string]interface{})
for field, _ := range schema {
result[field] = d.getObject(k, []string{field}, schema, source).Value
}
return getResult{
Value: result,
Exists: true,
Schema: &Schema{
Elem: schema,
},
}
}
func (d *ResourceData) getList(
k string,
parts []string,
schema *Schema,
source getSource) getResult {
if len(parts) > 0 {
// We still have parts left over meaning we're accessing an
// element of this list.
idx := parts[0]
parts = parts[1:]
// Special case if we're accessing the count of the list
if idx == "#" {
schema := &Schema{Type: TypeInt}
return d.get(k+".#", parts, schema, source)
}
key := fmt.Sprintf("%s.%s", k, idx)
switch t := schema.Elem.(type) {
case *Resource:
return d.getObject(key, parts, t.Schema, source)
case *Schema:
return d.get(key, parts, t, source)
}
}
// Get the entire list.
var result []interface{}
count := d.getList(k, []string{"#"}, schema, source)
if !count.Computed {
result = make([]interface{}, count.Value.(int))
for i, _ := range result {
is := strconv.FormatInt(int64(i), 10)
result[i] = d.getList(k, []string{is}, schema, source).Value
}
}
return getResult{
Value: result,
Computed: count.Computed,
Exists: count.Exists,
Schema: schema,
}
}
func (d *ResourceData) getPrimitive(
k string,
parts []string,
schema *Schema,
source getSource) getResult {
var result string
var resultProcessed interface{}
var resultComputed, resultSet bool
flags := source & ^getSourceLevelMask
source = source & getSourceLevelMask
exact := flags&getSourceExact != 0
diff := flags&getSourceDiff != 0
if !exact || source == getSourceState {
if d.state != nil && source >= getSourceState {
result, resultSet = d.state.Attributes[k]
}
}
// No exact check is needed here because config is always exact
if d.config != nil && source == getSourceConfig {
// For config, we always return the exact value
if v, ok := d.config.Get(k); ok {
if err := mapstructure.WeakDecode(v, &result); err != nil {
panic(err)
}
resultSet = true
} else {
result = ""
resultSet = false
}
// If it is computed, set that.
resultComputed = d.config.IsComputed(k)
}
if d.diff != nil && diff {
attrD, ok := d.diff.Attributes[k]
if ok {
if !attrD.NewComputed {
result = attrD.New
if attrD.NewExtra != nil {
// If NewExtra != nil, then we have processed data as the New,
// so we store that but decode the unprocessed data into result
resultProcessed = result
err := mapstructure.WeakDecode(attrD.NewExtra, &result)
if err != nil {
panic(err)
}
}
resultSet = true
} else {
result = ""
resultSet = false
}
}
}
if !exact || source == getSourceSet {
if d.setMap != nil && source >= getSourceSet {
if v, ok := d.setMap[k]; ok {
result = v
resultSet = true
}
}
}
if !resultSet {
result = ""
}
var resultValue interface{}
switch schema.Type {
case TypeBool:
if result == "" {
resultValue = false
break
}
v, err := strconv.ParseBool(result)
if err != nil {
panic(err)
}
resultValue = v
case TypeString:
// Use the value as-is. We just put this case here to be explicit.
resultValue = result
case TypeInt:
if result == "" {
resultValue = 0
break
}
if resultComputed {
break
}
v, err := strconv.ParseInt(result, 0, 0)
if err != nil {
panic(err)
}
resultValue = int(v)
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
return getResult{
Value: resultValue,
ValueProcessed: resultProcessed,
Computed: resultComputed,
Exists: resultSet,
Schema: schema,
}
}
func (d *ResourceData) set(
k string,
parts []string,
@ -1195,7 +696,7 @@ func (d *ResourceData) stateList(
func (d *ResourceData) stateMap(
prefix string,
schema *Schema) map[string]string {
v := d.getMap(prefix, nil, schema, d.stateSource(prefix))
v := d.get(prefix, nil, schema, d.stateSource(prefix))
if !v.Exists {
return nil
}

View File

@ -1391,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{
@ -1421,7 +1421,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Basic primitive set override
// #1 Basic primitive set override
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1455,6 +1455,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #2
{
Schema: map[string]*Schema{
"vpc": &Schema{
@ -1478,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{
@ -1508,7 +1509,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List
// #4 List
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1547,7 +1548,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of resources
// #5 List of resources
{
Schema: map[string]*Schema{
"ingress": &Schema{
@ -1597,7 +1598,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of maps
// #6 List of maps
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@ -1647,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{
@ -1687,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{
@ -1724,7 +1725,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Sets
// #9 Sets
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1759,6 +1760,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #10
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1789,6 +1791,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #11
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1861,7 +1864,7 @@ func TestResourceDataState(t *testing.T) {
* PARTIAL STATES
*/
// Basic primitive
// #12 Basic primitive
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
@ -1891,7 +1894,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List
// #13 List
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1931,6 +1934,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #14
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -1965,7 +1969,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of resources
// #15 List of resources
{
Schema: map[string]*Schema{
"ingress": &Schema{
@ -2016,7 +2020,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// List of maps
// #16 List of maps
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@ -2070,7 +2074,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Sets
// #17 Sets
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -2113,6 +2117,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #18
{
Schema: map[string]*Schema{
"ports": &Schema{
@ -2146,7 +2151,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// Maps
// #19 Maps
{
Schema: map[string]*Schema{
"tags": &Schema{
@ -2174,6 +2179,7 @@ func TestResourceDataState(t *testing.T) {
},
},
// #20
{
Schema: map[string]*Schema{
"tags": &Schema{