88 lines
3.5 KiB
Go
88 lines
3.5 KiB
Go
|
package funcs
|
||
|
|
||
|
import (
|
||
|
"strconv"
|
||
|
|
||
|
"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,
|
||
|
},
|
||
|
},
|
||
|
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 {
|
||
|
// 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 := args[0].Type()
|
||
|
switch {
|
||
|
case gotTy == cty.String && wantTy == cty.Bool:
|
||
|
what := "string"
|
||
|
if !args[0].IsNull() {
|
||
|
what = strconv.Quote(args[0].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 !args[0].IsNull() {
|
||
|
what = strconv.Quote(args[0].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
|
||
|
},
|
||
|
})
|
||
|
}
|