diff --git a/configs/configschema/empty_value.go b/configs/configschema/empty_value.go new file mode 100644 index 000000000..b72b9f86a --- /dev/null +++ b/configs/configschema/empty_value.go @@ -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) + } +} diff --git a/configs/configschema/empty_value_test.go b/configs/configschema/empty_value_test.go new file mode 100644 index 000000000..5ff786fc8 --- /dev/null +++ b/configs/configschema/empty_value_test.go @@ -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)) + } + }) + } +}