github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/lang/funcs/conversion.go (about)

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