github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/lang/funcs/conversion.go (about) 1 package funcs 2 3 import ( 4 "strconv" 5 6 "github.com/zclconf/go-cty/cty" 7 "github.com/zclconf/go-cty/cty/convert" 8 "github.com/zclconf/go-cty/cty/function" 9 ) 10 11 // MakeToFunc constructs a "to..." function, like "tostring", which converts 12 // its argument to a specific type or type kind. 13 // 14 // The given type wantTy can be any type constraint that cty's "convert" package 15 // would accept. In particular, this means that you can pass 16 // cty.List(cty.DynamicPseudoType) to mean "list of any single type", which 17 // will then cause cty to attempt to unify all of the element types when given 18 // a tuple. 19 func MakeToFunc(wantTy cty.Type) function.Function { 20 return function.New(&function.Spec{ 21 Params: []function.Parameter{ 22 { 23 Name: "v", 24 // We use DynamicPseudoType rather than wantTy here so that 25 // all values will pass through the function API verbatim and 26 // we can handle the conversion logic within the Type and 27 // Impl functions. This allows us to customize the error 28 // messages to be more appropriate for an explicit type 29 // conversion, whereas the cty function system produces 30 // messages aimed at _implicit_ type conversions. 31 Type: cty.DynamicPseudoType, 32 AllowNull: true, 33 }, 34 }, 35 Type: func(args []cty.Value) (cty.Type, error) { 36 gotTy := args[0].Type() 37 if gotTy.Equals(wantTy) { 38 return wantTy, nil 39 } 40 conv := convert.GetConversionUnsafe(args[0].Type(), wantTy) 41 if conv == nil { 42 // We'll use some specialized errors for some trickier cases, 43 // but most we can handle in a simple way. 44 switch { 45 case gotTy.IsTupleType() && wantTy.IsTupleType(): 46 return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) 47 case gotTy.IsObjectType() && wantTy.IsObjectType(): 48 return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) 49 default: 50 return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) 51 } 52 } 53 // If a conversion is available then everything is fine. 54 return wantTy, nil 55 }, 56 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 57 // We didn't set "AllowUnknown" on our argument, so it is guaranteed 58 // to be known here but may still be null. 59 ret, err := convert.Convert(args[0], retType) 60 if err != nil { 61 // Because we used GetConversionUnsafe above, conversion can 62 // still potentially fail in here. For example, if the user 63 // asks to convert the string "a" to bool then we'll 64 // optimistically permit it during type checking but fail here 65 // once we note that the value isn't either "true" or "false". 66 gotTy := args[0].Type() 67 switch { 68 case gotTy == cty.String && wantTy == cty.Bool: 69 what := "string" 70 if !args[0].IsNull() { 71 what = strconv.Quote(args[0].AsString()) 72 } 73 return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what) 74 case gotTy == cty.String && wantTy == cty.Number: 75 what := "string" 76 if !args[0].IsNull() { 77 what = strconv.Quote(args[0].AsString()) 78 } 79 return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what) 80 default: 81 return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) 82 } 83 } 84 return ret, nil 85 }, 86 }) 87 }