Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

types: Introduce ListValueFrom, MapValueFrom, ObjectValueFrom, and SetValueFrom functions #522

Merged
merged 2 commits into from Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/522.txt
@@ -0,0 +1,3 @@
```release-note:enhancement
types: Added `ListValueFrom()`, `MapValueFrom()`, `ObjectValueFrom()`, and `SetValueFrom()` functions, which can create value types from standard Go types using reflection similar to `tfsdk.ValueFrom()`
```
29 changes: 29 additions & 0 deletions types/list.go
Expand Up @@ -230,6 +230,35 @@ func ListValue(elementType attr.Type, elements []attr.Value) (List, diag.Diagnos
}, nil
}

// ListValueFrom creates a List with a known value, using reflection rules.
// The elements must be a slice which can convert into the given element type.
// Access the value via the List type Elements or ElementsAs methods.
func ListValueFrom(ctx context.Context, elementType attr.Type, elements any) (List, diag.Diagnostics) {
attrValue, diags := reflect.FromValue(
ctx,
ListType{ElemType: elementType},
elements,
path.Empty(),
)

if diags.HasError() {
return ListUnknown(elementType), diags
}

list, ok := attrValue.(List)

// This should not happen, but ensure there is an error if it does.
if !ok {
diags.AddError(
"Unable to Convert List Value",
"An unexpected result occurred when creating a List using ListValueFrom. "+
"This is an issue with terraform-plugin-framework and should be reported to the provider developers.",
)
}

return list, diags
}

// ListValueMust creates a List with a known value, converting any diagnostics
// into a panic at runtime. Access the value via the List
// type Elements or ElementsAs methods.
Expand Down
118 changes: 118 additions & 0 deletions types/list_test.go
Expand Up @@ -334,6 +334,124 @@ func TestListValue(t *testing.T) {
}
}

func TestListValueFrom(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
elementType attr.Type
elements any
expected List
expectedDiags diag.Diagnostics
}{
"valid-StringType-[]attr.Value-empty": {
elementType: StringType,
elements: []attr.Value{},
expected: List{
ElemType: StringType,
Elems: []attr.Value{},
},
},
"valid-StringType-[]types.String-empty": {
elementType: StringType,
elements: []String{},
expected: List{
ElemType: StringType,
Elems: []attr.Value{},
},
},
"valid-StringType-[]types.String": {
elementType: StringType,
elements: []String{
StringNull(),
StringUnknown(),
StringValue("test"),
},
expected: List{
ElemType: StringType,
Elems: []attr.Value{
String{Null: true},
String{Unknown: true},
String{Value: "test"},
},
},
},
"valid-StringType-[]*string": {
elementType: StringType,
elements: []*string{
nil,
pointer("test1"),
pointer("test2"),
},
expected: List{
ElemType: StringType,
Elems: []attr.Value{
String{Null: true},
String{Value: "test1"},
String{Value: "test2"},
},
},
},
"valid-StringType-[]string": {
elementType: StringType,
elements: []string{
"test1",
"test2",
},
expected: List{
ElemType: StringType,
Elems: []attr.Value{
String{Value: "test1"},
String{Value: "test2"},
},
},
},
"invalid-not-slice": {
elementType: StringType,
elements: "oops",
expected: ListUnknown(StringType),
expectedDiags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Empty(),
"List Type Validation Error",
"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"expected List value, received tftypes.Value with value: tftypes.String<\"oops\">",
),
},
},
"invalid-type": {
elementType: StringType,
elements: []bool{true},
expected: ListUnknown(StringType),
expectedDiags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Empty().AtListIndex(0),
"Value Conversion Error",
"An unexpected error was encountered trying to convert the Terraform value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"can't unmarshal tftypes.Bool into *string, expected string",
),
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got, diags := ListValueFrom(context.Background(), testCase.elementType, testCase.elements)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}

if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" {
t.Errorf("unexpected diagnostics difference: %s", diff)
}
})
}
}

// This test verifies the assumptions that creating the Value via function then
// setting the fields directly has no effects.
func TestListValue_DeprecatedFieldSetting(t *testing.T) {
Expand Down
30 changes: 30 additions & 0 deletions types/map.go
Expand Up @@ -234,6 +234,36 @@ func MapValue(elementType attr.Type, elements map[string]attr.Value) (Map, diag.
}, nil
}

