281 lines
8.7 KiB
Go
281 lines
8.7 KiB
Go
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 || 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, NestingGroup:
|
|
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:
|
|
if blockS.Nesting == NestingGroup {
|
|
attrs[typeName] = blockS.EmptyValue()
|
|
} else {
|
|
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(blockS.ImpliedType()))
|
|
continue
|
|
case !coll.IsKnown():
|
|
attrs[typeName] = cty.UnknownVal(cty.List(blockS.ImpliedType()))
|
|
continue
|
|
}
|
|
|
|
if !coll.CanIterateElements() {
|
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a list")
|
|
}
|
|
l := coll.LengthInt()
|
|
|
|
// Assume that if there are unknowns this could have come from
|
|
// a dynamic block, and we can't validate MinItems yet.
|
|
if l < blockS.MinItems && coll.IsWhollyKnown() {
|
|
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; cannot have more than %d", typeName, blockS.MaxItems)
|
|
}
|
|
if l == 0 {
|
|
attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType())
|
|
continue
|
|
}
|
|
elems := make([]cty.Value, 0, l)
|
|
{
|
|
path = append(path, cty.GetAttrStep{Name: typeName})
|
|
for it := coll.ElementIterator(); it.Next(); {
|
|
var err error
|
|
idx, val := it.Element()
|
|
val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
|
|
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(blockS.ImpliedType()))
|
|
continue
|
|
case !coll.IsKnown():
|
|
attrs[typeName] = cty.UnknownVal(cty.Set(blockS.ImpliedType()))
|
|
continue
|
|
}
|
|
|
|
if !coll.CanIterateElements() {
|
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set")
|
|
}
|
|
l := coll.LengthInt()
|
|
|
|
// Assume that if there are unknowns this could have come from
|
|
// a dynamic block, and we can't validate MinItems yet.
|
|
if l < blockS.MinItems && coll.IsWhollyKnown() {
|
|
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; cannot have more than %d", typeName, blockS.MaxItems)
|
|
}
|
|
if l == 0 {
|
|
attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType())
|
|
continue
|
|
}
|
|
elems := make([]cty.Value, 0, l)
|
|
{
|
|
path = append(path, cty.GetAttrStep{Name: typeName})
|
|
for it := coll.ElementIterator(); it.Next(); {
|
|
var err error
|
|
idx, val := it.Element()
|
|
val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
|
|
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(blockS.ImpliedType()))
|
|
continue
|
|
case !coll.IsKnown():
|
|
attrs[typeName] = cty.UnknownVal(cty.Map(blockS.ImpliedType()))
|
|
continue
|
|
}
|
|
|
|
if !coll.CanIterateElements() {
|
|
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map")
|
|
}
|
|
l := coll.LengthInt()
|
|
if l == 0 {
|
|
attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType())
|
|
continue
|
|
}
|
|
elems := make(map[string]cty.Value)
|
|
{
|
|
path = append(path, cty.GetAttrStep{Name: typeName})
|
|
for it := coll.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("must be a map")
|
|
}
|
|
val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key}))
|
|
if err != nil {
|
|
return cty.UnknownVal(b.ImpliedType()), err
|
|
}
|
|
elems[key.AsString()] = val
|
|
}
|
|
}
|
|
|
|
// If the attribute values here contain any DynamicPseudoTypes,
|
|
// the concrete type must be an object.
|
|
useObject := false
|
|
switch {
|
|
case coll.Type().IsObjectType():
|
|
useObject = true
|
|
default:
|
|
// It's possible that we were given a map, and need to coerce it to an object
|
|
ety := coll.Type().ElementType()
|
|
for _, v := range elems {
|
|
if !v.Type().Equals(ety) {
|
|
useObject = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if useObject {
|
|
attrs[typeName] = cty.ObjectVal(elems)
|
|
} else {
|
|
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
|
|
}
|