Skip to content

Commit ac40997

Browse files
ewbankkitappilon
authored andcommitted
Add various Map key and value validators.
1 parent bc7b1c9 commit ac40997

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed

helper/validation/map.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package validation
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
)
9+
10+
// MapKeyLenBetween returns a SchemaValidateFunc which tests if the provided value
11+
// is of type map and the length of all keys are between min and max (inclusive)
12+
func MapKeyLenBetween(min, max int) schema.SchemaValidateFunc {
13+
return func(i interface{}, k string) (warnings []string, errors []error) {
14+
v, ok := i.(map[string]interface{})
15+
if !ok {
16+
errors = append(errors, fmt.Errorf("expected type of %[1]q to be Map, got %[1]T", k))
17+
return warnings, errors
18+
}
19+
20+
for key := range v {
21+
len := len(key)
22+
if len < min || len > max {
23+
errors = append(errors, fmt.Errorf("expected the length of all keys of %q to be in the range (%d - %d), got %q (length = %d)", k, min, max, key, len))
24+
return warnings, errors
25+
}
26+
}
27+
28+
return warnings, errors
29+
}
30+
}
31+
32+
// MapValueLenBetween returns a SchemaValidateFunc which tests if the provided value
33+
// is of type map and the length of all values are between min and max (inclusive)
34+
func MapValueLenBetween(min, max int) schema.SchemaValidateFunc {
35+
return func(i interface{}, k string) (warnings []string, errors []error) {
36+
v, ok := i.(map[string]interface{})
37+
if !ok {
38+
errors = append(errors, fmt.Errorf("expected type of %[1]q to be Map, got %[1]T", k))
39+
return warnings, errors
40+
}
41+
42+
for _, val := range v {
43+
if _, ok := val.(string); !ok {
44+
errors = append(errors, fmt.Errorf("expected all values of %[1]q to be strings, found %[2]v (type = %[2]T)", k, val))
45+
return warnings, errors
46+
}
47+
}
48+
49+
for _, val := range v {
50+
len := len(val.(string))
51+
if len < min || len > max {
52+
errors = append(errors, fmt.Errorf("expected the length of all values of %q to be in the range (%d - %d), got %q (length = %d)", k, min, max, val, len))
53+
return warnings, errors
54+
}
55+
}
56+
57+
return warnings, errors
58+
}
59+
}
60+
61+
// MapKeyMatch returns a SchemaValidateFunc which tests if the provided value
62+
// is of type map and all keys match a given regexp. Optionally an error message
63+
// can be provided to return something friendlier than "expected to match some globby regexp".
64+
func MapKeyMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc {
65+
return func(i interface{}, k string) (warnings []string, errors []error) {
66+
v, ok := i.(map[string]interface{})
67+
if !ok {
68+
errors = append(errors, fmt.Errorf("expected type of %[1]q to be Map, got %[1]T", k))
69+
return warnings, errors
70+
}
71+
72+
for key := range v {
73+
if ok := r.MatchString(key); !ok {
74+
if message != "" {
75+
errors = append(errors, fmt.Errorf("invalid key %q for %q (%s)", key, k, message))
76+
return warnings, errors
77+
}
78+
79+
errors = append(errors, fmt.Errorf("invalid key %q for %q (expected to match regular expression %q)", key, k, r))
80+
return warnings, errors
81+
}
82+
}
83+
84+
return warnings, errors
85+
}
86+
}
87+
88+
// MapValueMatch returns a SchemaValidateFunc which tests if the provided value
89+
// is of type map and all values match a given regexp. Optionally an error message
90+
// can be provided to return something friendlier than "expected to match some globby regexp".
91+
func MapValueMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc {
92+
return func(i interface{}, k string) (warnings []string, errors []error) {
93+
v, ok := i.(map[string]interface{})
94+
if !ok {
95+
errors = append(errors, fmt.Errorf("expected type of %[1]q to be Map, got %[1]T", k))
96+
return warnings, errors
97+
}
98+
99+
for _, val := range v {
100+
if _, ok := val.(string); !ok {
101+
errors = append(errors, fmt.Errorf("expected all values of %[1]q to be strings, found %[2]v (type = %[2]T)", k, val))
102+
return warnings, errors
103+
}
104+
}
105+
106+
for _, val := range v {
107+
if ok := r.MatchString(val.(string)); !ok {
108+
if message != "" {
109+
errors = append(errors, fmt.Errorf("invalid value %q for %q (%s)", val, k, message))
110+
return warnings, errors
111+
}
112+
113+
errors = append(errors, fmt.Errorf("invalid value %q for %q (expected to match regular expression %q)", val, k, r))
114+
return warnings, errors
115+
}
116+
}
117+
118+
return warnings, errors
119+
}
120+
}

