package funcs import ( "fmt" "sort" "strconv" "strings" "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, AllowMarked: 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 args[0].ContainsMarked(): // Generic message so we won't inadvertently disclose // information about sensitive values. return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) 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 }, }) } var TypeFunc = function.New(&function.Spec{ Params: []function.Parameter{ { Name: "value", Type: cty.DynamicPseudoType, AllowDynamicType: true, AllowUnknown: true, AllowNull: true, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(TypeString(args[0].Type())).Mark("raw"), nil }, }) // Modified copy of TypeString from go-cty: // https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go // // TypeString returns a string representation of a given type that is // reminiscent of Go syntax calling into the cty package but is mainly // intended for easy human inspection of values in tests, debug output, etc. // // The resulting string will include newlines and indentation in order to // increase the readability of complex structures. It always ends with a // newline, so you can print this result directly to your output. func TypeString(ty cty.Type) string { var b strings.Builder writeType(ty, &b, 0) return b.String() } func writeType(ty cty.Type, b *strings.Builder, indent int) { switch { case ty == cty.NilType: b.WriteString("nil") return case ty.IsObjectType(): atys := ty.AttributeTypes() if len(atys) == 0 { b.WriteString("object({})") return } attrNames := make([]string, 0, len(atys)) for name := range atys { attrNames = append(attrNames, name) } sort.Strings(attrNames) b.WriteString("object({\n") indent++ for _, name := range attrNames { aty := atys[name] b.WriteString(indentSpaces(indent)) fmt.Fprintf(b, "%s: ", name) writeType(aty, b, indent) b.WriteString(",\n") } indent-- b.WriteString(indentSpaces(indent)) b.WriteString("})") case ty.IsTupleType(): etys := ty.TupleElementTypes() if len(etys) == 0 { b.WriteString("tuple([])") return } b.WriteString("tuple([\n") indent++ for _, ety := range etys { b.WriteString(indentSpaces(indent)) writeType(ety, b, indent) b.WriteString(",\n") } indent-- b.WriteString(indentSpaces(indent)) b.WriteString("])") case ty.IsCollectionType(): ety := ty.ElementType() switch { case ty.IsListType(): b.WriteString("list(") case ty.IsMapType(): b.WriteString("map(") case ty.IsSetType(): b.WriteString("set(") default: // At the time of writing there are no other collection types, // but we'll be robust here and just pass through the GoString // of anything we don't recognize. b.WriteString(ty.FriendlyName()) return } // Because object and tuple types render split over multiple // lines, a collection type container around them can end up // being hard to see when scanning, so we'll generate some extra // indentation to make a collection of structural type more visually // distinct from the structural type alone. complexElem := ety.IsObjectType() || ety.IsTupleType() if complexElem { indent++ b.WriteString("\n") b.WriteString(indentSpaces(indent)) } writeType(ty.ElementType(), b, indent) if complexElem { indent-- b.WriteString(",\n") b.WriteString(indentSpaces(indent)) } b.WriteString(")") default: // For any other type we'll just use its GoString and assume it'll // follow the usual GoString conventions. b.WriteString(ty.FriendlyName()) } } func indentSpaces(level int) string { return strings.Repeat(" ", level) } func Type(input []cty.Value) (cty.Value, error) { return TypeFunc.Call(input) }