260 lines
8.9 KiB
Go
260 lines
8.9 KiB
Go
package funcs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// DefaultsFunc is a helper function for substituting default values in
|
|
// place of null values in a given data structure.
|
|
//
|
|
// See the documentation for function Defaults for more information.
|
|
var DefaultsFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "input",
|
|
Type: cty.DynamicPseudoType,
|
|
AllowNull: true,
|
|
},
|
|
{
|
|
Name: "defaults",
|
|
Type: cty.DynamicPseudoType,
|
|
},
|
|
},
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
// The result type is guaranteed to be the same as the input type,
|
|
// since all we're doing is replacing null values with non-null
|
|
// values of the same type.
|
|
retType := args[0].Type()
|
|
defaultsType := args[1].Type()
|
|
|
|
// This function is aimed at filling in object types or collections
|
|
// of object types where some of the attributes might be null, so
|
|
// it doesn't make sense to use a primitive type directly with it.
|
|
// (The "coalesce" function may be appropriate for such cases.)
|
|
if retType.IsPrimitiveType() {
|
|
// This error message is a bit of a fib because we can actually
|
|
// apply defaults to tuples too, but we expect that to be so
|
|
// unusual as to not be worth mentioning here, because mentioning
|
|
// it would require using some less-well-known Terraform language
|
|
// terminology in the message (tuple types, structural types).
|
|
return cty.DynamicPseudoType, function.NewArgErrorf(1, "only object types and collections of object types can have defaults applied")
|
|
}
|
|
|
|
defaultsPath := make(cty.Path, 0, 4) // some capacity so that most structures won't reallocate
|
|
if err := defaultsAssertSuitableFallback(retType, defaultsType, defaultsPath); err != nil {
|
|
errMsg := tfdiags.FormatError(err) // add attribute path prefix
|
|
return cty.DynamicPseudoType, function.NewArgErrorf(1, "%s", errMsg)
|
|
}
|
|
|
|
return retType, nil
|
|
},
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
if args[0].Type().HasDynamicTypes() {
|
|
// If the types our input object aren't known yet for some reason
|
|
// then we'll defer all of our work here, because our
|
|
// interpretation of the defaults depends on the types in
|
|
// the input.
|
|
return cty.UnknownVal(retType), nil
|
|
}
|
|
|
|
v := defaultsApply(args[0], args[1])
|
|
return v, nil
|
|
},
|
|
})
|
|
|
|
func defaultsApply(input, fallback cty.Value) cty.Value {
|
|
wantTy := input.Type()
|
|
if !(input.IsKnown() && fallback.IsKnown()) {
|
|
return cty.UnknownVal(wantTy)
|
|
}
|
|
|
|
// For the rest of this function we're assuming that the given defaults
|
|
// will always be valid, because we expect to have caught any problems
|
|
// during the type checking phase. Any inconsistencies that reach here are
|
|
// therefore considered to be implementation bugs, and so will panic.
|
|
|
|
// Our strategy depends on the kind of type we're working with.
|
|
switch {
|
|
case wantTy.IsPrimitiveType():
|
|
// For leaf primitive values the rule is relatively simple: use the
|
|
// input if it's non-null, or fallback if input is null.
|
|
if !input.IsNull() {
|
|
return input
|
|
}
|
|
v, err := convert.Convert(fallback, wantTy)
|
|
if err != nil {
|
|
// Should not happen because we checked in defaultsAssertSuitableFallback
|
|
panic(err.Error())
|
|
}
|
|
return v
|
|
|
|
case wantTy.IsObjectType():
|
|
atys := wantTy.AttributeTypes()
|
|
ret := map[string]cty.Value{}
|
|
for attr, aty := range atys {
|
|
inputSub := input.GetAttr(attr)
|
|
fallbackSub := cty.NullVal(aty)
|
|
if fallback.Type().HasAttribute(attr) {
|
|
fallbackSub = fallback.GetAttr(attr)
|
|
}
|
|
ret[attr] = defaultsApply(inputSub, fallbackSub)
|
|
}
|
|
return cty.ObjectVal(ret)
|
|
|
|
case wantTy.IsTupleType():
|
|
l := wantTy.Length()
|
|
ret := make([]cty.Value, l)
|
|
for i := 0; i < l; i++ {
|
|
inputSub := input.Index(cty.NumberIntVal(int64(i)))
|
|
fallbackSub := fallback.Index(cty.NumberIntVal(int64(i)))
|
|
ret[i] = defaultsApply(inputSub, fallbackSub)
|
|
}
|
|
return cty.TupleVal(ret)
|
|
|
|
case wantTy.IsCollectionType():
|
|
// For collection types we apply a single fallback value to each
|
|
// element of the input collection, because in the situations this
|
|
// function is intended for we assume that the number of elements
|
|
// is the caller's decision, and so we'll just apply the same defaults
|
|
// to all of the elements.
|
|
ety := wantTy.ElementType()
|
|
switch {
|
|
case wantTy.IsMapType():
|
|
newVals := map[string]cty.Value{}
|
|
|
|
if !input.IsNull() {
|
|
for it := input.ElementIterator(); it.Next(); {
|
|
k, v := it.Element()
|
|
newVals[k.AsString()] = defaultsApply(v, fallback)
|
|
}
|
|
}
|
|
|
|
if len(newVals) == 0 {
|
|
return cty.MapValEmpty(ety)
|
|
}
|
|
return cty.MapVal(newVals)
|
|
case wantTy.IsListType(), wantTy.IsSetType():
|
|
var newVals []cty.Value
|
|
|
|
if !input.IsNull() {
|
|
for it := input.ElementIterator(); it.Next(); {
|
|
_, v := it.Element()
|
|
newV := defaultsApply(v, fallback)
|
|
newVals = append(newVals, newV)
|
|
}
|
|
}
|
|
|
|
if len(newVals) == 0 {
|
|
if wantTy.IsSetType() {
|
|
return cty.SetValEmpty(ety)
|
|
}
|
|
return cty.ListValEmpty(ety)
|
|
}
|
|
if wantTy.IsSetType() {
|
|
return cty.SetVal(newVals)
|
|
}
|
|
return cty.ListVal(newVals)
|
|
default:
|
|
// There are no other collection types, so this should not happen
|
|
panic(fmt.Sprintf("invalid collection type %#v", wantTy))
|
|
}
|
|
default:
|
|
// We should've caught anything else in defaultsAssertSuitableFallback,
|
|
// so this should not happen.
|
|
panic(fmt.Sprintf("invalid target type %#v", wantTy))
|
|
}
|
|
}
|
|
|
|
func defaultsAssertSuitableFallback(wantTy, fallbackTy cty.Type, fallbackPath cty.Path) error {
|
|
// If the type we want is a collection type then we need to keep peeling
|
|
// away collection type wrappers until we find the non-collection-type
|
|
// that's underneath, which is what the fallback will actually be applied
|
|
// to.
|
|
inCollection := false
|
|
for wantTy.IsCollectionType() {
|
|
wantTy = wantTy.ElementType()
|
|
inCollection = true
|
|
}
|
|
|
|
switch {
|
|
case wantTy.IsPrimitiveType():
|
|
// The fallback is valid if it's equal to or convertible to what we want.
|
|
if fallbackTy.Equals(wantTy) {
|
|
return nil
|
|
}
|
|
conversion := convert.GetConversion(fallbackTy, wantTy)
|
|
if conversion == nil {
|
|
msg := convert.MismatchMessage(fallbackTy, wantTy)
|
|
return fallbackPath.NewErrorf("invalid default value for %s: %s", wantTy.FriendlyName(), msg)
|
|
}
|
|
return nil
|
|
case wantTy.IsObjectType():
|
|
if !fallbackTy.IsObjectType() {
|
|
if inCollection {
|
|
return fallbackPath.NewErrorf("the default value for a collection of an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
|
|
}
|
|
return fallbackPath.NewErrorf("the default value for an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
|
|
}
|
|
for attr, wantAty := range wantTy.AttributeTypes() {
|
|
if !fallbackTy.HasAttribute(attr) {
|
|
continue // it's always okay to not have a default value
|
|
}
|
|
fallbackSubpath := fallbackPath.GetAttr(attr)
|
|
fallbackSubTy := fallbackTy.AttributeType(attr)
|
|
err := defaultsAssertSuitableFallback(wantAty, fallbackSubTy, fallbackSubpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for attr := range fallbackTy.AttributeTypes() {
|
|
if !wantTy.HasAttribute(attr) {
|
|
fallbackSubpath := fallbackPath.GetAttr(attr)
|
|
return fallbackSubpath.NewErrorf("target type does not expect an attribute named %q", attr)
|
|
}
|
|
}
|
|
return nil
|
|
case wantTy.IsTupleType():
|
|
if !fallbackTy.IsTupleType() {
|
|
if inCollection {
|
|
return fallbackPath.NewErrorf("the default value for a collection of a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
|
|
}
|
|
return fallbackPath.NewErrorf("the default value for a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
|
|
}
|
|
wantEtys := wantTy.TupleElementTypes()
|
|
fallbackEtys := fallbackTy.TupleElementTypes()
|
|
if got, want := len(wantEtys), len(fallbackEtys); got != want {
|
|
return fallbackPath.NewErrorf("the default value for a tuple type of length %d must also have length %d, not %d", want, want, got)
|
|
}
|
|
for i := 0; i < len(wantEtys); i++ {
|
|
fallbackSubpath := fallbackPath.IndexInt(i)
|
|
wantSubTy := wantEtys[i]
|
|
fallbackSubTy := fallbackEtys[i]
|
|
err := defaultsAssertSuitableFallback(wantSubTy, fallbackSubTy, fallbackSubpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
default:
|
|
// No other types are supported right now.
|
|
return fallbackPath.NewErrorf("cannot apply defaults to %s", wantTy.FriendlyName())
|
|
}
|
|
}
|
|
|
|
// Defaults is a helper function for substituting default values in
|
|
// place of null values in a given data structure.
|
|
//
|
|
// This is primarily intended for use with a module input variable that
|
|
// has an object type constraint (or a collection thereof) that has optional
|
|
// attributes, so that the receiver of a value that omits those attributes
|
|
// can insert non-null default values in place of the null values caused by
|
|
// omitting the attributes.
|
|
func Defaults(input, defaults cty.Value) (cty.Value, error) {
|
|
return DefaultsFunc.Call([]cty.Value{input, defaults})
|
|
}
|