Skip to content

Commit 4af42b1

Browse files
authored
Adding List element validation for ValuesAre (#37)
* Adding List element validation for ValuesAre (#11) * Renaming test file (#11) * Rename test (#11) * Fix tests (#11) * Updating CHANGELOG (#11) * Adding doc.go for listvalidator pkg (#11) * Adding docs and completing all validations before returning (#11) * Rename .changelog file to match PR number and remove updates to CHANGELOG.md (#11)
1 parent 32de284 commit 4af42b1

File tree

6 files changed

+298
-0
lines changed

6 files changed

+298
-0
lines changed

.changelog/37.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:feature
2+
Introduced `listvalidator` package with `ValuesAre()` validation functions
3+
```

listvalidator/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package listvalidator provides validators for types.List attributes.
2+
package listvalidator

listvalidator/type_validation.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package listvalidator
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/attr"
7+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
// validateList ensures that the request contains a List value.
12+
func validateList(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) ([]attr.Value, bool) {
13+
var l types.List
14+
15+
diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &l)
16+
17+
if diags.HasError() {
18+
response.Diagnostics = append(response.Diagnostics, diags...)
19+
20+
return nil, false
21+
}
22+
23+
if l.Unknown || l.Null {
24+
return nil, false
25+
}
26+
27+
return l.Elems, true
28+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package listvalidator
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/tfsdk"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
)
13+
14+
func TestValidateList(t *testing.T) {
15+
t.Parallel()
16+
17+
testCases := map[string]struct {
18+
request tfsdk.ValidateAttributeRequest
19+
expectedListElems []attr.Value
20+
expectedOk bool
21+
}{
22+
"invalid-type": {
23+
request: tfsdk.ValidateAttributeRequest{
24+
AttributeConfig: types.Bool{Value: true},
25+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
26+
},
27+
expectedListElems: nil,
28+
expectedOk: false,
29+
},
30+
"list-null": {
31+
request: tfsdk.ValidateAttributeRequest{
32+
AttributeConfig: types.List{Null: true},
33+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
34+
},
35+
expectedListElems: nil,
36+
expectedOk: false,
37+
},
38+
"list-unknown": {
39+
request: tfsdk.ValidateAttributeRequest{
40+
AttributeConfig: types.List{Unknown: true},
41+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
42+
},
43+
expectedListElems: nil,
44+
expectedOk: false,
45+
},
46+
"list-value": {
47+
request: tfsdk.ValidateAttributeRequest{
48+
AttributeConfig: types.List{
49+
ElemType: types.StringType,
50+
Elems: []attr.Value{
51+
types.String{Value: "first"},
52+
types.String{Value: "second"},
53+
},
54+
},
55+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
56+
},
57+
expectedListElems: []attr.Value{
58+
types.String{Value: "first"},
59+
types.String{Value: "second"},
60+
},
61+
expectedOk: true,
62+
},
63+
}
64+
65+
for name, testCase := range testCases {
66+
name, testCase := name, testCase
67+
68+
t.Run(name, func(t *testing.T) {
69+
t.Parallel()
70+
71+
gotListElems, gotOk := validateList(context.Background(), testCase.request, &tfsdk.ValidateAttributeResponse{})
72+
73+
if diff := cmp.Diff(gotListElems, testCase.expectedListElems); diff != "" {
74+
t.Errorf("unexpected float64 difference: %s", diff)
75+
}
76+
77+
if diff := cmp.Diff(gotOk, testCase.expectedOk); diff != "" {
78+
t.Errorf("unexpected ok difference: %s", diff)
79+
}
80+
})
81+
}
82+
}

listvalidator/values_are.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package listvalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
9+
)
10+
11+
var _ tfsdk.AttributeValidator = valuesAreValidator{}
12+
13+
// valuesAreValidator validates that each list member validates against each of the value validators.
14+
type valuesAreValidator struct {
15+
valueValidators []tfsdk.AttributeValidator
16+
}
17+
18+
// Description describes the validation in plain text formatting.
19+
func (v valuesAreValidator) Description(ctx context.Context) string {
20+
var descriptions []string
21+
for _, validator := range v.valueValidators {
22+
descriptions = append(descriptions, validator.Description(ctx))
23+
}
24+
25+
return fmt.Sprintf("value must satisfy all validations: %s", strings.Join(descriptions, " + "))
26+
}
27+
28+
// MarkdownDescription describes the validation in Markdown formatting.
29+
func (v valuesAreValidator) MarkdownDescription(ctx context.Context) string {
30+
return v.Description(ctx)
31+
}
32+
33+
// Validate performs the validation.
34+
func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) {
35+
elems, ok := validateList(ctx, req, resp)
36+
if !ok {
37+
return
38+
}
39+
40+
for k, elem := range elems {
41+
request := tfsdk.ValidateAttributeRequest{
42+
AttributePath: req.AttributePath.WithElementKeyInt(k),
43+
AttributeConfig: elem,
44+
Config: req.Config,
45+
}
46+
47+
for _, validator := range v.valueValidators {
48+
validator.Validate(ctx, request, resp)
49+
}
50+
}
51+
}
52+
53+
// ValuesAre returns an AttributeValidator which ensures that any configured
54+
// attribute value:
55+
//
56+
// - Is a List.
57+
// - That contains list elements, each of which validate against each value validator.
58+
//
59+
// Null (unconfigured) and unknown (known after apply) values are skipped.
60+
func ValuesAre(valueValidators ...tfsdk.AttributeValidator) tfsdk.AttributeValidator {
61+
return valuesAreValidator{
62+
valueValidators: valueValidators,
63+
}
64+
}

listvalidator/values_are_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package listvalidator
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
9+
"github.com/hashicorp/terraform-plugin-framework/types"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
12+
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
13+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
14+
)
15+
16+
func TestValuesAreValidator(t *testing.T) {
17+
t.Parallel()
18+
19+
type testCase struct {
20+
val attr.Value
21+
valuesAreValidators []tfsdk.AttributeValidator
22+
expectError bool
23+
}
24+
tests := map[string]testCase{
25+
"not List": {
26+
val: types.Set{
27+
ElemType: types.StringType,
28+
},
29+
expectError: true,
30+
},
31+
"List unknown": {
32+
val: types.List{
33+
Unknown: true,
34+
ElemType: types.StringType,
35+
},
36+
expectError: false,
37+
},
38+
"List null": {
39+
val: types.List{
40+
Null: true,
41+
ElemType: types.StringType,
42+
},
43+
expectError: false,
44+
},
45+
"List elems invalid": {
46+
val: types.List{
47+
ElemType: types.StringType,
48+
Elems: []attr.Value{
49+
types.String{Value: "first"},
50+
types.String{Value: "second"},
51+
},
52+
},
53+
valuesAreValidators: []tfsdk.AttributeValidator{
54+
stringvalidator.LengthAtLeast(6),
55+
},
56+
expectError: true,
57+
},
58+
"List elems invalid for second validator": {
59+
val: types.List{
60+
ElemType: types.StringType,
61+
Elems: []attr.Value{
62+
types.String{Value: "first"},
63+
types.String{Value: "second"},
64+
},
65+
},
66+
valuesAreValidators: []tfsdk.AttributeValidator{
67+
stringvalidator.LengthAtLeast(2),
68+
stringvalidator.LengthAtLeast(6),
69+
},
70+
expectError: true,
71+
},
72+
"List elems wrong type for validator": {
73+
val: types.List{
74+
ElemType: types.StringType,
75+
Elems: []attr.Value{
76+
types.String{Value: "first"},
77+
types.String{Value: "second"},
78+
},
79+
},
80+
valuesAreValidators: []tfsdk.AttributeValidator{
81+
int64validator.AtLeast(6),
82+
},
83+
expectError: true,
84+
},
85+
"List elems valid": {
86+
val: types.List{
87+
ElemType: types.StringType,
88+
Elems: []attr.Value{
89+
types.String{Value: "first"},
90+
types.String{Value: "second"},
91+
},
92+
},
93+
valuesAreValidators: []tfsdk.AttributeValidator{
94+
stringvalidator.LengthAtLeast(5),
95+
},
96+
expectError: false,
97+
},
98+
}
99+
100+
for name, test := range tests {
101+
name, test := name, test
102+
t.Run(name, func(t *testing.T) {
103+
request := tfsdk.ValidateAttributeRequest{
104+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
105+
AttributeConfig: test.val,
106+
}
107+
response := tfsdk.ValidateAttributeResponse{}
108+
ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response)
109+
110+
if !response.Diagnostics.HasError() && test.expectError {
111+
t.Fatal("expected error, got no error")
112+
}
113+
114+
if response.Diagnostics.HasError() && !test.expectError {
115+
t.Fatalf("got unexpected error: %s", response.Diagnostics)
116+
}
117+
})
118+
}
119+
}

0 commit comments

Comments
 (0)