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
|
@ -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
|
||||
}
|
|
@ -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