Skip to content

Commit b0e3081

Browse files
authored
types: Support Set and SetType (#126)
Reference: #53
1 parent 9fd6d73 commit b0e3081

14 files changed

+2588
-60
lines changed

.changelog/126.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:feature
2+
types: Support `Set` and `SetType`
3+
```
4+
5+
```release-note:bug
6+
attr: Ensure `List` types implementing `attr.TypeWithValidate` call `ElementType` validation only if that type implements `attr.TypeWithValidate`
7+
```

internal/reflect/slice.go

+24-5
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@ func reflectSlice(ctx context.Context, typ attr.Type, val tftypes.Value, target
6565
targetValue := reflect.Zero(elemType)
6666

6767
// update our path so we can have nice errors
68-
path := path.WithElementKeyInt(int64(pos))
68+
valPath := path.WithElementKeyInt(int64(pos))
69+
70+
if typ.TerraformType(ctx).Is(tftypes.Set{}) {
71+
valPath = path.WithElementKeyValue(value)
72+
}
6973

7074
// reflect the value into our new target
71-
val, valDiags := BuildValue(ctx, elemAttrType, value, targetValue, opts, path)
75+
val, valDiags := BuildValue(ctx, elemAttrType, value, targetValue, opts, valPath)
7276
diags.Append(valDiags...)
7377

7478
if diags.HasError() {
@@ -135,25 +139,40 @@ func FromSlice(ctx context.Context, typ attr.Type, val reflect.Value, path *tfty
135139
elemType := t.ElementType()
136140
tfElems := make([]tftypes.Value, 0, val.Len())
137141
for i := 0; i < val.Len(); i++ {
138-
val, valDiags := FromValue(ctx, elemType, val.Index(i).Interface(), path.WithElementKeyInt(int64(i)))
142+
// The underlying reflect.Slice is fetched by Index(). For set types,
143+
// the path is value-based instead of index-based. Since there is only
144+
// the index until the value is retrieved, this will pass the
145+
// technically incorrect index-based path at first for framework
146+
// debugging purposes, then correct the path afterwards.
147+
valPath := path.WithElementKeyInt(int64(i))
148+
149+
val, valDiags := FromValue(ctx, elemType, val.Index(i).Interface(), valPath)
139150
diags.Append(valDiags...)
140151

141152
if diags.HasError() {
142153
return nil, diags
143154
}
155+
144156
tfVal, err := val.ToTerraformValue(ctx)
157+
145158
if err != nil {
146159
return nil, append(diags, toTerraformValueErrorDiag(err, path))
147160
}
161+
148162
err = tftypes.ValidateValue(elemType.TerraformType(ctx), tfVal)
163+
149164
if err != nil {
150165
return nil, append(diags, validateValueErrorDiag(err, path))
151166
}
152167

153168
tfElemVal := tftypes.NewValue(elemType.TerraformType(ctx), tfVal)
154169

155-
if typeWithValidate, ok := typ.(attr.TypeWithValidate); ok {
156-
diags.Append(typeWithValidate.Validate(ctx, tfElemVal, path.WithElementKeyInt(int64(i)))...)
170+
if tfType.Is(tftypes.Set{}) {
171+
valPath = path.WithElementKeyValue(tfElemVal)
172+
}
173+
174+
if typeWithValidate, ok := elemType.(attr.TypeWithValidate); ok {
175+
diags.Append(typeWithValidate.Validate(ctx, tfElemVal, valPath)...)
157176

158177
if diags.HasError() {
159178
return nil, diags

internal/reflect/struct_test.go

+176-22
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,16 @@ func TestNewStruct_complex(t *testing.T) {
205205
t.Parallel()
206206

207207
type myStruct struct {
208-
Slice []string `tfsdk:"slice"`
209-
SliceOfStructs []struct {
208+
ListSlice []string `tfsdk:"list_slice"`
209+
ListSliceOfStructs []struct {
210210
A string `tfsdk:"a"`
211211
B int `tfsdk:"b"`
212-
} `tfsdk:"slice_of_structs"`
212+
} `tfsdk:"list_slice_of_structs"`
213+
SetSlice []string `tfsdk:"set_slice"`
214+
SetSliceOfStructs []struct {
215+
A string `tfsdk:"a"`
216+
B int `tfsdk:"b"`
217+
} `tfsdk:"set_slice_of_structs"`
213218
Struct struct {
214219
A bool `tfsdk:"a"`
215220
Slice []float64 `tfsdk:"slice"`
@@ -226,10 +231,21 @@ func TestNewStruct_complex(t *testing.T) {
226231
var s myStruct
227232
result, diags := refl.Struct(context.Background(), types.ObjectType{
228233
AttrTypes: map[string]attr.Type{
229-
"slice": types.ListType{
234+
"list_slice": types.ListType{
230235
ElemType: types.StringType,
231236
},
232-
"slice_of_structs": types.ListType{
237+
"list_slice_of_structs": types.ListType{
238+
ElemType: types.ObjectType{
239+
AttrTypes: map[string]attr.Type{
240+
"a": types.StringType,
241+
"b": types.NumberType,
242+
},
243+
},
244+
},
245+
"set_slice": types.SetType{
246+
ElemType: types.StringType,
247+
},
248+
"set_slice_of_structs": types.SetType{
233249
ElemType: types.ObjectType{
234250
AttrTypes: map[string]attr.Type{
235251
"a": types.StringType,
@@ -260,10 +276,21 @@ func TestNewStruct_complex(t *testing.T) {
260276
},
261277
}, tftypes.NewValue(tftypes.Object{
262278
AttributeTypes: map[string]tftypes.Type{
263-
"slice": tftypes.List{
279+
"list_slice": tftypes.List{
264280
ElementType: tftypes.String,
265281
},
266-
"slice_of_structs": tftypes.List{
282+
"list_slice_of_structs": tftypes.List{
283+
ElementType: tftypes.Object{
284+
AttributeTypes: map[string]tftypes.Type{
285+
"a": tftypes.String,
286+
"b": tftypes.Number,
287+
},
288+
},
289+
},
290+
"set_slice": tftypes.Set{
291+
ElementType: tftypes.String,
292+
},
293+
"set_slice_of_structs": tftypes.Set{
267294
ElementType: tftypes.Object{
268295
AttributeTypes: map[string]tftypes.Type{
269296
"a": tftypes.String,
@@ -293,14 +320,48 @@ func TestNewStruct_complex(t *testing.T) {
293320
"unhandled_unknown": tftypes.String,
294321
},
295322
}, map[string]tftypes.Value{
296-
"slice": tftypes.NewValue(tftypes.List{
323+
"list_slice": tftypes.NewValue(tftypes.List{
324+
ElementType: tftypes.String,
325+
}, []tftypes.Value{
326+
tftypes.NewValue(tftypes.String, "red"),
327+
tftypes.NewValue(tftypes.String, "blue"),
328+
tftypes.NewValue(tftypes.String, "green"),
329+
}),
330+
"list_slice_of_structs": tftypes.NewValue(tftypes.List{
331+
ElementType: tftypes.Object{
332+
AttributeTypes: map[string]tftypes.Type{
333+
"a": tftypes.String,
334+
"b": tftypes.Number,
335+
},
336+
},
337+
}, []tftypes.Value{
338+
tftypes.NewValue(tftypes.Object{
339+
AttributeTypes: map[string]tftypes.Type{
340+
"a": tftypes.String,
341+
"b": tftypes.Number,
342+
},
343+
}, map[string]tftypes.Value{
344+
"a": tftypes.NewValue(tftypes.String, "hello, world"),
345+
"b": tftypes.NewValue(tftypes.Number, 123),
346+
}),
347+
tftypes.NewValue(tftypes.Object{
348+
AttributeTypes: map[string]tftypes.Type{
349+
"a": tftypes.String,
350+
"b": tftypes.Number,
351+
},
352+
}, map[string]tftypes.Value{
353+
"a": tftypes.NewValue(tftypes.String, "goodnight, moon"),
354+
"b": tftypes.NewValue(tftypes.Number, 456),
355+
}),
356+
}),
357+
"set_slice": tftypes.NewValue(tftypes.Set{
297358
ElementType: tftypes.String,
298359
}, []tftypes.Value{
299360
tftypes.NewValue(tftypes.String, "red"),
300361
tftypes.NewValue(tftypes.String, "blue"),
301362
tftypes.NewValue(tftypes.String, "green"),
302363
}),
303-
"slice_of_structs": tftypes.NewValue(tftypes.List{
364+
"set_slice_of_structs": tftypes.NewValue(tftypes.Set{
304365
ElementType: tftypes.Object{
305366
AttributeTypes: map[string]tftypes.Type{
306367
"a": tftypes.String,
@@ -380,8 +441,22 @@ func TestNewStruct_complex(t *testing.T) {
380441
}
381442
str := "pointed"
382443
expected := myStruct{
383-
Slice: []string{"red", "blue", "green"},
384-
SliceOfStructs: []struct {
444+
ListSlice: []string{"red", "blue", "green"},
445+
ListSliceOfStructs: []struct {
446+
A string `tfsdk:"a"`
447+
B int `tfsdk:"b"`
448+
}{
449+
{
450+
A: "hello, world",
451+
B: 123,
452+
},
453+
{
454+
A: "goodnight, moon",
455+
B: 456,
456+
},
457+
},
458+
SetSlice: []string{"red", "blue", "green"},
459+
SetSliceOfStructs: []struct {
385460
A string `tfsdk:"a"`
386461
B int `tfsdk:"b"`
387462
}{
@@ -471,11 +546,16 @@ func TestFromStruct_complex(t *testing.T) {
471546
t.Parallel()
472547

473548
type myStruct struct {
474-
Slice []string `tfsdk:"slice"`
475-
SliceOfStructs []struct {
549+
ListSlice []string `tfsdk:"list_slice"`
550+
ListSliceOfStructs []struct {
551+
A string `tfsdk:"a"`
552+
B int `tfsdk:"b"`
553+
} `tfsdk:"list_slice_of_structs"`
554+
SetSlice []string `tfsdk:"set_slice"`
555+
SetSliceOfStructs []struct {
476556
A string `tfsdk:"a"`
477557
B int `tfsdk:"b"`
478-
} `tfsdk:"slice_of_structs"`
558+
} `tfsdk:"set_slice_of_structs"`
479559
Struct struct {
480560
A bool `tfsdk:"a"`
481561
Slice []float64 `tfsdk:"slice"`
@@ -492,8 +572,22 @@ func TestFromStruct_complex(t *testing.T) {
492572
}
493573
str := "pointed"
494574
s := myStruct{
495-
Slice: []string{"red", "blue", "green"},
496-
SliceOfStructs: []struct {
575+
ListSlice: []string{"red", "blue", "green"},
576+
ListSliceOfStructs: []struct {
577+
A string `tfsdk:"a"`
578+
B int `tfsdk:"b"`
579+
}{
580+
{
581+
A: "hello, world",
582+
B: 123,
583+
},
584+
{
585+
A: "goodnight, moon",
586+
B: 456,
587+
},
588+
},
589+
SetSlice: []string{"red", "blue", "green"},
590+
SetSliceOfStructs: []struct {
497591
A string `tfsdk:"a"`
498592
B int `tfsdk:"b"`
499593
}{
@@ -536,10 +630,21 @@ func TestFromStruct_complex(t *testing.T) {
536630
}
537631
result, diags := refl.FromStruct(context.Background(), types.ObjectType{
538632
AttrTypes: map[string]attr.Type{
539-
"slice": types.ListType{
633+
"list_slice": types.ListType{
540634
ElemType: types.StringType,
541635
},
542-
"slice_of_structs": types.ListType{
636+
"list_slice_of_structs": types.ListType{
637+
ElemType: types.ObjectType{
638+
AttrTypes: map[string]attr.Type{
639+
"a": types.StringType,
640+
"b": types.NumberType,
641+
},
642+
},
643+
},
644+
"set_slice": types.SetType{
645+
ElemType: types.StringType,
646+
},
647+
"set_slice_of_structs": types.SetType{
543648
ElemType: types.ObjectType{
544649
AttrTypes: map[string]attr.Type{
545650
"a": types.StringType,
@@ -575,10 +680,21 @@ func TestFromStruct_complex(t *testing.T) {
575680
}
576681
expected := types.Object{
577682
AttrTypes: map[string]attr.Type{
578-
"slice": types.ListType{
683+
"list_slice": types.ListType{
684+
ElemType: types.StringType,
685+
},
686+
"list_slice_of_structs": types.ListType{
687+
ElemType: types.ObjectType{
688+
AttrTypes: map[string]attr.Type{
689+
"a": types.StringType,
690+
"b": types.NumberType,
691+
},
692+
},
693+
},
694+
"set_slice": types.SetType{
579695
ElemType: types.StringType,
580696
},
581-
"slice_of_structs": types.ListType{
697+
"set_slice_of_structs": types.SetType{
582698
ElemType: types.ObjectType{
583699
AttrTypes: map[string]attr.Type{
584700
"a": types.StringType,
@@ -609,15 +725,53 @@ func TestFromStruct_complex(t *testing.T) {
609725
"uint": types.NumberType,
610726
},
611727
Attrs: map[string]attr.Value{
612-
"slice": types.List{
728+
"list_slice": types.List{
729+
ElemType: types.StringType,
730+
Elems: []attr.Value{
731+
types.String{Value: "red"},
732+
types.String{Value: "blue"},
733+
types.String{Value: "green"},
734+
},
735+
},
736+
"list_slice_of_structs": types.List{
737+
ElemType: types.ObjectType{
738+
AttrTypes: map[string]attr.Type{
739+
"a": types.StringType,
740+
"b": types.NumberType,
741+
},
742+
},
743+
Elems: []attr.Value{
744+
types.Object{
745+
AttrTypes: map[string]attr.Type{
746+
"a": types.StringType,
747+
"b": types.NumberType,
748+
},
749+
Attrs: map[string]attr.Value{
750+
"a": types.String{Value: "hello, world"},
751+
"b": types.Number{Value: big.NewFloat(123)},
752+
},
753+
},
754+
types.Object{
755+
AttrTypes: map[string]attr.Type{
756+
"a": types.StringType,
757+
"b": types.NumberType,
758+
},
759+
Attrs: map[string]attr.Value{
760+
"a": types.String{Value: "goodnight, moon"},
761+
"b": types.Number{Value: big.NewFloat(456)},
762+
},
763+
},
764+
},
765+
},
766+
"set_slice": types.Set{
613767
ElemType: types.StringType,
614768
Elems: []attr.Value{
615769
types.String{Value: "red"},
616770
types.String{Value: "blue"},
617771
types.String{Value: "green"},
618772
},
619773
},
620-
"slice_of_structs": types.List{
774+
"set_slice_of_structs": types.Set{
621775
ElemType: types.ObjectType{
622776
AttrTypes: map[string]attr.Type{
623777
"a": types.StringType,

0 commit comments

Comments
 (0)