Skip to content

Commit 5f9d3d3

Browse files
authored
types: Add element type validation to ListType, MapType, and SetType (#481)
Reference: #467
1 parent e4d656e commit 5f9d3d3

File tree

10 files changed

+260
-7
lines changed

10 files changed

+260
-7
lines changed

.changelog/481.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
types: Ensured `List`, `Map`, and `Set` types with `xattr.TypeWithValidate` elements run validation on those elements
3+
```

internal/fwserver/attribute_validation_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ func TestAttributeValidate(t *testing.T) {
143143
Diagnostics: diag.Diagnostics{
144144
diag.NewAttributeErrorDiagnostic(
145145
path.Root("test"),
146-
"Configuration Read Error",
147-
"An unexpected error was encountered trying to convert an attribute value from the configuration. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
148-
"Error: can't use tftypes.String<\"testvalue\"> as value of List with ElementType types.primitive, can only use tftypes.String values",
146+
"List Type Validation Error",
147+
"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"+
148+
"expected List value, received tftypes.Value with value: tftypes.String<\"testvalue\">",
149149
),
150150
},
151151
},

internal/fwserver/schema_plan_modification_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestSchemaModifyPlan(t *testing.T) {
8989
Diagnostics: diag.Diagnostics{
9090
diag.NewAttributeErrorDiagnostic(
9191
path.Root("test"),
92-
"Configuration Read Error",
92+
"List Type Validation Error",
9393
"An unexpected error was encountered trying to convert an attribute value from the configuration. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
9494
"Error: can't use tftypes.String<\"testvalue\"> as value of List with ElementType types.primitive, can only use tftypes.String values",
9595
),
@@ -175,7 +175,7 @@ func TestSchemaModifyPlan(t *testing.T) {
175175
),
176176
diag.NewAttributeErrorDiagnostic(
177177
path.Root("test"),
178-
"Configuration Read Error",
178+
"List Type Validation Error",
179179
"An unexpected error was encountered trying to convert an attribute value from the configuration. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
180180
"Error: can't use tftypes.String<\"testvalue\"> as value of List with ElementType types.primitive, can only use tftypes.String values",
181181
),

internal/reflect/map.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func FromMap(ctx context.Context, typ attr.TypeWithElementType, val reflect.Valu
145145
return nil, append(diags, toTerraformValueErrorDiag(err, path))
146146
}
147147

148-
if typeWithValidate, ok := typ.(xattr.TypeWithValidate); ok {
148+
if typeWithValidate, ok := elemType.(xattr.TypeWithValidate); ok {
149149
diags.Append(typeWithValidate.Validate(ctx, tfVal, path.AtMapKey(key.String()))...)
150150

151151
if diags.HasError() {

types/list.go

+51
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"strings"
77

88
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
910
"github.com/hashicorp/terraform-plugin-framework/diag"
1011
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
1113
"github.com/hashicorp/terraform-plugin-go/tftypes"
1214
)
1315

@@ -111,6 +113,55 @@ func (l ListType) String() string {
111113
return "types.ListType[" + l.ElemType.String() + "]"
112114
}
113115

116+
// Validate validates all elements of the list that are of type
117+
// xattr.TypeWithValidate.
118+
func (l ListType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics {
119+
var diags diag.Diagnostics
120+
121+
if in.Type() == nil {
122+
return diags
123+
}
124+
125+
if !in.Type().Is(tftypes.List{}) {
126+
err := fmt.Errorf("expected List value, received %T with value: %v", in, in)
127+
diags.AddAttributeError(
128+
path,
129+
"List Type Validation Error",
130+
"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"+err.Error(),
131+
)
132+
return diags
133+
}
134+
135+
if !in.IsKnown() || in.IsNull() {
136+
return diags
137+
}
138+
139+
var elems []tftypes.Value
140+
141+
if err := in.As(&elems); err != nil {
142+
diags.AddAttributeError(
143+
path,
144+
"List Type Validation Error",
145+
"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"+err.Error(),
146+
)
147+
return diags
148+
}
149+
150+
validatableType, isValidatable := l.ElemType.(xattr.TypeWithValidate)
151+
if !isValidatable {
152+
return diags
153+
}
154+
155+
for index, elem := range elems {
156+
if !elem.IsFullyKnown() {
157+
continue
158+
}
159+
diags = append(diags, validatableType.Validate(ctx, elem, path.AtListIndex(index))...)
160+
}
161+
162+
return diags
163+
}
164+
114165
// List represents a list of attr.Values, all of the same type, indicated
115166
// by ElemType.
116167
type List struct {

types/list_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66

77
"github.com/google/go-cmp/cmp"
88
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/path"
911
"github.com/hashicorp/terraform-plugin-go/tftypes"
1012
)
1113

@@ -768,3 +770,59 @@ func TestListString(t *testing.T) {
768770
})
769771
}
770772
}
773+
774+
func TestListTypeValidate(t *testing.T) {
775+
t.Parallel()
776+
777+
testCases := map[string]struct {
778+
listType ListType
779+
tfValue tftypes.Value
780+
path path.Path
781+
expectedDiags diag.Diagnostics
782+
}{
783+
"wrong-value-type": {
784+
listType: ListType{
785+
ElemType: StringType,
786+
},
787+
tfValue: tftypes.NewValue(tftypes.Set{
788+
ElementType: tftypes.String,
789+
}, []tftypes.Value{
790+
tftypes.NewValue(tftypes.String, "testvalue"),
791+
}),
792+
path: path.Root("test"),
793+
expectedDiags: diag.Diagnostics{
794+
diag.NewAttributeErrorDiagnostic(
795+
path.Root("test"),
796+
"List Type Validation Error",
797+
"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"+
798+
"expected List value, received tftypes.Value with value: tftypes.Set[tftypes.String]<tftypes.String<\"testvalue\">>",
799+
),
800+
},
801+
},
802+
"no-validation": {
803+
listType: ListType{
804+
ElemType: StringType,
805+
},
806+
tfValue: tftypes.NewValue(tftypes.List{
807+
ElementType: tftypes.String,
808+
}, []tftypes.Value{
809+
tftypes.NewValue(tftypes.String, "testvalue"),
810+
}),
811+
path: path.Root("test"),
812+
},
813+
}
814+
815+
for name, testCase := range testCases {
816+
name, testCase := name, testCase
817+
818+
t.Run(name, func(t *testing.T) {
819+
t.Parallel()
820+
821+
diags := testCase.listType.Validate(context.Background(), testCase.tfValue, testCase.path)
822+
823+
if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" {
824+
t.Errorf("unexpected diagnostics difference: %s", diff)
825+
}
826+
})
827+
}
828+
}

types/map.go

+51
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import (
77
"strings"
88

99
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
1011
"github.com/hashicorp/terraform-plugin-framework/diag"
1112
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
1214
"github.com/hashicorp/terraform-plugin-go/tftypes"
1315
)
1416

@@ -115,6 +117,55 @@ func (m MapType) String() string {
115117
return "types.MapType[" + m.ElemType.String() + "]"
116118
}
117119

120+
// Validate validates all elements of the map that are of type
121+
// xattr.TypeWithValidate.
122+
func (m MapType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics {
123+
var diags diag.Diagnostics
124+
125+
if in.Type() == nil {
126+
return diags
127+
}
128+
129+
if !in.Type().Is(tftypes.Map{}) {
130+
err := fmt.Errorf("expected Map value, received %T with value: %v", in, in)
131+
diags.AddAttributeError(
132+
path,
133+
"Map Type Validation Error",
134+
"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"+err.Error(),
135+
)
136+
return diags
137+
}
138+
139+
if !in.IsKnown() || in.IsNull() {
140+
return diags
141+
}
142+
143+
var elems map[string]tftypes.Value
144+
145+
if err := in.As(&elems); err != nil {
146+
diags.AddAttributeError(
147+
path,
148+
"Map Type Validation Error",
149+
"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"+err.Error(),
150+
)
151+
return diags
152+
}
153+
154+
validatableType, isValidatable := m.ElemType.(xattr.TypeWithValidate)
155+
if !isValidatable {
156+
return diags
157+
}
158+
159+
for index, elem := range elems {
160+
if !elem.IsFullyKnown() {
161+
continue
162+
}
163+
diags = append(diags, validatableType.Validate(ctx, elem, path.AtMapKey(index))...)
164+
}
165+
166+
return diags
167+
}
168+
118169
// Map represents a map of attr.Values, all of the same type, indicated by
119170
// ElemType. Keys for the map will always be strings.
120171
type Map struct {

types/map_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
"github.com/google/go-cmp/cmp"
99
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
1012
"github.com/hashicorp/terraform-plugin-go/tftypes"
1113
)
1214

@@ -782,3 +784,59 @@ func TestMapString(t *testing.T) {
782784
})
783785
}
784786
}
787+
788+
func TestMapTypeValidate(t *testing.T) {
789+
t.Parallel()
790+
791+
testCases := map[string]struct {
792+
mapType MapType
793+
tfValue tftypes.Value
794+
path path.Path
795+
expectedDiags diag.Diagnostics
796+
}{
797+
"wrong-value-type": {
798+
mapType: MapType{
799+
ElemType: StringType,
800+
},
801+
tfValue: tftypes.NewValue(tftypes.List{
802+
ElementType: tftypes.String,
803+
}, []tftypes.Value{
804+
tftypes.NewValue(tftypes.String, "testvalue"),
805+
}),
806+
path: path.Root("test"),
807+
expectedDiags: diag.Diagnostics{
808+
diag.NewAttributeErrorDiagnostic(
809+
path.Root("test"),
810+
"Map Type Validation Error",
811+
"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"+
812+
"expected Map value, received tftypes.Value with value: tftypes.List[tftypes.String]<tftypes.String<\"testvalue\">>",
813+
),
814+
},
815+
},
816+
"no-validation": {
817+
mapType: MapType{
818+
ElemType: StringType,
819+
},
820+
tfValue: tftypes.NewValue(tftypes.Map{
821+
ElementType: tftypes.String,
822+
}, map[string]tftypes.Value{
823+
"testkey": tftypes.NewValue(tftypes.String, "testvalue"),
824+
}),
825+
path: path.Root("test"),
826+
},
827+
}
828+
829+
for name, testCase := range testCases {
830+
name, testCase := name, testCase
831+
832+
t.Run(name, func(t *testing.T) {
833+
t.Parallel()
834+
835+
diags := testCase.mapType.Validate(context.Background(), testCase.tfValue, testCase.path)
836+
837+
if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" {
838+
t.Errorf("unexpected diagnostics difference: %s", diff)
839+
}
840+
})
841+
}
842+
}

types/set.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,32 @@ func (st SetType) Validate(ctx context.Context, in tftypes.Value, path path.Path
148148
return diags
149149
}
150150

151+
validatableType, isValidatable := st.ElemType.(xattr.TypeWithValidate)
152+
151153
// Attempting to use map[tftypes.Value]struct{} for duplicate detection yields:
152154
// panic: runtime error: hash of unhashable type tftypes.primitive
153155
// Instead, use for loops.
154156
for indexOuter, elemOuter := range elems {
155-
// Only evaluate fully known values for duplicates.
157+
// Only evaluate fully known values for duplicates and validation.
156158
if !elemOuter.IsFullyKnown() {
157159
continue
158160
}
159161

162+
// Validate the element first
163+
if isValidatable {
164+
elemValue, err := st.ElemType.ValueFromTerraform(ctx, elemOuter)
165+
if err != nil {
166+
diags.AddAttributeError(
167+
path,
168+
"Set Type Validation Error",
169+
"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"+err.Error(),
170+
)
171+
return diags
172+
}
173+
diags = append(diags, validatableType.Validate(ctx, elemOuter, path.AtSetValue(elemValue))...)
174+
}
175+
176+
// Then check for duplicates
160177
for indexInner := indexOuter + 1; indexInner < len(elems); indexInner++ {
161178
elemInner := elems[indexInner]
162179

types/set_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,21 @@ func TestSetTypeValidate(t *testing.T) {
540540
),
541541
},
542542
},
543+
"wrong-value-type": {
544+
in: tftypes.NewValue(tftypes.List{
545+
ElementType: tftypes.String,
546+
}, []tftypes.Value{
547+
tftypes.NewValue(tftypes.String, "testvalue"),
548+
}),
549+
expectedDiags: diag.Diagnostics{
550+
diag.NewAttributeErrorDiagnostic(
551+
path.Root("test"),
552+
"Set Type Validation Error",
553+
"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"+
554+
"expected Set value, received tftypes.Value with value: tftypes.List[tftypes.String]<tftypes.String<\"testvalue\">>",
555+
),
556+
},
557+
},
543558
}
544559
for name, testCase := range testCases {
545560
name, testCase := name, testCase

0 commit comments

Comments
 (0)