// MapValueFrom creates a Map with a known value, using reflection rules.
// The elements must be a map of string keys to values which can convert into
// the given element type. Access the value via the Map type Elements or
// ElementsAs methods.
func MapValueFrom(ctx context.Context, elementType attr.Type, elements any) (Map, diag.Diagnostics) {
attrValue, diags := reflect.FromValue(
ctx,
MapType{ElemType: elementType},
elements,
path.Empty(),
)

if diags.HasError() {
return MapUnknown(elementType), diags
}

m, ok := attrValue.(Map)

// This should not happen, but ensure there is an error if it does.
if !ok {
diags.AddError(
"Unable to Convert Map Value",
"An unexpected result occurred when creating a Map using MapValueFrom. "+
"This is an issue with terraform-plugin-framework and should be reported to the provider developers.",
)
}

return m, diags
}

// MapValueMust creates a Map with a known value, converting any diagnostics
// into a panic at runtime. Access the value via the Map
// type Elements or ElementsAs methods.
Expand Down
118 changes: 118 additions & 0 deletions types/map_test.go
Expand Up @@ -313,6 +313,124 @@ func TestMapValue(t *testing.T) {
}
}

func TestMapValueFrom(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
elementType attr.Type
elements any
expected Map
expectedDiags diag.Diagnostics
}{
"valid-StringType-map[string]attr.Value-empty": {
elementType: StringType,
elements: map[string]attr.Value{},
expected: Map{
ElemType: StringType,
Elems: map[string]attr.Value{},
},
},
"valid-StringType-map[string]types.String-empty": {
elementType: StringType,
elements: map[string]String{},
expected: Map{
ElemType: StringType,
Elems: map[string]attr.Value{},
},
},
"valid-StringType-map[string]types.String": {
elementType: StringType,
elements: map[string]String{
"key1": StringNull(),
"key2": StringUnknown(),
"key3": StringValue("test"),
},
expected: Map{
ElemType: StringType,
Elems: map[string]attr.Value{
"key1": String{Null: true},
"key2": String{Unknown: true},
"key3": String{Value: "test"},
},
},
},
"valid-StringType-map[string]*string": {
elementType: StringType,
elements: map[string]*string{
"key1": nil,
"key2": pointer("test1"),
"key3": pointer("test2"),
},
expected: Map{
ElemType: StringType,
Elems: map[string]attr.Value{
"key1": String{Null: true},
"key2": String{Value: "test1"},
"key3": String{Value: "test2"},
},
},
},
"valid-StringType-map[string]string": {
elementType: StringType,
elements: map[string]string{
"key1": "test1",
"key2": "test2",
},
expected: Map{
ElemType: StringType,
Elems: map[string]attr.Value{
"key1": String{Value: "test1"},
"key2": String{Value: "test2"},
},
},
},
"invalid-not-map": {
elementType: StringType,
elements: "oops",
expected: MapUnknown(StringType),
expectedDiags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Empty(),
"Map Type Validation Error",
"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"expected Map value, received tftypes.Value with value: tftypes.String<\"oops\">",
),
},
},
"invalid-type": {
elementType: StringType,
elements: map[string]bool{"key1": true},
expected: MapUnknown(StringType),
expectedDiags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Empty().AtMapKey("key1"),
"Value Conversion Error",
"An unexpected error was encountered trying to convert the Terraform value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"can't unmarshal tftypes.Bool into *string, expected string",
),
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got, diags := MapValueFrom(context.Background(), testCase.elementType, testCase.elements)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}

if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" {
t.Errorf("unexpected diagnostics difference: %s", diff)
}
})
}
}

// This test verifies the assumptions that creating the Value via function then
// setting the fields directly has no effects.
func TestMapValue_DeprecatedFieldSetting(t *testing.T) {
Expand Down
30 changes: 30 additions & 0 deletions types/object.go
Expand Up @@ -239,6 +239,36 @@ func ObjectValue(attributeTypes map[string]attr.Type, attributes map[string]attr
}, nil
}

// ObjectValueFrom creates a Object with a known value, using reflection rules.
// The attributes must be a map of string attribute names to attribute values
// which can convert into the given attribute type or a struct with tfsdk field
// tags. Access the value via the Object type Elements or ElementsAs methods.
func ObjectValueFrom(ctx context.Context, attributeTypes map[string]attr.Type, attributes any) (Object, diag.Diagnostics) {
attrValue, diags := reflect.FromValue(
ctx,
ObjectType{AttrTypes: attributeTypes},
attributes,
path.Empty(),
)

if diags.HasError() {
return ObjectUnknown(attributeTypes), diags
}

m, ok := attrValue.(Object)

// This should not happen, but ensure there is an error if it does.
if !ok {
diags.AddError(
"Unable to Convert Object Value",
"An unexpected result occurred when creating a Object using ObjectValueFrom. "+
"This is an issue with terraform-plugin-framework and should be reported to the provider developers.",
)
}

return m, diags
}

// ObjectValueMust creates a Object with a known value, converting any diagnostics
// into a panic at runtime. Access the value via the Object
// type Elements or ElementsAs methods.
Expand Down