Skip to content

Commit 9fd6d73

Browse files
authored
Add a tfsdk.ConvertValue helper. (#120)
This function converts an `attr.Value` to use a new `attr.Type`, so long as they share a compatible `tftypes.Type` representation. This allows code that doesn't know the type of an `attr.Value` to still use it, provided they know the kind of data it holds. For example, a validation helper that works on strings--MaxStringLength, say--could look something like this: ``` // assume req.Config is an attr.Value strAttr, err := tfsdk.ConvertValue(ctx, req.Config, types.StringType) if err != nil { panic(err) } var str types.String diags := tfsdk.ValueAs(ctx, strAttr, &str) if diags.HasError() { panic("oops") } // do something with `str`, which is now strongly typed ``` Then no matter what `attr.Type` provider devs use in their schema, this validator would still work, then. Without the conversion step, it wouldn't work, as `tfsdk.ValueAs` implementation needs to know the `attr.Type` of the `attr.Value`, and reusable validators _can't_ know that. So the point of this helper is to get an `attr.Value` into a known `attr.Type` format.
1 parent 1de21d9 commit 9fd6d73

File tree

8 files changed

+155
-0
lines changed

8 files changed

+155
-0
lines changed

.changelog/120.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:breaking-change
2+
`attr.Type` implementations must now have a `String()` method that returns a human-friendly name for the type.
3+
```
4+
5+
```release-note:enhancement
6+
Added a `tfsdk.ConvertValue` helper that will convert any `attr.Value` into any compatible `attr.Type`. Compatibility happens at the terraform-plugin-go level; the type that the `attr.Value`'s `ToTerraformValue` method produces must be compatible with the `attr.Type`'s `TerraformType()`. Generally, this means that the `attr.Type` of the `attr.Value` and the `attr.Type` being converted to must both produce the same `tftypes.Type` when their `TerraformType()` method is called.
7+
```

attr/type.go

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ type Type interface {
2727
// to the Type passed as an argument.
2828
Equal(Type) bool
2929

30+
// String should return a human-friendly version of the Type.
31+
String() string
32+
3033
tftypes.AttributePathStepper
3134
}
3235

tfsdk/convert.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package tfsdk
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-go/tftypes"
10+
)
11+
12+
// ConvertValue creates a new attr.Value of the attr.Type `typ`, using the data
13+
// in `val`, which can be of any attr.Type so long as its TerraformType method
14+
// returns a tftypes.Type that `typ`'s ValueFromTerraform method can accept.
15+
func ConvertValue(ctx context.Context, val attr.Value, typ attr.Type) (attr.Value, diag.Diagnostics) {
16+
tftype := typ.TerraformType(ctx)
17+
tfval, err := val.ToTerraformValue(ctx)
18+
if err != nil {
19+
return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Error converting value",
20+
fmt.Sprintf("An unexpected error was encountered converting a %T to a %s. This is always a problem with the provider. Please tell the provider developers that %T ran into the following error during ToTerraformValue: %s", val, typ, val, err),
21+
)}
22+
}
23+
err = tftypes.ValidateValue(tftype, tfval)
24+
if err != nil {
25+
return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Error converting value",
26+
fmt.Sprintf("An unexpected error was encountered converting a %T to a %s. This is always a problem with the provider. Please tell the provider developers that %T is not compatible with %s.", val, typ, val, typ),
27+
)}
28+
}
29+
newVal := tftypes.NewValue(tftype, tfval)
30+
res, err := typ.ValueFromTerraform(ctx, newVal)
31+
if err != nil {
32+
return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Error converting value",
33+
fmt.Sprintf("An unexpected error was encountered converting a %T to a %s. This is always a problem with the provider. Please tell the provider developers that %s returned the following error when calling ValueFromTerraform: %s", val, typ, typ, err),
34+
)}
35+
}
36+
return res, nil
37+
}

tfsdk/convert_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package tfsdk
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
)
13+
14+
func TestConvert(t *testing.T) {
15+
t.Parallel()
16+
17+
type testCase struct {
18+
val attr.Value
19+
typ attr.Type
20+
expected attr.Value
21+
expectedDiags diag.Diagnostics
22+
}
23+
24+
tests := map[string]testCase{
25+
"string-to-testtype-string": {
26+
val: types.String{Value: "hello"},
27+
typ: testtypes.StringType{},
28+
expected: testtypes.String{
29+
String: types.String{Value: "hello"},
30+
CreatedBy: testtypes.StringType{},
31+
},
32+
},
33+
"testtype-string-to-string": {
34+
val: testtypes.String{
35+
String: types.String{Value: "hello"},
36+
CreatedBy: testtypes.StringType{},
37+
},
38+
typ: types.StringType,
39+
expected: types.String{Value: "hello"},
40+
},
41+
"string-to-number": {
42+
val: types.String{Value: "hello"},
43+
typ: types.NumberType,
44+
expectedDiags: diag.Diagnostics{diag.NewErrorDiagnostic(
45+
"Error converting value",
46+
"An unexpected error was encountered converting a types.String to a types.NumberType. This is always a problem with the provider. Please tell the provider developers that types.String is not compatible with types.NumberType.",
47+
)},
48+
},
49+
}
50+
51+
for name, tc := range tests {
52+
name, tc := name, tc
53+
t.Run(name, func(t *testing.T) {
54+
t.Parallel()
55+
56+
got, diags := ConvertValue(context.Background(), tc.val, tc.typ)
57+
58+
if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" {
59+
t.Fatalf("Unexpected diff in diags (-wanted, +got): %s", diff)
60+
}
61+
62+
if diags.HasError() {
63+
return
64+
}
65+
66+
if diff := cmp.Diff(got, tc.expected); diff != "" {
67+
t.Fatalf("Unexpected diff in result (-wanted, +got): %s", diff)
68+
}
69+
})
70+
}
71+
}

types/list.go

+5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ func (l ListType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathSte
101101
return l.ElemType, nil
102102
}
103103

104+
// String returns a human-friendly description of the ListType.
105+
func (l ListType) String() string {
106+
return "types.ListType[" + l.ElemType.String() + "]"
107+
}
108+
104109
// List represents a list of AttributeValues, all of the same type, indicated
105110
// by ElemType.
106111
type List struct {

types/map.go

+5
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ func (m MapType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep
9999
return m.ElemType, nil
100100
}
101101

102+
// String returns a human-friendly description of the MapType.
103+
func (m MapType) String() string {
104+
return "types.MapType[" + m.ElemType.String() + "]"
105+
}
106+
102107
// Map represents a map of AttributeValues, all of the same type, indicated by
103108
// ElemType. Keys for the map will always be strings.
104109
type Map struct {

types/object.go

+22
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package types
33
import (
44
"context"
55
"fmt"
6+
"sort"
7+
"strings"
68

79
"github.com/hashicorp/terraform-plugin-framework/attr"
810
"github.com/hashicorp/terraform-plugin-framework/diag"
@@ -112,6 +114,26 @@ func (o ObjectType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathS
112114
return o.AttrTypes[string(step.(tftypes.AttributeName))], nil
113115
}
114116

117+
// String returns a human-friendly description of the ObjectType.
118+
func (o ObjectType) String() string {
119+
var res strings.Builder
120+
res.WriteString("types.ObjectType[")
121+
keys := make([]string, 0, len(o.AttrTypes))
122+
for k := range o.AttrTypes {
123+
keys = append(keys, k)
124+
}
125+
sort.Strings(keys)
126+
for pos, key := range keys {
127+
if pos != 0 {
128+
res.WriteString(", ")
129+
}
130+
res.WriteString(`"` + key + `":`)
131+
res.WriteString(o.AttrTypes[key].String())
132+
}
133+
res.WriteString("]")
134+
return res.String()
135+
}
136+
115137
// Object represents an object
116138
type Object struct {
117139
// Unknown will be set to true if the entire object is an unknown value.

types/primitive_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ func (t testAttributeType) ApplyTerraform5AttributePathStep(_ tftypes.AttributeP
7171
panic("not implemented")
7272
}
7373

74+
// String should return a human-friendly version of the Type.
75+
func (t testAttributeType) String() string {
76+
panic("not implemented")
77+
}
78+
7479
func TestPrimitiveEqual(t *testing.T) {
7580
t.Parallel()
7681

0 commit comments

Comments
 (0)