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  }