configs/configschema: EmptyValue methods

These helpers determine the value that would be used for a particular
schema construct if the configuration construct it represents is not
present (or, in the case of *Block, empty) in the configuration.

This is different than cty.NullVal on the implied type because it might
return non-null "empty" values for certain constructs if their absence
would be reported as such during a decode with no required attributes or
blocks.
This commit is contained in:
Martin Atkins 2019-04-06 09:02:18 -07:00
parent 44c9e81d4c
commit a20084dc0e
2 changed files with 208 additions and 0 deletions

View File

@ -0,0 +1,57 @@
package configschema
import (
"github.com/zclconf/go-cty/cty"
)
// EmptyValue returns the "empty value" for the recieving block, which for
// a block type is a non-null object where all of the attribute values are
// the empty values of the block's attributes and nested block types.
//
// In other words, it returns the value that would be returned if an empty
// block were decoded against the recieving schema, assuming that no required
// attribute or block constraints were honored.
func (b *Block) EmptyValue() cty.Value {
vals := make(map[string]cty.Value)
for name, attrS := range b.Attributes {
vals[name] = attrS.EmptyValue()
}
for name, blockS := range b.BlockTypes {
vals[name] = blockS.EmptyValue()
}
return cty.ObjectVal(vals)
}
// EmptyValue returns the "empty value" for the receiving attribute, which is
// the value that would be returned if there were no definition of the attribute
// at all, ignoring any required constraint.
func (a *Attribute) EmptyValue() cty.Value {
return cty.NullVal(a.Type)
}
// EmptyValue returns the "empty value" for when there are zero nested blocks
// present of the receiving type.
func (b *NestedBlock) EmptyValue() cty.Value {
switch b.Nesting {
case NestingSingle:
return cty.NullVal(b.Block.ImpliedType())
case NestingList:
if ty := b.Block.ImpliedType(); ty.HasDynamicTypes() {
return cty.EmptyTupleVal
} else {
return cty.ListValEmpty(ty)
}
case NestingMap:
if ty := b.Block.ImpliedType(); ty.HasDynamicTypes() {
return cty.EmptyObjectVal
} else {
return cty.MapValEmpty(ty)
}
case NestingSet:
return cty.SetValEmpty(b.Block.ImpliedType())
default:
// Should never get here because the above is intended to be exhaustive,
// but we'll be robust and return a result nonetheless.
return cty.NullVal(cty.DynamicPseudoType)
}
}

View File

@ -0,0 +1,151 @@
package configschema
import (
"fmt"
"testing"
"github.com/apparentlymart/go-dump/dump"
"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"
)
func TestBlockEmptyValue(t *testing.T) {
tests := []struct {
Schema *Block
Want cty.Value
}{
{
&Block{},
cty.EmptyObjectVal,
},
{
&Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
cty.ObjectVal(map[string]cty.Value{
"str": cty.NullVal(cty.String),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"single": {
Nesting: NestingSingle,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"single": cty.NullVal(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"list": cty.ListValEmpty(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"list_dynamic": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.DynamicPseudoType, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"list_dynamic": cty.EmptyTupleVal,
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"map": {
Nesting: NestingMap,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"map": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"map_dynamic": {
Nesting: NestingMap,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.DynamicPseudoType, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"map_dynamic": cty.EmptyObjectVal,
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"set": {
Nesting: NestingSet,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"set": cty.SetValEmpty(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))
}
})
}
}