| package funcs |
| |
| import ( |
| "strconv" |
| |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/convert" |
| "github.com/zclconf/go-cty/cty/function" |
| ) |
| |
| // MakeToFunc constructs a "to..." function, like "tostring", which converts |
| // its argument to a specific type or type kind. |
| // |
| // The given type wantTy can be any type constraint that cty's "convert" package |
| // would accept. In particular, this means that you can pass |
| // cty.List(cty.DynamicPseudoType) to mean "list of any single type", which |
| // will then cause cty to attempt to unify all of the element types when given |
| // a tuple. |
| func MakeToFunc(wantTy cty.Type) function.Function { |
| return function.New(&function.Spec{ |
| Params: []function.Parameter{ |
| { |
| Name: "v", |
| // We use DynamicPseudoType rather than wantTy here so that |
| // all values will pass through the function API verbatim and |
| // we can handle the conversion logic within the Type and |
| // Impl functions. This allows us to customize the error |
| // messages to be more appropriate for an explicit type |
| // conversion, whereas the cty function system produces |
| // messages aimed at _implicit_ type conversions. |
| Type: cty.DynamicPseudoType, |
| AllowNull: true, |
| }, |
| }, |
| Type: func(args []cty.Value) (cty.Type, error) { |
| gotTy := args[0].Type() |
| if gotTy.Equals(wantTy) { |
| return wantTy, nil |
| } |
| conv := convert.GetConversionUnsafe(args[0].Type(), wantTy) |
| if conv == nil { |
| // We'll use some specialized errors for some trickier cases, |
| // but most we can handle in a simple way. |
| switch { |
| case gotTy.IsTupleType() && wantTy.IsTupleType(): |
| return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) |
| case gotTy.IsObjectType() && wantTy.IsObjectType(): |
| return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) |
| default: |
| return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) |
| } |
| } |
| // If a conversion is available then everything is fine. |
| return wantTy, nil |
| }, |
| Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { |
| // We didn't set "AllowUnknown" on our argument, so it is guaranteed |
| // to be known here but may still be null. |
| ret, err := convert.Convert(args[0], retType) |
| if err != nil { |
| // Because we used GetConversionUnsafe above, conversion can |
| // still potentially fail in here. For example, if the user |
| // asks to convert the string "a" to bool then we'll |
| // optimistically permit it during type checking but fail here |
| // once we note that the value isn't either "true" or "false". |
| gotTy := args[0].Type() |
| switch { |
| case gotTy == cty.String && wantTy == cty.Bool: |
| what := "string" |
| if !args[0].IsNull() { |
| what = strconv.Quote(args[0].AsString()) |
| } |
| return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what) |
| case gotTy == cty.String && wantTy == cty.Number: |
| what := "string" |
| if !args[0].IsNull() { |
| what = strconv.Quote(args[0].AsString()) |
| } |
| return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what) |
| default: |
| return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) |
| } |
| } |
| return ret, nil |
| }, |
| }) |
| } |