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