github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/funcs/conversion.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package funcs 5 6 import ( 7 "strconv" 8 9 "github.com/terramate-io/tf/lang/marks" 10 "github.com/terramate-io/tf/lang/types" 11 "github.com/zclconf/go-cty/cty" 12 "github.com/zclconf/go-cty/cty/convert" 13 "github.com/zclconf/go-cty/cty/function" 14 ) 15 16 // MakeToFunc constructs a "to..." function, like "tostring", which converts 17 // its argument to a specific type or type kind. 18 // 19 // The given type wantTy can be any type constraint that cty's "convert" package 20 // would accept. In particular, this means that you can pass 21 // cty.List(cty.DynamicPseudoType) to mean "list of any single type", which 22 // will then cause cty to attempt to unify all of the element types when given 23 // a tuple. 24 func MakeToFunc(wantTy cty.Type) function.Function { 25 return function.New(&function.Spec{ 26 Params: []function.Parameter{ 27 { 28 Name: "v", 29 // We use DynamicPseudoType rather than wantTy here so that 30 // all values will pass through the function API verbatim and 31 // we can handle the conversion logic within the Type and 32 // Impl functions. This allows us to customize the error 33 // messages to be more appropriate for an explicit type 34 // conversion, whereas the cty function system produces 35 // messages aimed at _implicit_ type conversions. 36 Type: cty.DynamicPseudoType, 37 AllowNull: true, 38 AllowMarked: true, 39 AllowDynamicType: true, 40 }, 41 }, 42 Type: func(args []cty.Value) (cty.Type, error) { 43 gotTy := args[0].Type() 44 if gotTy.Equals(wantTy) { 45 return wantTy, nil 46 } 47 conv := convert.GetConversionUnsafe(args[0].Type(), wantTy) 48 if conv == nil { 49 // We'll use some specialized errors for some trickier cases, 50 // but most we can handle in a simple way. 51 switch { 52 case gotTy.IsTupleType() && wantTy.IsTupleType(): 53 return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) 54 case gotTy.IsObjectType() && wantTy.IsObjectType(): 55 return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) 56 default: 57 return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) 58 } 59 } 60 // If a conversion is available then everything is fine. 61 return wantTy, nil 62 }, 63 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 64 // We didn't set "AllowUnknown" on our argument, so it is guaranteed 65 // to be known here but may still be null. 66 ret, err := convert.Convert(args[0], retType) 67 if err != nil { 68 val, _ := args[0].UnmarkDeep() 69 // Because we used GetConversionUnsafe above, conversion can 70 // still potentially fail in here. For example, if the user 71 // asks to convert the string "a" to bool then we'll 72 // optimistically permit it during type checking but fail here 73 // once we note that the value isn't either "true" or "false". 74 gotTy := val.Type() 75 switch { 76 case marks.Contains(args[0], marks.Sensitive): 77 // Generic message so we won't inadvertently disclose 78 // information about sensitive values. 79 return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) 80 81 case gotTy == cty.String && wantTy == cty.Bool: 82 what := "string" 83 if !val.IsNull() { 84 what = strconv.Quote(val.AsString()) 85 } 86 return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what) 87 case gotTy == cty.String && wantTy == cty.Number: 88 what := "string" 89 if !val.IsNull() { 90 what = strconv.Quote(val.AsString()) 91 } 92 return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what) 93 default: 94 return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) 95 } 96 } 97 return ret, nil 98 }, 99 }) 100 } 101 102 // TypeFunc returns an encapsulated value containing its argument's type. This 103 // value is marked to allow us to limit the use of this function at the moment 104 // to only a few supported use cases. 105 var TypeFunc = function.New(&function.Spec{ 106 Params: []function.Parameter{ 107 { 108 Name: "value", 109 Type: cty.DynamicPseudoType, 110 AllowDynamicType: true, 111 AllowUnknown: true, 112 AllowNull: true, 113 }, 114 }, 115 Type: function.StaticReturnType(types.TypeType), 116 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 117 givenType := args[0].Type() 118 return cty.CapsuleVal(types.TypeType, &givenType).Mark(marks.TypeType), nil 119 }, 120 }) 121 122 func Type(input []cty.Value) (cty.Value, error) { 123 return TypeFunc.Call(input) 124 }