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  }