From a20084dc0e38dfb0920cda9de55a823c87755c85 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 6 Apr 2019 09:02:18 -0700 Subject: [PATCH] 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. --- configs/configschema/empty_value.go | 57 +++++++++ configs/configschema/empty_value_test.go | 151 +++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 configs/configschema/empty_value.go create mode 100644 configs/configschema/empty_value_test.go 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)) + } + }) + } +}