configschema: fix various issues with NestedTypes
A handful of bugs popped up while extending the testing in plans/objchange. The main themes were failing to recurse through deeply nested NestedType attributes and improperly building up the ImpliedType. This commit fixes those issues and extends the test coverage to match.
This commit is contained in:
parent
6aa90a51d0
commit
1cf4909b28
|
@ -197,21 +197,9 @@ func (a *Attribute) decoderSpec(name string) hcldec.Spec {
|
||||||
panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
|
panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var optAttrs []string
|
|
||||||
optAttrs = listOptionalAttrsFromObject(a.NestedType, optAttrs)
|
|
||||||
ty := a.NestedType.ImpliedType()
|
ty := a.NestedType.ImpliedType()
|
||||||
|
ret.Type = ty
|
||||||
switch a.NestedType.Nesting {
|
ret.Required = a.Required || a.NestedType.MinItems > 0
|
||||||
case NestingList:
|
|
||||||
ret.Type = cty.List(ty)
|
|
||||||
case NestingSet:
|
|
||||||
ret.Type = cty.Set(ty)
|
|
||||||
case NestingMap:
|
|
||||||
ret.Type = cty.Map(ty)
|
|
||||||
default: // NestingSingle, NestingGroup, or no NestingMode
|
|
||||||
ret.Type = ty
|
|
||||||
}
|
|
||||||
ret.Required = a.NestedType.MinItems > 0
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,12 +208,16 @@ func (a *Attribute) decoderSpec(name string) hcldec.Spec {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func listOptionalAttrsFromObject(o *Object, optAttrs []string) []string {
|
// listOptionalAttrsFromObject is a helper function which does *not* recurse
|
||||||
|
// into NestedType Attributes, because the optional types for each of those will
|
||||||
|
// belong to their own cty.Object definitions. It is used in other functions
|
||||||
|
// which themselves handle that recursion.
|
||||||
|
func listOptionalAttrsFromObject(o *Object) []string {
|
||||||
|
var ret []string
|
||||||
for name, attr := range o.Attributes {
|
for name, attr := range o.Attributes {
|
||||||
if attr.Optional == true {
|
if attr.Optional == true {
|
||||||
optAttrs = append(optAttrs, name)
|
ret = append(ret, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ret
|
||||||
return optAttrs
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -451,7 +451,7 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
hcl.EmptyBody(),
|
hcltest.MockBody(&hcl.BodyContent{}),
|
||||||
cty.NullVal(cty.String),
|
cty.NullVal(cty.String),
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
|
@ -474,6 +474,7 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
"NestedType with required string": {
|
"NestedType with required string": {
|
||||||
&Attribute{
|
&Attribute{
|
||||||
NestedType: &Object{
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSingle,
|
||||||
Attributes: map[string]*Attribute{
|
Attributes: map[string]*Attribute{
|
||||||
"foo": {
|
"foo": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
|
@ -501,6 +502,7 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
"NestedType with optional attributes": {
|
"NestedType with optional attributes": {
|
||||||
&Attribute{
|
&Attribute{
|
||||||
NestedType: &Object{
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSingle,
|
||||||
Attributes: map[string]*Attribute{
|
Attributes: map[string]*Attribute{
|
||||||
"foo": {
|
"foo": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
|
@ -533,6 +535,7 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
"NestedType with missing required string": {
|
"NestedType with missing required string": {
|
||||||
&Attribute{
|
&Attribute{
|
||||||
NestedType: &Object{
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSingle,
|
||||||
Attributes: map[string]*Attribute{
|
Attributes: map[string]*Attribute{
|
||||||
"foo": {
|
"foo": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
|
@ -575,11 +578,9 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
Name: "attr",
|
Name: "attr",
|
||||||
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
||||||
cty.ObjectVal(map[string]cty.Value{
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
// "foo" should be a string, not a list
|
|
||||||
"foo": cty.StringVal("bar"),
|
"foo": cty.StringVal("bar"),
|
||||||
}),
|
}),
|
||||||
cty.ObjectVal(map[string]cty.Value{
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
// "foo" should be a string, not a list
|
|
||||||
"foo": cty.StringVal("baz"),
|
"foo": cty.StringVal("baz"),
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
@ -638,11 +639,9 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
Name: "attr",
|
Name: "attr",
|
||||||
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
|
||||||
cty.ObjectVal(map[string]cty.Value{
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
// "foo" should be a string, not a list
|
|
||||||
"foo": cty.StringVal("bar"),
|
"foo": cty.StringVal("bar"),
|
||||||
}),
|
}),
|
||||||
cty.ObjectVal(map[string]cty.Value{
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
// "foo" should be a string, not a list
|
|
||||||
"foo": cty.StringVal("baz"),
|
"foo": cty.StringVal("baz"),
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
@ -701,11 +700,9 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
Name: "attr",
|
Name: "attr",
|
||||||
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
||||||
"one": cty.ObjectVal(map[string]cty.Value{
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
// "foo" should be a string, not a list
|
|
||||||
"foo": cty.StringVal("bar"),
|
"foo": cty.StringVal("bar"),
|
||||||
}),
|
}),
|
||||||
"two": cty.ObjectVal(map[string]cty.Value{
|
"two": cty.ObjectVal(map[string]cty.Value{
|
||||||
// "foo" should be a string, not a list
|
|
||||||
"foo": cty.StringVal("baz"),
|
"foo": cty.StringVal("baz"),
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
@ -747,6 +744,102 @@ func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||||
1,
|
1,
|
||||||
},
|
},
|
||||||
|
"deeply NestedType NestingModeList valid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"bar": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
|
||||||
|
})}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
||||||
|
})}),
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"deeply NestedType NestingList invalid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"bar": {
|
||||||
|
Type: cty.Number,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.ListVal([]cty.Value{
|
||||||
|
// bar should be a Number
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.True}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.False}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.Number})),
|
||||||
|
}))),
|
||||||
|
1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
|
|
|
@ -27,16 +27,7 @@ func (b *Block) EmptyValue() cty.Value {
|
||||||
// at all, ignoring any required constraint.
|
// at all, ignoring any required constraint.
|
||||||
func (a *Attribute) EmptyValue() cty.Value {
|
func (a *Attribute) EmptyValue() cty.Value {
|
||||||
if a.NestedType != nil {
|
if a.NestedType != nil {
|
||||||
switch a.NestedType.Nesting {
|
return cty.NullVal(a.NestedType.ImpliedType())
|
||||||
case NestingList:
|
|
||||||
return cty.NullVal(cty.List(a.NestedType.ImpliedType()))
|
|
||||||
case NestingSet:
|
|
||||||
return cty.NullVal(cty.Set(a.NestedType.ImpliedType()))
|
|
||||||
case NestingMap:
|
|
||||||
return cty.NullVal(cty.Map(a.NestedType.ImpliedType()))
|
|
||||||
default: // NestingSingle, NestingGroup, or no NestingMode
|
|
||||||
return cty.NullVal(a.NestedType.ImpliedType())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return cty.NullVal(a.Type)
|
return cty.NullVal(a.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,19 +186,6 @@ func TestAttributeEmptyValue(t *testing.T) {
|
||||||
},
|
},
|
||||||
cty.NullVal(cty.String),
|
cty.NullVal(cty.String),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
&Attribute{
|
|
||||||
NestedType: &Object{
|
|
||||||
// no Nesting set should behave the same as NestingSingle
|
|
||||||
Attributes: map[string]*Attribute{
|
|
||||||
"str": {Type: cty.String, Required: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
||||||
"str": cty.String,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
&Attribute{
|
&Attribute{
|
||||||
NestedType: &Object{
|
NestedType: &Object{
|
||||||
|
@ -212,19 +199,6 @@ func TestAttributeEmptyValue(t *testing.T) {
|
||||||
"str": cty.String,
|
"str": cty.String,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
&Attribute{
|
|
||||||
NestedType: &Object{
|
|
||||||
Nesting: NestingGroup, // functionally equivalent to NestingSingle in a NestedType
|
|
||||||
Attributes: map[string]*Attribute{
|
|
||||||
"str": {Type: cty.String, Required: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
||||||
"str": cty.String,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
&Attribute{
|
&Attribute{
|
||||||
NestedType: &Object{
|
NestedType: &Object{
|
||||||
|
|
|
@ -55,13 +55,29 @@ func (o *Object) ImpliedType() cty.Type {
|
||||||
|
|
||||||
attrTys := make(map[string]cty.Type, len(o.Attributes))
|
attrTys := make(map[string]cty.Type, len(o.Attributes))
|
||||||
for name, attrS := range o.Attributes {
|
for name, attrS := range o.Attributes {
|
||||||
attrTys[name] = attrS.Type
|
if attrS.NestedType != nil {
|
||||||
|
attrTys[name] = attrS.NestedType.ImpliedType()
|
||||||
|
} else {
|
||||||
|
attrTys[name] = attrS.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optAttrs := listOptionalAttrsFromObject(o)
|
||||||
|
if len(optAttrs) > 0 {
|
||||||
|
return cty.ObjectWithOptionalAttrs(attrTys, optAttrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
var optAttrs []string
|
switch o.Nesting {
|
||||||
optAttrs = listOptionalAttrsFromObject(o, optAttrs)
|
case NestingSingle:
|
||||||
|
return cty.Object(attrTys)
|
||||||
return cty.ObjectWithOptionalAttrs(attrTys, optAttrs)
|
case NestingList:
|
||||||
|
return cty.List(cty.Object(attrTys))
|
||||||
|
case NestingMap:
|
||||||
|
return cty.Map(cty.Object(attrTys))
|
||||||
|
case NestingSet:
|
||||||
|
return cty.Set(cty.Object(attrTys))
|
||||||
|
default: // Should never happen
|
||||||
|
panic("invalid Nesting")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainsSensitive returns true if any of the attributes of the receiving
|
// ContainsSensitive returns true if any of the attributes of the receiving
|
||||||
|
|
|
@ -132,10 +132,6 @@ func TestObjectImpliedType(t *testing.T) {
|
||||||
nil,
|
nil,
|
||||||
cty.EmptyObject,
|
cty.EmptyObject,
|
||||||
},
|
},
|
||||||
"empty": {
|
|
||||||
&Object{},
|
|
||||||
cty.EmptyObject,
|
|
||||||
},
|
|
||||||
"attributes": {
|
"attributes": {
|
||||||
&Object{
|
&Object{
|
||||||
Attributes: map[string]*Attribute{
|
Attributes: map[string]*Attribute{
|
||||||
|
@ -167,6 +163,86 @@ func TestObjectImpliedType(t *testing.T) {
|
||||||
[]string{"optional", "optional_computed"},
|
[]string{"optional", "optional_computed"},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"nested attributes": {
|
||||||
|
&Object{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"nested_type": {
|
||||||
|
NestedType: &Object{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"optional": {
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
Type: cty.Number,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"computed": {
|
||||||
|
Type: cty.List(cty.Bool),
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"optional_computed": {
|
||||||
|
Type: cty.Map(cty.Bool),
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||||
|
"nested_type": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||||
|
"optional": cty.String,
|
||||||
|
"required": cty.Number,
|
||||||
|
"computed": cty.List(cty.Bool),
|
||||||
|
"optional_computed": cty.Map(cty.Bool),
|
||||||
|
}, []string{"optional", "optional_computed"}),
|
||||||
|
}, []string{"nested_type"}),
|
||||||
|
},
|
||||||
|
"NestingList": {
|
||||||
|
&Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {Type: cty.String},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.List(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||||
|
},
|
||||||
|
"NestingMap": {
|
||||||
|
&Object{
|
||||||
|
Nesting: NestingMap,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {Type: cty.String},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||||
|
},
|
||||||
|
"NestingSet": {
|
||||||
|
&Object{
|
||||||
|
Nesting: NestingSet,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {Type: cty.String},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||||
|
},
|
||||||
|
"deeply nested NestingList": {
|
||||||
|
&Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"bar": {Type: cty.String},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.List(cty.Object(map[string]cty.Type{"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.String}))})),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
|
|
Loading…
Reference in New Issue