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