switch blocks based on value type, and check attrs

Check attributes on null objects, and fill in unknowns. If we're
evaluating the object, it either means we are at the top level, or a
NestingSingle block was present, and in either case we need to treat the
attributes as null rather than the entire object.

Switch on the block types rather than Nesting, so we don't need add any
logic to change between List/Tuple or Map/Object when DynamicPseudoType
is involved.
This commit is contained in:
James Bardin 2019-02-08 14:46:29 -05:00
parent 312d798a89
commit 82588af892
2 changed files with 211 additions and 39 deletions

View File

@ -8,12 +8,35 @@ import (
)
// SetUnknowns takes a cty.Value, and compares it to the schema setting any null
// leaf values which are computed as unknown.
// values which are computed to unknown.
func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value {
if val.IsNull() || !val.IsKnown() {
if !val.IsKnown() {
return val
}
// If the object was null, we still need to handle the top level attributes
// which might be computed, but we don't need to expand the blocks.
if val.IsNull() {
objMap := map[string]cty.Value{}
allNull := true
for name, attr := range schema.Attributes {
switch {
case attr.Computed:
objMap[name] = cty.UnknownVal(attr.Type)
allNull = false
default:
objMap[name] = cty.NullVal(attr.Type)
}
}
// If this object has no unknown attributes, then we can leave it null.
if allNull {
return val
}
return cty.ObjectVal(objMap)
}
valMap := val.AsValueMap()
newVals := make(map[string]cty.Value)
@ -35,12 +58,18 @@ func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value {
continue
}
blockType := blockS.Block.ImpliedType()
blockValType := blockVal.Type()
blockElementType := blockS.Block.ImpliedType()
switch blockS.Nesting {
case configschema.NestingSingle:
// This switches on the value type here, so we can correctly switch
// between Tuples/Lists and Maps/Objects.
switch {
case blockS.Nesting == configschema.NestingSingle:
// NestingSingle is the only exception here, where we treat the
// block directly as an object
newVals[name] = SetUnknowns(blockVal, &blockS.Block)
case configschema.NestingSet, configschema.NestingList:
case blockValType.IsSetType(), blockValType.IsListType(), blockValType.IsTupleType():
listVals := blockVal.AsValueSlice()
newListVals := make([]cty.Value, 0, len(listVals))
@ -48,24 +77,26 @@ func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value {
newListVals = append(newListVals, SetUnknowns(v, &blockS.Block))
}
switch blockS.Nesting {
case configschema.NestingSet:
switch {
case blockValType.IsSetType():
switch len(newListVals) {
case 0:
newVals[name] = cty.SetValEmpty(blockType)
newVals[name] = cty.SetValEmpty(blockElementType)
default:
newVals[name] = cty.SetVal(newListVals)
}
case configschema.NestingList:
case blockValType.IsListType():
switch len(newListVals) {
case 0:
newVals[name] = cty.ListValEmpty(blockType)
newVals[name] = cty.ListValEmpty(blockElementType)
default:
newVals[name] = cty.ListVal(newListVals)
}
case blockValType.IsTupleType():
newVals[name] = cty.TupleVal(newListVals)
}
case configschema.NestingMap:
case blockValType.IsMapType(), blockValType.IsObjectType():
mapVals := blockVal.AsValueMap()
newMapVals := make(map[string]cty.Value)
@ -73,15 +104,26 @@ func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value {
newMapVals[k] = SetUnknowns(v, &blockS.Block)
}
switch len(newMapVals) {
case 0:
newVals[name] = cty.MapValEmpty(blockType)
default:
newVals[name] = cty.MapVal(newMapVals)
switch {
case blockValType.IsMapType():
switch len(newMapVals) {
case 0:
newVals[name] = cty.MapValEmpty(blockElementType)
default:
newVals[name] = cty.MapVal(newMapVals)
}
case blockValType.IsObjectType():
if len(newMapVals) == 0 {
// We need to populate empty values to make a valid object.
for attr, ty := range blockElementType.AttributeTypes() {
newMapVals[attr] = cty.NullVal(ty)
}
}
newVals[name] = cty.ObjectVal(newMapVals)
}
default:
panic(fmt.Sprintf("failed to set unknown values for nested block %q", name))
panic(fmt.Sprintf("failed to set unknown values for nested block %q:%#v", name, blockValType))
}
}

View File

@ -50,14 +50,27 @@ func TestSetUnknowns(t *testing.T) {
},
},
},
cty.ObjectVal(map[string]cty.Value{}),
cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
"bar": cty.String,
"baz": cty.Object(map[string]cty.Type{
"boz": cty.String,
"biz": cty.String,
}),
})),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"bar": cty.UnknownVal(cty.String),
}),
},
"no prior with set": {
// the set value should remain null
"null stays null": {
// if the object has no computed attributes, it should stay null
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": &configschema.Attribute{
Type: cty.String,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"baz": {
Nesting: configschema.NestingSet,
@ -73,8 +86,52 @@ func TestSetUnknowns(t *testing.T) {
},
},
},
cty.ObjectVal(map[string]cty.Value{}),
cty.ObjectVal(map[string]cty.Value{}),
cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
"baz": cty.Set(cty.Object(map[string]cty.Type{
"boz": cty.String,
})),
})),
cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
"baz": cty.Set(cty.Object(map[string]cty.Type{
"boz": cty.String,
})),
})),
},
"no prior with set": {
// the set value should remain null
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": &configschema.Attribute{
Type: cty.String,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"baz": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"boz": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
"baz": cty.Set(cty.Object(map[string]cty.Type{
"boz": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
},
"prior attributes": {
&configschema.Block{
@ -329,24 +386,97 @@ func TestSetUnknowns(t *testing.T) {
}),
}),
},
"prior nested list with dynamic": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"foo": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.String,
Optional: true,
Computed: true,
},
"baz": {
Type: cty.DynamicPseudoType,
Optional: true,
Computed: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.String),
"baz": cty.NumberIntVal(8),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.UnknownVal(cty.String),
"baz": cty.NumberIntVal(8),
}),
}),
}),
},
"prior nested map with dynamic": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"foo": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.String,
Optional: true,
Computed: true,
},
"baz": {
Type: cty.DynamicPseudoType,
Optional: true,
Computed: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("beep"),
"baz": cty.NullVal(cty.DynamicPseudoType),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("boop"),
"baz": cty.NumberIntVal(8),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("beep"),
"baz": cty.UnknownVal(cty.DynamicPseudoType),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("boop"),
"baz": cty.NumberIntVal(8),
}),
}),
}),
},
} {
t.Run(n, func(t *testing.T) {
// coerce the values because SetUnknowns expects the values to be
// complete, and so we can take shortcuts writing them in the
// test.
v, err := tc.Schema.CoerceValue(tc.Val)
if err != nil {
t.Fatal(err)
}
expected, err := tc.Schema.CoerceValue(tc.Expected)
if err != nil {
t.Fatal(err)
}
got := SetUnknowns(v, tc.Schema)
if !got.RawEquals(expected) {
t.Fatalf("\nexpected: %#v\ngot: %#v\n", expected, got)
got := SetUnknowns(tc.Val, tc.Schema)
if !got.RawEquals(tc.Expected) {
t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.Expected, got)
}
})
}