Skip to content

Commit de565fa

Browse files
authored
types: Introduce ListValueFrom, MapValueFrom, ObjectValueFrom, and SetValueFrom functions (#522)
Reference: #520 These will enable provider developers to use the framework type system's built-in conversion rules to create collection types, rather than using more generic `tfsdk.ValueFrom()` or other methodologies. In this example, a map using standard Go types is used to create a `types.Map` framework type with known values: ```go apiMap := map[string]string{ "key1": "value1", "key2": "value2", } fwMap, diags := types.MapValueFrom(ctx, types.StringType, apiMap) ``` There may be additional use cases or needs that get teased out with this introduction, such as the ability to create a `types.Object` from a `map[string]any`, however those can be handled in potential future feature requests.
1 parent 4b21cf8 commit de565fa

File tree

11 files changed

+648
-0
lines changed

11 files changed

+648
-0
lines changed

.changelog/522.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
types: Added `ListValueFrom()`, `MapValueFrom()`, `ObjectValueFrom()`, and `SetValueFrom()` functions, which can create value types from standard Go types using reflection similar to `tfsdk.ValueFrom()`
3+
```

types/list.go

+29
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,35 @@ func ListValue(elementType attr.Type, elements []attr.Value) (List, diag.Diagnos
230230
}, nil
231231
}
232232

233+
// ListValueFrom creates a List with a known value, using reflection rules.
234+
// The elements must be a slice which can convert into the given element type.
235+
// Access the value via the List type Elements or ElementsAs methods.
236+
func ListValueFrom(ctx context.Context, elementType attr.Type, elements any) (List, diag.Diagnostics) {
237+
attrValue, diags := reflect.FromValue(
238+
ctx,
239+
ListType{ElemType: elementType},
240+
elements,
241+
path.Empty(),
242+
)
243+
244+
if diags.HasError() {
245+
return ListUnknown(elementType), diags
246+
}
247+
248+
list, ok := attrValue.(List)
249+
250+
// This should not happen, but ensure there is an error if it does.
251+
if !ok {
252+
diags.AddError(
253+
"Unable to Convert List Value",
254+
"An unexpected result occurred when creating a List using ListValueFrom. "+
255+
"This is an issue with terraform-plugin-framework and should be reported to the provider developers.",
256+
)
257+
}
258+
259+
return list, diags
260+
}
261+
233262
// ListValueMust creates a List with a known value, converting any diagnostics
234263
// into a panic at runtime. Access the value via the List
235264
// type Elements or ElementsAs methods.

types/list_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,124 @@ func TestListValue(t *testing.T) {
334334
}
335335
}
336336

337+
func TestListValueFrom(t *testing.T) {
338+
t.Parallel()
339+
340+
testCases := map[string]struct {
341+
elementType attr.Type
342+
elements any
343+
expected List
344+
expectedDiags diag.Diagnostics
345+
}{
346+
"valid-StringType-[]attr.Value-empty": {
347+
elementType: StringType,
348+
elements: []attr.Value{},
349+
expected: List{
350+
ElemType: StringType,
351+
Elems: []attr.Value{},
352+
},
353+
},
354+
"valid-StringType-[]types.String-empty": {
355+
elementType: StringType,
356+
elements: []String{},
357+
expected: List{
358+
ElemType: StringType,
359+
Elems: []attr.Value{},
360+
},
361+
},
362+
"valid-StringType-[]types.String": {
363+
elementType: StringType,
364+
elements: []String{
365+
StringNull(),
366+
StringUnknown(),
367+
StringValue("test"),
368+
},
369+
expected: List{
370+
ElemType: StringType,
371+
Elems: []attr.Value{
372+
String{Null: true},
373+
String{Unknown: true},
374+
String{Value: "test"},
375+
},
376+
},
377+
},
378+
"valid-StringType-[]*string": {
379+
elementType: StringType,
380+
elements: []*string{
381+
nil,
382+
pointer("test1"),
383+
pointer("test2"),
384+
},
385+
expected: List{
386+
ElemType: StringType,
387+
Elems: []attr.Value{
388+
String{Null: true},
389+
String{Value: "test1"},
390+
String{Value: "test2"},
391+
},
392+
},
393+
},
394+
"valid-StringType-[]string": {
395+
elementType: StringType,
396+
elements: []string{
397+
"test1",
398+
"test2",
399+
},
400+
expected: List{
401+
ElemType: StringType,
402+
Elems: []attr.Value{
403+
String{Value: "test1"},
404+
String{Value: "test2"},
405+
},
406+
},
407+
},
408+
"invalid-not-slice": {
409+
elementType: StringType,
410+
elements: "oops",
411+
expected: ListUnknown(StringType),
412+
expectedDiags: diag.Diagnostics{
413+
diag.NewAttributeErrorDiagnostic(
414+
path.Empty(),
415+
"List Type Validation Error",
416+
"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"+
417+
"expected List value, received tftypes.Value with value: tftypes.String<\"oops\">",
418+
),
419+
},
420+
},
421+
"invalid-type": {
422+
elementType: StringType,
423+
elements: []bool{true},
424+
expected: ListUnknown(StringType),
425+
expectedDiags: diag.Diagnostics{
426+
diag.NewAttributeErrorDiagnostic(
427+
path.Empty().AtListIndex(0),
428+
"Value Conversion Error",
429+
"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"+
430+
"can't unmarshal tftypes.Bool into *string, expected string",
431+
),
432+
},
433+
},
434+
}
435+
436+
for name, testCase := range testCases {
437+
name, testCase := name, testCase
438+
439+
t.Run(name, func(t *testing.T) {
440+
t.Parallel()
441+
442+
got, diags := ListValueFrom(context.Background(), testCase.elementType, testCase.elements)
443+
444+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
445+
t.Errorf("unexpected difference: %s", diff)
446+
}
447+
448+
if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" {
449+
t.Errorf("unexpected diagnostics difference: %s", diff)
450+
}
451+
})
452+
}
453+
}
454+
337455
// This test verifies the assumptions that creating the Value via function then
338456
// setting the fields directly has no effects.
339457
func TestListValue_DeprecatedFieldSetting(t *testing.T) {

types/map.go

+30
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,36 @@ func MapValue(elementType attr.Type, elements map[string]attr.Value) (Map, diag.
234234
}, nil
235235
}
236236

237+
// MapValueFrom creates a Map with a known value, using reflection rules.
238+
// The elements must be a map of string keys to values which can convert into
239+
// the given element type. Access the value via the Map type Elements or
240+
// ElementsAs methods.
241+
func MapValueFrom(ctx context.Context, elementType attr.Type, elements any) (Map, diag.Diagnostics) {
242+
attrValue, diags := reflect.FromValue(
243+
ctx,
244+
MapType{ElemType: elementType},
245+
elements,
246+
path.Empty(),
247+
)
248+
249+
if diags.HasError() {
250+
return MapUnknown(elementType), diags
251+
}
252+
253+
m, ok := attrValue.(Map)
254+
255+
// This should not happen, but ensure there is an error if it does.
256+
if !ok {
257+
diags.AddError(
258+
"Unable to Convert Map Value",
259+
"An unexpected result occurred when creating a Map using MapValueFrom. "+
260+
"This is an issue with terraform-plugin-framework and should be reported to the provider developers.",
261+
)
262+
}
263+
264+
return m, diags
265+
}
266+
237267
// MapValueMust creates a Map with a known value, converting any diagnostics
238268
// into a panic at runtime. Access the value via the Map
239269
// type Elements or ElementsAs methods.

types/map_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,124 @@ func TestMapValue(t *testing.T) {
313313
}
314314
}
315315

316+
func TestMapValueFrom(t *testing.T) {
317+
t.Parallel()
318+
319+
testCases := map[string]struct {
320+
elementType attr.Type
321+
elements any
322+
expected Map
323+
expectedDiags diag.Diagnostics
324+
}{
325+
"valid-StringType-map[string]attr.Value-empty": {
326+
elementType: StringType,
327+
elements: map[string]attr.Value{},
328+
expected: Map{
329+
ElemType: StringType,
330+
Elems: map[string]attr.Value{},
331+
},
332+
},
333+
"valid-StringType-map[string]types.String-empty": {
334+
elementType: StringType,
335+
elements: map[string]String{},
336+
expected: Map{
337+
ElemType: StringType,
338+
Elems: map[string]attr.Value{},
339+
},
340+
},
341+
"valid-StringType-map[string]types.String": {
342+
elementType: StringType,
343+
elements: map[string]String{
344+
"key1": StringNull(),
345+
"key2": StringUnknown(),
346+
"key3": StringValue("test"),
347+
},
348+
expected: Map{
349+
ElemType: StringType,
350+
Elems: map[string]attr.Value{
351+
"key1": String{Null: true},
352+
"key2": String{Unknown: true},
353+
"key3": String{Value: "test"},
354+
},
355+
},
356+
},
357+
"valid-StringType-map[string]*string": {
358+
elementType: StringType,
359+
elements: map[string]*string{
360+
"key1": nil,
361+
"key2": pointer("test1"),
362+
"key3": pointer("test2"),
363+
},
364+
expected: Map{
365+
ElemType: StringType,
366+
Elems: map[string]attr.Value{
367+
"key1": String{Null: true},
368+
"key2": String{Value: "test1"},
369+
"key3": String{Value: "test2"},
370+
},
371+
},
372+
},
373+
"valid-StringType-map[string]string": {
374+
elementType: StringType,
375+
elements: map[string]string{
376+
"key1": "test1",
377+
"key2": "test2",
378+
},
379+
expected: Map{
380+
ElemType: StringType,
381+
Elems: map[string]attr.Value{
382+
"key1": String{Value: "test1"},
383+
"key2": String{Value: "test2"},
384+
},
385+
},
386+
},
387+
"invalid-not-map": {
388+
elementType: StringType,
389+
elements: "oops",
390+
expected: MapUnknown(StringType),
391+
expectedDiags: diag.Diagnostics{
392+
diag.NewAttributeErrorDiagnostic(
393+
path.Empty(),
394+
"Map Type Validation Error",
395+
"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"+
396+
"expected Map value, received tftypes.Value with value: tftypes.String<\"oops\">",
397+
),
398+
},
399+
},
400+
"invalid-type": {
401+
elementType: StringType,
402+
elements: map[string]bool{"key1": true},
403+
expected: MapUnknown(StringType),
404+
expectedDiags: diag.Diagnostics{
405+
diag.NewAttributeErrorDiagnostic(
406+
path.Empty().AtMapKey("key1"),
407+
"Value Conversion Error",
408+
"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"+
409+
"can't unmarshal tftypes.Bool into *string, expected string",
410+
),
411+
},
412+
},
413+
}
414+
415+
for name, testCase := range testCases {
416+
name, testCase := name, testCase
417+
418+
t.Run(name, func(t *testing.T) {
419+
t.Parallel()
420+
421+
got, diags := MapValueFrom(context.Background(), testCase.elementType, testCase.elements)
422+
423+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
424+
t.Errorf("unexpected difference: %s", diff)
425+
}
426+
427+
if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" {
428+
t.Errorf("unexpected diagnostics difference: %s", diff)
429+
}
430+
})
431+
}
432+
}
433+
316434
// This test verifies the assumptions that creating the Value via function then
317435
// setting the fields directly has no effects.
318436
func TestMapValue_DeprecatedFieldSetting(t *testing.T) {

types/object.go

+30
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,36 @@ func ObjectValue(attributeTypes map[string]attr.Type, attributes map[string]attr
239239
}, nil
240240
}
241241

242+
// ObjectValueFrom creates a Object with a known value, using reflection rules.
243+
// The attributes must be a map of string attribute names to attribute values
244+
// which can convert into the given attribute type or a struct with tfsdk field
245+
// tags. Access the value via the Object type Elements or ElementsAs methods.
246+
func ObjectValueFrom(ctx context.Context, attributeTypes map[string]attr.Type, attributes any) (Object, diag.Diagnostics) {
247+
attrValue, diags := reflect.FromValue(
248+
ctx,
249+
ObjectType{AttrTypes: attributeTypes},
250+
attributes,
251+
path.Empty(),
252+
)
253+
254+
if diags.HasError() {
255+
return ObjectUnknown(attributeTypes), diags
256+
}
257+
258+
m, ok := attrValue.(Object)
259+
260+
// This should not happen, but ensure there is an error if it does.
261+
if !ok {
262+
diags.AddError(
263+
"Unable to Convert Object Value",
264+
"An unexpected result occurred when creating a Object using ObjectValueFrom. "+
265+
"This is an issue with terraform-plugin-framework and should be reported to the provider developers.",
266+
)
267+
}
268+
269+
return m, diags
270+
}
271+
242272
// ObjectValueMust creates a Object with a known value, converting any diagnostics
243273
// into a panic at runtime. Access the value via the Object
244274
// type Elements or ElementsAs methods.

0 commit comments

Comments
 (0)