configs/configschema: add new NestedType to attribute
This commit adds a new field, NestedType, to the Attribute schema, and extends the current Attribute decoderSpec to account for the new type. The codepaths are mostly unused and included in a separate commit to verify that the included changes do not impact any other tests yet.
This commit is contained in:
parent
f3a057eb35
commit
3ad720e9dc
|
@ -6,6 +6,7 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2/hcldec"
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mapLabelNames = []string{"key"}
|
var mapLabelNames = []string{"key"}
|
||||||
|
@ -183,9 +184,48 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Attribute) decoderSpec(name string) hcldec.Spec {
|
func (a *Attribute) decoderSpec(name string) hcldec.Spec {
|
||||||
return &hcldec.AttrSpec{
|
ret := &hcldec.AttrSpec{Name: name}
|
||||||
Name: name,
|
if a == nil {
|
||||||
Type: a.Type,
|
return ret
|
||||||
Required: a.Required,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.NestedType != nil {
|
||||||
|
// FIXME: a panic() is a bad UX. Fix this, probably by extending
|
||||||
|
// InternalValidate() to check Attribute schemas as well and calling it
|
||||||
|
// when we get the schema from the provider in Context().
|
||||||
|
if a.Type != cty.NilType {
|
||||||
|
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()
|
||||||
|
|
||||||
|
switch a.NestedType.Nesting {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Type = a.Type
|
||||||
|
ret.Required = a.Required
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func listOptionalAttrsFromObject(o *Object, optAttrs []string) []string {
|
||||||
|
for name, attr := range o.Attributes {
|
||||||
|
if attr.Optional == true {
|
||||||
|
optAttrs = append(optAttrs, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return optAttrs
|
||||||
}
|
}
|
||||||
|
|
|
@ -426,3 +426,363 @@ func TestBlockDecoderSpec(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAttributeDecoderSpec(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Schema *Attribute
|
||||||
|
TestBody hcl.Body
|
||||||
|
Want cty.Value
|
||||||
|
DiagCount int
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
&Attribute{},
|
||||||
|
hcl.EmptyBody(),
|
||||||
|
cty.NilVal,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"nil": {
|
||||||
|
nil,
|
||||||
|
hcl.EmptyBody(),
|
||||||
|
cty.NilVal,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"optional string (null)": {
|
||||||
|
&Attribute{
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcl.EmptyBody(),
|
||||||
|
cty.NullVal(cty.String),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"optional string": {
|
||||||
|
&Attribute{
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.StringVal("bar")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.StringVal("bar"),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"NestedType with required string": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"NestedType with optional attributes": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
"bar": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"NestedType with missing required string": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.String,
|
||||||
|
})),
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
// NestedModes
|
||||||
|
"NestedType NestingModeList valid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
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" should be a string, not a list
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.StringVal("baz"),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"NestedType NestingModeList invalid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
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" should be a string, not a list
|
||||||
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
||||||
|
})})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
"NestedType NestingModeSet valid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSet,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.StringVal("baz"),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.SetVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"NestedType NestingModeSet invalid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSet,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
||||||
|
})})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
"NestedType NestingModeMap valid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingMap,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
"two": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.StringVal("baz"),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.MapVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||||
|
"two": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
"NestedType NestingModeMap invalid": {
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingMap,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"attr": {
|
||||||
|
Name: "attr",
|
||||||
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
// "foo" should be a string, not a list
|
||||||
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
spec := test.Schema.decoderSpec("attr")
|
||||||
|
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
||||||
|
if len(diags) != test.DiagCount {
|
||||||
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
||||||
|
for _, diag := range diags {
|
||||||
|
t.Logf("- %s", diag.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !got.RawEquals(test.Want) {
|
||||||
|
t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
|
||||||
|
t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAttributeDecodeSpec_panic is a temporary test which verifies that
|
||||||
|
// decoderSpec panics when an invalid Attribute schema is encountered. It will
|
||||||
|
// be removed when InternalValidate() is extended to validate Attribute specs
|
||||||
|
// (and is used). See the #FIXME in decoderSpec.
|
||||||
|
func TestAttributeDecoderSpec_panic(t *testing.T) {
|
||||||
|
attrS := &Attribute{
|
||||||
|
Type: cty.Object(map[string]cty.Type{
|
||||||
|
"nested_attribute": cty.String,
|
||||||
|
}),
|
||||||
|
NestedType: &Object{},
|
||||||
|
Optional: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { recover() }()
|
||||||
|
attrS.decoderSpec("attr")
|
||||||
|
t.Errorf("expected panic")
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,18 @@ func (b *Block) EmptyValue() cty.Value {
|
||||||
// the value that would be returned if there were no definition of the attribute
|
// the value that would be returned if there were no definition of the attribute
|
||||||
// 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 {
|
||||||
|
switch a.NestedType.Nesting {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,3 +168,116 @@ func TestBlockEmptyValue(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attribute EmptyValue() is well covered by the Block tests above; these tests
|
||||||
|
// focus on the behavior with NestedType field inside an Attribute
|
||||||
|
func TestAttributeEmptyValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Schema *Attribute
|
||||||
|
Want cty.Value
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
&Attribute{},
|
||||||
|
cty.NilVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Attribute{
|
||||||
|
Type: 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{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSingle,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"str": {Type: cty.String, Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||||
|
"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{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingList,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"str": {Type: cty.String, Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.NullVal(cty.List(
|
||||||
|
cty.Object(map[string]cty.Type{
|
||||||
|
"str": cty.String,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingMap,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"str": {Type: cty.String, Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.NullVal(cty.Map(
|
||||||
|
cty.Object(map[string]cty.Type{
|
||||||
|
"str": cty.String,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Attribute{
|
||||||
|
NestedType: &Object{
|
||||||
|
Nesting: NestingSet,
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"str": {Type: cty.String, Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.NullVal(cty.Set(
|
||||||
|
cty.Object(map[string]cty.Type{
|
||||||
|
"str": cty.String,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) {
|
||||||
|
got := test.Schema.EmptyValue()
|
||||||
|
if !test.Want.RawEquals(got) {
|
||||||
|
t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), dump.Value(got), dump.Value(test.Want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,3 +40,37 @@ func (b *Block) ContainsSensitive() bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImpliedType returns the cty.Type that would result from decoding a NestedType
|
||||||
|
// Attribute using the receiving block schema.
|
||||||
|
//
|
||||||
|
// ImpliedType always returns a result, even if the given schema is
|
||||||
|
// inconsistent. Code that creates configschema.Object objects should be tested
|
||||||
|
// using the InternalValidate method to detect any inconsistencies that would
|
||||||
|
// cause this method to fall back on defaults and assumptions.
|
||||||
|
func (o *Object) ImpliedType() cty.Type {
|
||||||
|
if o == nil {
|
||||||
|
return cty.EmptyObject
|
||||||
|
}
|
||||||
|
|
||||||
|
attrTys := make(map[string]cty.Type, len(o.Attributes))
|
||||||
|
for name, attrS := range o.Attributes {
|
||||||
|
attrTys[name] = attrS.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
var optAttrs []string
|
||||||
|
optAttrs = listOptionalAttrsFromObject(o, optAttrs)
|
||||||
|
|
||||||
|
return cty.ObjectWithOptionalAttrs(attrTys, optAttrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsSensitive returns true if any of the attributes of the receiving
|
||||||
|
// Object are marked as sensitive.
|
||||||
|
func (o *Object) ContainsSensitive() bool {
|
||||||
|
for _, attrS := range o.Attributes {
|
||||||
|
if attrS.Sensitive {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -122,3 +122,59 @@ func TestBlockImpliedType(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestObjectImpliedType(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Schema *Object
|
||||||
|
Want cty.Type
|
||||||
|
}{
|
||||||
|
"nil": {
|
||||||
|
nil,
|
||||||
|
cty.EmptyObject,
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
&Object{},
|
||||||
|
cty.EmptyObject,
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
&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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := test.Schema.ImpliedType()
|
||||||
|
if !got.Equals(test.Want) {
|
||||||
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,8 +38,13 @@ type Block struct {
|
||||||
// Attribute represents a configuration attribute, within a block.
|
// Attribute represents a configuration attribute, within a block.
|
||||||
type Attribute struct {
|
type Attribute struct {
|
||||||
// Type is a type specification that the attribute's value must conform to.
|
// Type is a type specification that the attribute's value must conform to.
|
||||||
|
// It conflicts with NestedType.
|
||||||
Type cty.Type
|
Type cty.Type
|
||||||
|
|
||||||
|
// NestedType indicates that the attribute is a NestedBlock-style object.
|
||||||
|
// This field conflicts with Type.
|
||||||
|
NestedType *Object
|
||||||
|
|
||||||
// Description is an English-language description of the purpose and
|
// Description is an English-language description of the purpose and
|
||||||
// usage of the attribute. A description should be concise and use only
|
// usage of the attribute. A description should be concise and use only
|
||||||
// one or two sentences, leaving full definition to longer-form
|
// one or two sentences, leaving full definition to longer-form
|
||||||
|
@ -72,6 +77,25 @@ type Attribute struct {
|
||||||
Deprecated bool
|
Deprecated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Object represents the embedding of a structural object inside an Attribute.
|
||||||
|
type Object struct {
|
||||||
|
// Attributes describes the nested attributes which may appear inside the
|
||||||
|
// Object.
|
||||||
|
Attributes map[string]*Attribute
|
||||||
|
|
||||||
|
// Nesting provides the nesting mode for this Object, which determines how
|
||||||
|
// many instances of the Object are allowed, how many labels it expects, and
|
||||||
|
// how the resulting data will be converted into a data structure.
|
||||||
|
Nesting NestingMode
|
||||||
|
|
||||||
|
// MinItems and MaxItems set, for the NestingList and NestingSet nesting
|
||||||
|
// modes, lower and upper limits on the number of child blocks allowed
|
||||||
|
// of the given type. If both are left at zero, no limit is applied.
|
||||||
|
// These fields are ignored for other nesting modes and must both be left
|
||||||
|
// at zero.
|
||||||
|
MinItems, MaxItems int
|
||||||
|
}
|
||||||
|
|
||||||
// NestedBlock represents the embedding of one block within another.
|
// NestedBlock represents the embedding of one block within another.
|
||||||
type NestedBlock struct {
|
type NestedBlock struct {
|
||||||
// Block is the description of the block that's nested.
|
// Block is the description of the block that's nested.
|
||||||
|
@ -98,6 +122,8 @@ type NestedBlock struct {
|
||||||
// blocks.
|
// blocks.
|
||||||
type NestingMode int
|
type NestingMode int
|
||||||
|
|
||||||
|
// Object represents the embedding of a NestedBl
|
||||||
|
|
||||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=NestingMode
|
//go:generate go run golang.org/x/tools/cmd/stringer -type=NestingMode
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
Loading…
Reference in New Issue