Skip to content

Commit 1a3ea19

Browse files
authored
Adding Map validation for KeysAre and ValuesAre (#38)
* Adding Map validation for KeysAre and ValuesAre (#13) * Updates following code review (#13) * Update CHANGELOG.md (#13) * Rename .changelog file to match PR number and remove updates to CHANGELOG.md (#13) * Updating docs to clarify validation of Map keys (#13)
1 parent fc8a1e4 commit 1a3ea19

File tree

8 files changed

+475
-0
lines changed

8 files changed

+475
-0
lines changed

.changelog/38.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 `mapvalidator` package with `ValuesAre()` validation function
3+
```

mapvalidator/doc.go

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

mapvalidator/keys_are.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package mapvalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
9+
"github.com/hashicorp/terraform-plugin-framework/types"
10+
)
11+
12+
var _ tfsdk.AttributeValidator = keysAreValidator{}
13+
14+
// keysAreValidator validates that each map key validates against each of the value validators.
15+
type keysAreValidator struct {
16+
keyValidators []tfsdk.AttributeValidator
17+
}
18+
19+
// Description describes the validation in plain text formatting.
20+
func (v keysAreValidator) Description(ctx context.Context) string {
21+
var descriptions []string
22+
for _, validator := range v.keyValidators {
23+
descriptions = append(descriptions, validator.Description(ctx))
24+
}
25+
26+
return fmt.Sprintf("key must satisfy all validations: %s", strings.Join(descriptions, " + "))
27+
}
28+
29+
// MarkdownDescription describes the validation in Markdown formatting.
30+
func (v keysAreValidator) MarkdownDescription(ctx context.Context) string {
31+
return v.Description(ctx)
32+
}
33+
34+
// Validate performs the validation.
35+
// Note that the AttributePath specified in the ValidateAttributeRequest refers to the value in the Map with key `k`,
36+
// whereas the AttributeConfig refers to the key itself (i.e., `k`). This is intentional as the validation being
37+
// performed is for the keys of the Map.
38+
func (v keysAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) {
39+
elems, ok := validateMap(ctx, req, resp)
40+
if !ok {
41+
return
42+
}
43+
44+
for k := range elems {
45+
request := tfsdk.ValidateAttributeRequest{
46+
AttributePath: req.AttributePath.WithElementKeyString(k),
47+
AttributeConfig: types.String{Value: k},
48+
Config: req.Config,
49+
}
50+
51+
for _, validator := range v.keyValidators {
52+
validator.Validate(ctx, request, resp)
53+
if resp.Diagnostics.HasError() {
54+
return
55+
}
56+
}
57+
}
58+
}
59+
60+
func KeysAre(keyValidators ...tfsdk.AttributeValidator) tfsdk.AttributeValidator {
61+
return keysAreValidator{
62+
keyValidators: keyValidators,
63+
}
64+
}

mapvalidator/keys_are_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package mapvalidator
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 TestKeysAreValidator(t *testing.T) {
17+
t.Parallel()
18+
19+
type testCase struct {
20+
val attr.Value
21+
keysAreValidators []tfsdk.AttributeValidator
22+
expectError bool
23+
}
24+
tests := map[string]testCase{
25+
"not Map": {
26+
val: types.List{
27+
ElemType: types.StringType,
28+
},
29+
expectError: true,
30+
},
31+
"Map unknown": {
32+
val: types.Map{
33+
Unknown: true,
34+
ElemType: types.StringType,
35+
},
36+
expectError: false,
37+
},
38+
"Map null": {
39+
val: types.Map{
40+
Null: true,
41+
ElemType: types.StringType,
42+
},
43+
expectError: false,
44+
},
45+
"Map key invalid": {
46+
val: types.Map{
47+
ElemType: types.StringType,
48+
Elems: map[string]attr.Value{
49+
"one": types.String{Value: "first"},
50+
"two": types.String{Value: "second"},
51+
},
52+
},
53+
keysAreValidators: []tfsdk.AttributeValidator{
54+
stringvalidator.LengthAtLeast(4),
55+
},
56+
expectError: true,
57+
},
58+
"Map key invalid for second validator": {
59+
val: types.Map{
60+
ElemType: types.StringType,
61+
Elems: map[string]attr.Value{
62+
"one": types.String{Value: "first"},
63+
"two": types.String{Value: "second"},
64+
},
65+
},
66+
keysAreValidators: []tfsdk.AttributeValidator{
67+
stringvalidator.LengthAtLeast(2),
68+
stringvalidator.LengthAtLeast(6),
69+
},
70+
expectError: true,
71+
},
72+
"Map keys wrong type for validator": {
73+
val: types.Map{
74+
ElemType: types.StringType,
75+
Elems: map[string]attr.Value{
76+
"one": types.String{Value: "first"},
77+
"two": types.String{Value: "second"},
78+
},
79+
},
80+
keysAreValidators: []tfsdk.AttributeValidator{
81+
int64validator.AtLeast(6),
82+
},
83+
expectError: true,
84+
},
85+
"Map keys valid": {
86+
val: types.Map{
87+
ElemType: types.StringType,
88+
Elems: map[string]attr.Value{
89+
"one": types.String{Value: "first"},
90+
"two": types.String{Value: "second"},
91+
},
92+
},
93+
keysAreValidators: []tfsdk.AttributeValidator{
94+
stringvalidator.LengthAtLeast(3),
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+
KeysAre(test.keysAreValidators...).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+
}

mapvalidator/type_validation.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package mapvalidator
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+
// validateMap ensures that the request contains a Map value.
12+
func validateMap(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) (map[string]attr.Value, bool) {
13+
var m types.Map
14+
15+
diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &m)
16+
17+
if diags.HasError() {
18+
response.Diagnostics = append(response.Diagnostics, diags...)
19+
20+
return nil, false
21+
}
22+
23+
if m.Unknown || m.Null {
24+
return nil, false
25+
}
26+
27+
return m.Elems, true
28+
}

mapvalidator/type_validation_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package mapvalidator
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 TestValidateMap(t *testing.T) {
15+
t.Parallel()
16+
17+
testCases := map[string]struct {
18+
request tfsdk.ValidateAttributeRequest
19+
expectedMap map[string]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+
expectedMap: nil,
28+
expectedOk: false,
29+
},
30+
"map-null": {
31+
request: tfsdk.ValidateAttributeRequest{
32+
AttributeConfig: types.Map{Null: true},
33+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
34+
},
35+
expectedMap: nil,
36+
expectedOk: false,
37+
},
38+
"map-unknown": {
39+
request: tfsdk.ValidateAttributeRequest{
40+
AttributeConfig: types.Map{Unknown: true},
41+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
42+
},
43+
expectedMap: nil,
44+
expectedOk: false,
45+
},
46+
"map-value": {
47+
request: tfsdk.ValidateAttributeRequest{
48+
AttributeConfig: types.Map{
49+
ElemType: types.StringType,
50+
Elems: map[string]attr.Value{
51+
"one": types.String{Value: "first"},
52+
"two": types.String{Value: "second"},
53+
},
54+
},
55+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
56+
},
57+
expectedMap: map[string]attr.Value{
58+
"one": types.String{Value: "first"},
59+
"two": 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+
gotMapElems, gotOk := validateMap(context.Background(), testCase.request, &tfsdk.ValidateAttributeResponse{})
72+
73+
if diff := cmp.Diff(gotMapElems, testCase.expectedMap); diff != "" {
74+
t.Errorf("unexpected map 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+
}

mapvalidator/values_are.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package mapvalidator
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 map value 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 := validateMap(ctx, req, resp)
36+
if !ok {
37+
return
38+
}
39+
40+
for k, elem := range elems {
41+
request := tfsdk.ValidateAttributeRequest{
42+
AttributePath: req.AttributePath.WithElementKeyString(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 Map.
57+
// - Contains Map 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+
}

0 commit comments

Comments
 (0)