helper/validation/map_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package validation
2+
3+
import (
4+
"regexp"
5+
"testing"
6+
)
7+
8+
func TestValidationMapKeyLenBetween(t *testing.T) {
9+
cases := map[string]struct {
10+
Value interface{}
11+
Error bool
12+
}{
13+
"NotMap": {
14+
Value: "the map is a lie",
15+
Error: true,
16+
},
17+
"TooLong": {
18+
Value: map[string]interface{}{
19+
"ABC": "123",
20+
"UVWXYZ": "123456",
21+
},
22+
Error: true,
23+
},
24+
"TooShort": {
25+
Value: map[string]interface{}{
26+
"ABC": "123",
27+
"U": "1",
28+
},
29+
Error: true,
30+
},
31+
"AllGood": {
32+
Value: map[string]interface{}{
33+
"AB": "12",
34+
"UVWXY": "12345",
35+
},
36+
Error: false,
37+
},
38+
}
39+
40+
fn := MapKeyLenBetween(2, 5)
41+
42+
for tn, tc := range cases {
43+
t.Run(tn, func(t *testing.T) {
44+
_, errors := fn(tc.Value, tn)
45+
46+
if len(errors) > 0 && !tc.Error {
47+
t.Errorf("MapKeyLenBetween(%s) produced an unexpected error", tc.Value)
48+
} else if len(errors) == 0 && tc.Error {
49+
t.Errorf("MapKeyLenBetween(%s) did not error", tc.Value)
50+
}
51+
})
52+
}
53+
}
54+
55+
func TestValidationMapValueLenBetween(t *testing.T) {
56+
cases := map[string]struct {
57+
Value interface{}
58+
Error bool
59+
}{
60+
"NotMap": {
61+
Value: "the map is a lie",
62+
Error: true,
63+
},
64+
"NotStringValue": {
65+
Value: map[string]interface{}{
66+
"ABC": "123",
67+
"UVWXYZ": 123456,
68+
},
69+
Error: true,
70+
},
71+
"TooLong": {
72+
Value: map[string]interface{}{
73+
"ABC": "123",
74+
"UVWXYZ": "123456",
75+
},
76+
Error: true,
77+
},
78+
"TooShort": {
79+
Value: map[string]interface{}{
80+
"ABC": "123",
81+
"U": "1",
82+
},
83+
Error: true,
84+
},
85+
"AllGood": {
86+
Value: map[string]interface{}{
87+
"AB": "12",
88+
"UVWXY": "12345",
89+
},
90+
Error: false,
91+
},
92+
}
93+
94+
fn := MapValueLenBetween(2, 5)
95+
96+
for tn, tc := range cases {
97+
t.Run(tn, func(t *testing.T) {
98+
_, errors := fn(tc.Value, tn)
99+
100+
if len(errors) > 0 && !tc.Error {
101+
t.Errorf("MapValueLenBetween(%s) produced an unexpected error", tc.Value)
102+
} else if len(errors) == 0 && tc.Error {
103+
t.Errorf("MapValueLenBetween(%s) did not error", tc.Value)
104+
}
105+
})
106+
}
107+
}
108+
109+
func TestValidationMapKeyMatch(t *testing.T) {
110+
cases := map[string]struct {
111+
Value interface{}
112+
Error bool
113+
}{
114+
"NotMap": {
115+
Value: "the map is a lie",
116+
Error: true,
117+
},
118+
"NoMatch": {
119+
Value: map[string]interface{}{
120+
"ABC": "123",
121+
"UVWXYZ": "123456",
122+
},
123+
Error: true,
124+
},
125+
"AllGood": {
126+
Value: map[string]interface{}{
127+
"AB": "12",
128+
"UVABY": "12345",
129+
},
130+
Error: false,
131+
},
132+
}
133+
134+
fn := MapKeyMatch(regexp.MustCompile(".*AB.*"), "")
135+
136+
for tn, tc := range cases {
137+
t.Run(tn, func(t *testing.T) {
138+
_, errors := fn(tc.Value, tn)
139+
140+
if len(errors) > 0 && !tc.Error {
141+
t.Errorf("MapKeyMatch(%s) produced an unexpected error", tc.Value)
142+
} else if len(errors) == 0 && tc.Error {
143+
t.Errorf("MapKeyMatch(%s) did not error", tc.Value)
144+
}
145+
})
146+
}
147+
}
148+
149+
func TestValidationValueKeyMatch(t *testing.T) {
150+
cases := map[string]struct {
151+
Value interface{}
152+
Error bool
153+
}{
154+
"NotMap": {
155+
Value: "the map is a lie",
156+
Error: true,
157+
},
158+
"NotStringValue": {
159+
Value: map[string]interface{}{
160+
"MNO": "123",
161+
"UVWXYZ": 123456,
162+
},
163+
Error: true,
164+
},
165+
"NoMatch": {
166+
Value: map[string]interface{}{
167+
"MNO": "ABC",
168+
"UVWXYZ": "UVWXYZ",
169+
},
170+
Error: true,
171+
},
172+
"AllGood": {
173+
Value: map[string]interface{}{
174+
"MNO": "ABC",
175+
"UVWXYZ": "UVABY",
176+
},
177+
Error: false,
178+
},
179+
}
180+
181+
fn := MapValueMatch(regexp.MustCompile(".*AB.*"), "")
182+
183+
for tn, tc := range cases {
184+
t.Run(tn, func(t *testing.T) {
185+
_, errors := fn(tc.Value, tn)
186+
187+
if len(errors) > 0 && !tc.Error {
188+
t.Errorf("MapValueMatch(%s) produced an unexpected error", tc.Value)
189+
} else if len(errors) == 0 && tc.Error {
190+
t.Errorf("MapValueMatch(%s) did not error", tc.Value)
191+
}
192+
})
193+
}
194+
}

0 commit comments

Comments
 (0)