mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-04 13:17:43 -06:00
d4776e8ef1
We had intended these functions to attempt to convert any given value, but there is a special behavior in the function system where functions must opt in to being able to handle dynamically-typed arguments so that we don't need to repeat the special case for that inside every function implementation. In this case we _do_ want to specially handle dynamically-typed values, because the keyword "null" in HCL produces cty.NullVal(cty.DynamicPseudoType) and we want the conversion function to convert it to a null of a more specific type. These conversion functions are already just a thin wrapper around the underlying type conversion functionality anyway, and that already supports converting dynamic-typed values in the expected way, so we can just opt in to allowing dynamically-typed values and let the conversion functionality do the expected work. Fixing this allows module authors to use type conversion functions to give additional type information to Terraform in situations that are too ambiguous to be handled automatically by the type inference/unification process. Previously tostring(null) was effectively a no-op, totally ignoring the author's request to treat the null as a string.
122 lines
4.7 KiB
Go
122 lines
4.7 KiB
Go
package funcs
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
|
"github.com/hashicorp/terraform/internal/lang/types"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// MakeToFunc constructs a "to..." function, like "tostring", which converts
|
|
// its argument to a specific type or type kind.
|
|
//
|
|
// The given type wantTy can be any type constraint that cty's "convert" package
|
|
// would accept. In particular, this means that you can pass
|
|
// cty.List(cty.DynamicPseudoType) to mean "list of any single type", which
|
|
// will then cause cty to attempt to unify all of the element types when given
|
|
// a tuple.
|
|
func MakeToFunc(wantTy cty.Type) function.Function {
|
|
return function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "v",
|
|
// We use DynamicPseudoType rather than wantTy here so that
|
|
// all values will pass through the function API verbatim and
|
|
// we can handle the conversion logic within the Type and
|
|
// Impl functions. This allows us to customize the error
|
|
// messages to be more appropriate for an explicit type
|
|
// conversion, whereas the cty function system produces
|
|
// messages aimed at _implicit_ type conversions.
|
|
Type: cty.DynamicPseudoType,
|
|
AllowNull: true,
|
|
AllowMarked: true,
|
|
AllowDynamicType: true,
|
|
},
|
|
},
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
gotTy := args[0].Type()
|
|
if gotTy.Equals(wantTy) {
|
|
return wantTy, nil
|
|
}
|
|
conv := convert.GetConversionUnsafe(args[0].Type(), wantTy)
|
|
if conv == nil {
|
|
// We'll use some specialized errors for some trickier cases,
|
|
// but most we can handle in a simple way.
|
|
switch {
|
|
case gotTy.IsTupleType() && wantTy.IsTupleType():
|
|
return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
|
|
case gotTy.IsObjectType() && wantTy.IsObjectType():
|
|
return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
|
|
default:
|
|
return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
|
}
|
|
}
|
|
// If a conversion is available then everything is fine.
|
|
return wantTy, nil
|
|
},
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
// We didn't set "AllowUnknown" on our argument, so it is guaranteed
|
|
// to be known here but may still be null.
|
|
ret, err := convert.Convert(args[0], retType)
|
|
if err != nil {
|
|
val, _ := args[0].UnmarkDeep()
|
|
// Because we used GetConversionUnsafe above, conversion can
|
|
// still potentially fail in here. For example, if the user
|
|
// asks to convert the string "a" to bool then we'll
|
|
// optimistically permit it during type checking but fail here
|
|
// once we note that the value isn't either "true" or "false".
|
|
gotTy := val.Type()
|
|
switch {
|
|
case marks.Contains(args[0], marks.Sensitive):
|
|
// Generic message so we won't inadvertently disclose
|
|
// information about sensitive values.
|
|
return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
|
|
|
case gotTy == cty.String && wantTy == cty.Bool:
|
|
what := "string"
|
|
if !val.IsNull() {
|
|
what = strconv.Quote(val.AsString())
|
|
}
|
|
return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what)
|
|
case gotTy == cty.String && wantTy == cty.Number:
|
|
what := "string"
|
|
if !val.IsNull() {
|
|
what = strconv.Quote(val.AsString())
|
|
}
|
|
return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what)
|
|
default:
|
|
return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
|
}
|
|
}
|
|
return ret, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// TypeFunc returns an encapsulated value containing its argument's type. This
|
|
// value is marked to allow us to limit the use of this function at the moment
|
|
// to only a few supported use cases.
|
|
var TypeFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "value",
|
|
Type: cty.DynamicPseudoType,
|
|
AllowDynamicType: true,
|
|
AllowUnknown: true,
|
|
AllowNull: true,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(types.TypeType),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
givenType := args[0].Type()
|
|
return cty.CapsuleVal(types.TypeType, &givenType).Mark(marks.TypeType), nil
|
|
},
|
|
})
|
|
|
|
func Type(input []cty.Value) (cty.Value, error) {
|
|
return TypeFunc.Call(input)
|
|
}
|