mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
configschema: Block.CoerceValue method
The usual way to use a configschema.Block is to obtain a hcldec spec from it and then decode an hcl.Body. There are inevitably situations though where a body has already been decoded into a cty.Value before we know which schema we need to use. This new method CoerceValue is intended to deal with this case, applying the schema to an already-decoded value in what should be an intuitive way for most situations.
This commit is contained in:
parent
6cf9346dfd
commit
22d140e883
241
config/configschema/coerce_value.go
Normal file
241
config/configschema/coerce_value.go
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
package configschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoerceValue attempts to force the given value to conform to the type
|
||||||
|
// implied by the receiever, while also applying the same validation and
|
||||||
|
// transformation rules that would be applied by the decoder specification
|
||||||
|
// returned by method DecoderSpec.
|
||||||
|
//
|
||||||
|
// This is useful in situations where a configuration must be derived from
|
||||||
|
// an already-decoded value. It is always better to decode directly from
|
||||||
|
// configuration where possible since then source location information is
|
||||||
|
// still available to produce diagnostics, but in special situations this
|
||||||
|
// function allows a compatible result to be obtained even if the
|
||||||
|
// configuration objects are not available.
|
||||||
|
//
|
||||||
|
// If the given value cannot be converted to conform to the receiving schema
|
||||||
|
// then an error is returned describing one of possibly many problems. This
|
||||||
|
// error may be a cty.PathError indicating a position within the nested
|
||||||
|
// data structure where the problem applies.
|
||||||
|
func (b *Block) CoerceValue(in cty.Value) (cty.Value, error) {
|
||||||
|
var path cty.Path
|
||||||
|
return b.coerceValue(in, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||||
|
switch {
|
||||||
|
case in.IsNull():
|
||||||
|
return cty.NullVal(b.ImpliedType()), nil
|
||||||
|
case !in.IsKnown():
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ty := in.Type()
|
||||||
|
if !ty.IsObjectType() {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("an object is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range ty.AttributeTypes() {
|
||||||
|
if _, defined := b.Attributes[name]; defined {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, defined := b.BlockTypes[name]; defined {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("unexpected attribute %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := make(map[string]cty.Value)
|
||||||
|
|
||||||
|
for name, attrS := range b.Attributes {
|
||||||
|
var val cty.Value
|
||||||
|
switch {
|
||||||
|
case ty.HasAttribute(name):
|
||||||
|
val = in.GetAttr(name)
|
||||||
|
case attrS.Computed:
|
||||||
|
val = cty.UnknownVal(attrS.Type)
|
||||||
|
case attrS.Optional:
|
||||||
|
val = cty.NullVal(attrS.Type)
|
||||||
|
default:
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := attrS.coerceValue(val, append(path, cty.GetAttrStep{Name: name}))
|
||||||
|
if err != nil {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), err
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs[name] = val
|
||||||
|
}
|
||||||
|
for typeName, blockS := range b.BlockTypes {
|
||||||
|
switch blockS.Nesting {
|
||||||
|
|
||||||
|
case NestingSingle:
|
||||||
|
switch {
|
||||||
|
case ty.HasAttribute(typeName):
|
||||||
|
var err error
|
||||||
|
val := in.GetAttr(typeName)
|
||||||
|
attrs[typeName], err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
|
||||||
|
if err != nil {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), err
|
||||||
|
}
|
||||||
|
case blockS.MinItems != 1 && blockS.MaxItems != 1:
|
||||||
|
attrs[typeName] = cty.NullVal(blockS.ImpliedType())
|
||||||
|
default:
|
||||||
|
// We use the word "attribute" here because we're talking about
|
||||||
|
// the cty sense of that word rather than the HCL sense.
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NestingList:
|
||||||
|
switch {
|
||||||
|
case ty.HasAttribute(typeName):
|
||||||
|
coll := in.GetAttr(typeName)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case coll.IsNull():
|
||||||
|
attrs[typeName] = cty.NullVal(cty.List(b.ImpliedType()))
|
||||||
|
continue
|
||||||
|
case !coll.IsKnown():
|
||||||
|
attrs[typeName] = cty.UnknownVal(cty.List(b.ImpliedType()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !coll.CanIterateElements() {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a list", typeName)
|
||||||
|
}
|
||||||
|
l := coll.LengthInt()
|
||||||
|
if l < blockS.MinItems {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems)
|
||||||
|
}
|
||||||
|
if l > blockS.MaxItems && blockS.MaxItems > 0 {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("too many items for attribute %q; must have at least %d", typeName, blockS.MinItems)
|
||||||
|
}
|
||||||
|
if l == 0 {
|
||||||
|
attrs[typeName] = cty.ListValEmpty(b.ImpliedType())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elems := make([]cty.Value, 0, l)
|
||||||
|
for it := in.ElementIterator(); it.Next(); {
|
||||||
|
var err error
|
||||||
|
_, val := it.Element()
|
||||||
|
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
|
||||||
|
if err != nil {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), err
|
||||||
|
}
|
||||||
|
elems = append(elems, val)
|
||||||
|
}
|
||||||
|
attrs[typeName] = cty.ListVal(elems)
|
||||||
|
case blockS.MinItems == 0:
|
||||||
|
attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType())
|
||||||
|
default:
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NestingSet:
|
||||||
|
switch {
|
||||||
|
case ty.HasAttribute(typeName):
|
||||||
|
coll := in.GetAttr(typeName)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case coll.IsNull():
|
||||||
|
attrs[typeName] = cty.NullVal(cty.Set(b.ImpliedType()))
|
||||||
|
continue
|
||||||
|
case !coll.IsKnown():
|
||||||
|
attrs[typeName] = cty.UnknownVal(cty.Set(b.ImpliedType()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !coll.CanIterateElements() {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a set", typeName)
|
||||||
|
}
|
||||||
|
l := coll.LengthInt()
|
||||||
|
if l < blockS.MinItems {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems)
|
||||||
|
}
|
||||||
|
if l > blockS.MaxItems && blockS.MaxItems > 0 {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("too many items for attribute %q; must have at least %d", typeName, blockS.MinItems)
|
||||||
|
}
|
||||||
|
if l == 0 {
|
||||||
|
attrs[typeName] = cty.SetValEmpty(b.ImpliedType())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elems := make([]cty.Value, 0, l)
|
||||||
|
for it := in.ElementIterator(); it.Next(); {
|
||||||
|
var err error
|
||||||
|
_, val := it.Element()
|
||||||
|
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
|
||||||
|
if err != nil {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), err
|
||||||
|
}
|
||||||
|
elems = append(elems, val)
|
||||||
|
}
|
||||||
|
attrs[typeName] = cty.SetVal(elems)
|
||||||
|
case blockS.MinItems == 0:
|
||||||
|
attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType())
|
||||||
|
default:
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NestingMap:
|
||||||
|
switch {
|
||||||
|
case ty.HasAttribute(typeName):
|
||||||
|
coll := in.GetAttr(typeName)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case coll.IsNull():
|
||||||
|
attrs[typeName] = cty.NullVal(cty.Map(b.ImpliedType()))
|
||||||
|
continue
|
||||||
|
case !coll.IsKnown():
|
||||||
|
attrs[typeName] = cty.UnknownVal(cty.Map(b.ImpliedType()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !coll.CanIterateElements() {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a map", typeName)
|
||||||
|
}
|
||||||
|
l := coll.LengthInt()
|
||||||
|
if l == 0 {
|
||||||
|
attrs[typeName] = cty.MapValEmpty(b.ImpliedType())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elems := make(map[string]cty.Value)
|
||||||
|
for it := in.ElementIterator(); it.Next(); {
|
||||||
|
var err error
|
||||||
|
key, val := it.Element()
|
||||||
|
if key.Type() != cty.String || key.IsNull() || !key.IsKnown() {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a map", typeName)
|
||||||
|
}
|
||||||
|
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
|
||||||
|
if err != nil {
|
||||||
|
return cty.UnknownVal(b.ImpliedType()), err
|
||||||
|
}
|
||||||
|
elems[key.AsString()] = val
|
||||||
|
}
|
||||||
|
attrs[typeName] = cty.MapVal(elems)
|
||||||
|
default:
|
||||||
|
attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// should never happen because above is exhaustive
|
||||||
|
panic(fmt.Errorf("unsupported nesting mode %#v", blockS.Nesting))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cty.ObjectVal(attrs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Attribute) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||||
|
val, err := convert.Convert(in, a.Type)
|
||||||
|
if err != nil {
|
||||||
|
return cty.UnknownVal(a.Type), path.NewError(err)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
233
config/configschema/coerce_value_test.go
Normal file
233
config/configschema/coerce_value_test.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package configschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCoerceValue(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Schema *Block
|
||||||
|
Input cty.Value
|
||||||
|
WantValue cty.Value
|
||||||
|
WantErr string
|
||||||
|
}{
|
||||||
|
"empty schema and value": {
|
||||||
|
&Block{},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"attribute present": {
|
||||||
|
&Block{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.True,
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("true"),
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"single block present": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingSingle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.EmptyObjectVal,
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.EmptyObjectVal,
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"single block wrong type": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingSingle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.True,
|
||||||
|
}),
|
||||||
|
cty.DynamicVal,
|
||||||
|
`an object is required`,
|
||||||
|
},
|
||||||
|
"missing optional attribute": {
|
||||||
|
&Block{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"missing optional single block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingSingle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.NullVal(cty.EmptyObject),
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"missing optional list block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.ListValEmpty(cty.EmptyObject),
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"missing optional set block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingSet,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.SetValEmpty(cty.EmptyObject),
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"missing optional map block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingMap,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.MapValEmpty(cty.EmptyObject),
|
||||||
|
}),
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"missing required attribute": {
|
||||||
|
&Block{
|
||||||
|
Attributes: map[string]*Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.DynamicVal,
|
||||||
|
`attribute "foo" is required`,
|
||||||
|
},
|
||||||
|
"missing required single block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingSingle,
|
||||||
|
MinItems: 1,
|
||||||
|
MaxItems: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.DynamicVal,
|
||||||
|
`attribute "foo" is required`,
|
||||||
|
},
|
||||||
|
"missing required list block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingList,
|
||||||
|
MinItems: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.DynamicVal,
|
||||||
|
`attribute "foo" is required`,
|
||||||
|
},
|
||||||
|
"missing required set block": {
|
||||||
|
&Block{
|
||||||
|
BlockTypes: map[string]*NestedBlock{
|
||||||
|
"foo": {
|
||||||
|
Block: Block{},
|
||||||
|
Nesting: NestingList,
|
||||||
|
MinItems: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cty.EmptyObjectVal,
|
||||||
|
cty.DynamicVal,
|
||||||
|
`attribute "foo" is required`,
|
||||||
|
},
|
||||||
|
"extraneous attribute": {
|
||||||
|
&Block{},
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
cty.DynamicVal,
|
||||||
|
`unexpected attribute "foo"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
gotValue, gotErrObj := test.Schema.CoerceValue(test.Input)
|
||||||
|
|
||||||
|
if gotErrObj == nil {
|
||||||
|
if test.WantErr != "" {
|
||||||
|
t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gotErr := gotErrObj.Error()
|
||||||
|
if gotErr != test.WantErr {
|
||||||
|
t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !gotValue.RawEquals(test.WantValue) {
|
||||||
|
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, gotValue, test.WantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user