-
Notifications
You must be signed in to change notification settings - Fork 233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add various Map key and value validators #304
Changes from 5 commits
6606a63
68bdf51
40e95fc
f7d2432
24d31ca
f43dac4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package validation | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"sort" | ||
|
||
"github.com/hashicorp/go-cty/cty" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
) | ||
|
||
// MapKeyLenBetween returns a SchemaValidateDiagFunc which tests if the provided value | ||
// is of type map and the length of all keys are between min and max (inclusive) | ||
func MapKeyLenBetween(min, max int) schema.SchemaValidateDiagFunc { | ||
return func(v interface{}, path cty.Path) diag.Diagnostics { | ||
var diags diag.Diagnostics | ||
|
||
for _, key := range sortedKeys(v.(map[string]interface{})) { | ||
len := len(key) | ||
if len < min || len > max { | ||
diags = append(diags, diag.Diagnostic{ | ||
Severity: diag.Error, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned in my question answering it might be a better pattern to declare There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now I'm exiting at the first error but it would make sense to accumulate a |
||
Summary: "Bad map key length", | ||
Detail: fmt.Sprintf("Map key lengths should be in the range (%d - %d): %s (length = %d)", min, max, key, len), | ||
AttributePath: append(path, cty.IndexStep{Key: cty.StringVal(key)}), | ||
}) | ||
} | ||
} | ||
|
||
return diags | ||
} | ||
} | ||
|
||
// MapValueLenBetween returns a SchemaValidateDiagFunc which tests if the provided value | ||
// is of type map and the length of all values are between min and max (inclusive) | ||
func MapValueLenBetween(min, max int) schema.SchemaValidateDiagFunc { | ||
return func(v interface{}, path cty.Path) diag.Diagnostics { | ||
var diags diag.Diagnostics | ||
|
||
m := v.(map[string]interface{}) | ||
|
||
for _, key := range sortedKeys(m) { | ||
val := m[key] | ||
|
||
if _, ok := val.(string); !ok { | ||
diags = append(diags, diag.Diagnostic{ | ||
Severity: diag.Error, | ||
Summary: "Bad map value type", | ||
Detail: fmt.Sprintf("Map values should be strings: %s => %v (type = %T)", key, val, val), | ||
AttributePath: append(path, cty.IndexStep{Key: cty.StringVal(key)}), | ||
}) | ||
continue | ||
} | ||
|
||
len := len(val.(string)) | ||
if len < min || len > max { | ||
diags = append(diags, diag.Diagnostic{ | ||
Severity: diag.Error, | ||
Summary: "Bad map value length", | ||
Detail: fmt.Sprintf("Map value lengths should be in the range (%d - %d): %s => %v (length = %d)", min, max, key, val, len), | ||
AttributePath: append(path, cty.IndexStep{Key: cty.StringVal(key)}), | ||
}) | ||
} | ||
} | ||
|
||
return diags | ||
} | ||
} | ||
|
||
// MapKeyMatch returns a SchemaValidateFunc which tests if the provided value | ||
appilon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// is of type map and all keys match a given regexp. Optionally an error message | ||
// can be provided to return something friendlier than "expected to match some globby regexp". | ||
func MapKeyMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { | ||
return func(i interface{}, k string) (warnings []string, errors []error) { | ||
v, ok := i.(map[string]interface{}) | ||
if !ok { | ||
errors = append(errors, fmt.Errorf("expected type of %[1]q to be Map, got %[1]T", k)) | ||
return warnings, errors | ||
} | ||
|
||
for key := range v { | ||
if ok := r.MatchString(key); !ok { | ||
if message != "" { | ||
errors = append(errors, fmt.Errorf("invalid key %q for %q (%s)", key, k, message)) | ||
return warnings, errors | ||
} | ||
|
||
errors = append(errors, fmt.Errorf("invalid key %q for %q (expected to match regular expression %q)", key, k, r)) | ||
return warnings, errors | ||
} | ||
} | ||
|
||
return warnings, errors | ||
} | ||
} | ||
|
||
// MapValueMatch returns a SchemaValidateFunc which tests if the provided value | ||
// is of type map and all values match a given regexp. Optionally an error message | ||
// can be provided to return something friendlier than "expected to match some globby regexp". | ||
func MapValueMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { | ||
return func(i interface{}, k string) (warnings []string, errors []error) { | ||
v, ok := i.(map[string]interface{}) | ||
if !ok { | ||
errors = append(errors, fmt.Errorf("expected type of %[1]q to be Map, got %[1]T", k)) | ||
return warnings, errors | ||
} | ||
|
||
for _, val := range v { | ||
if _, ok := val.(string); !ok { | ||
errors = append(errors, fmt.Errorf("expected all values of %[1]q to be strings, found %[2]v (type = %[2]T)", k, val)) | ||
return warnings, errors | ||
} | ||
} | ||
|
||
for _, val := range v { | ||
if ok := r.MatchString(val.(string)); !ok { | ||
if message != "" { | ||
errors = append(errors, fmt.Errorf("invalid value %q for %q (%s)", val, k, message)) | ||
return warnings, errors | ||
} | ||
|
||
errors = append(errors, fmt.Errorf("invalid value %q for %q (expected to match regular expression %q)", val, k, r)) | ||
return warnings, errors | ||
} | ||
} | ||
|
||
return warnings, errors | ||
} | ||
} | ||
|
||
func sortedKeys(m map[string]interface{}) []string { | ||
keys := make([]string, len(m)) | ||
|
||
i := 0 | ||
for key := range m { | ||
keys[i] = key | ||
i++ | ||
} | ||
|
||
sort.Strings(keys) | ||
|
||
return keys | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
package validation | ||
|
||
import ( | ||
"regexp" | ||
"testing" | ||
|
||
"github.com/hashicorp/go-cty/cty" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
) | ||
|
||
func TestValidationMapKeyLenBetween(t *testing.T) { | ||
cases := map[string]struct { | ||
Value interface{} | ||
ExpectedDiags diag.Diagnostics | ||
}{ | ||
"TooLong": { | ||
Value: map[string]interface{}{ | ||
"ABC": "123", | ||
"UVWXYZ": "123456", | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("UVWXYZ")}), | ||
}, | ||
}, | ||
}, | ||
"TooShort": { | ||
Value: map[string]interface{}{ | ||
"ABC": "123", | ||
"U": "1", | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("U")}), | ||
}, | ||
}, | ||
}, | ||
"TooLongAndTooShort": { | ||
Value: map[string]interface{}{ | ||
"UVWXYZ": "123456", | ||
"ABC": "123", | ||
"U": "1", | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("U")}), | ||
}, | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("UVWXYZ")}), | ||
}, | ||
}, | ||
}, | ||
"AllGood": { | ||
Value: map[string]interface{}{ | ||
"AB": "12", | ||
"UVWXY": "12345", | ||
}, | ||
ExpectedDiags: nil, | ||
}, | ||
} | ||
|
||
fn := MapKeyLenBetween(2, 5) | ||
|
||
for tn, tc := range cases { | ||
t.Run(tn, func(t *testing.T) { | ||
diags := fn(tc.Value, cty.Path{}) | ||
|
||
checkDiagnostics(t, tn, diags, tc.ExpectedDiags) | ||
}) | ||
} | ||
} | ||
|
||
func TestValidationMapValueLenBetween(t *testing.T) { | ||
cases := map[string]struct { | ||
Value interface{} | ||
ExpectedDiags diag.Diagnostics | ||
}{ | ||
"NotStringValue": { | ||
Value: map[string]interface{}{ | ||
"ABC": "123", | ||
"UVWXYZ": 123456, | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("UVWXYZ")}), | ||
}, | ||
}, | ||
}, | ||
"TooLong": { | ||
Value: map[string]interface{}{ | ||
"ABC": "123", | ||
"UVWXYZ": "123456", | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("UVWXYZ")}), | ||
}, | ||
}, | ||
}, | ||
"TooShort": { | ||
Value: map[string]interface{}{ | ||
"ABC": "123", | ||
"U": "1", | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("U")}), | ||
}, | ||
}, | ||
}, | ||
"TooLongAndTooShort": { | ||
Value: map[string]interface{}{ | ||
"UVWXYZ": "123456", | ||
"ABC": "123", | ||
"U": "1", | ||
}, | ||
ExpectedDiags: diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("U")}), | ||
}, | ||
{ | ||
Severity: diag.Error, | ||
AttributePath: append(cty.Path{}, cty.IndexStep{Key: cty.StringVal("UVWXYZ")}), | ||
}, | ||
}, | ||
}, | ||
"AllGood": { | ||
Value: map[string]interface{}{ | ||
"AB": "12", | ||
"UVWXY": "12345", | ||
}, | ||
ExpectedDiags: nil, | ||
}, | ||
} | ||
|
||
fn := MapValueLenBetween(2, 5) | ||
|
||
for tn, tc := range cases { | ||
t.Run(tn, func(t *testing.T) { | ||
diags := fn(tc.Value, cty.Path{}) | ||
|
||
checkDiagnostics(t, tn, diags, tc.ExpectedDiags) | ||
}) | ||
} | ||
} | ||
|
||
func TestValidationMapKeyMatch(t *testing.T) { | ||
cases := map[string]struct { | ||
Value interface{} | ||
Error bool | ||
}{ | ||
"NotMap": { | ||
Value: "the map is a lie", | ||
Error: true, | ||
}, | ||
"NoMatch": { | ||
Value: map[string]interface{}{ | ||
"ABC": "123", | ||
"UVWXYZ": "123456", | ||
}, | ||
Error: true, | ||
}, | ||
"AllGood": { | ||
Value: map[string]interface{}{ | ||
"AB": "12", | ||
"UVABY": "12345", | ||
}, | ||
Error: false, | ||
}, | ||
} | ||
|
||
fn := MapKeyMatch(regexp.MustCompile(".*AB.*"), "") | ||
|
||
for tn, tc := range cases { | ||
t.Run(tn, func(t *testing.T) { | ||
_, errors := fn(tc.Value, tn) | ||
|
||
if len(errors) > 0 && !tc.Error { | ||
t.Errorf("MapKeyMatch(%s) produced an unexpected error", tc.Value) | ||
} else if len(errors) == 0 && tc.Error { | ||
t.Errorf("MapKeyMatch(%s) did not error", tc.Value) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestValidationValueKeyMatch(t *testing.T) { | ||
cases := map[string]struct { | ||
Value interface{} | ||
Error bool | ||
}{ | ||
"NotMap": { | ||
Value: "the map is a lie", | ||
Error: true, | ||
}, | ||
"NotStringValue": { | ||
Value: map[string]interface{}{ | ||
"MNO": "123", | ||
"UVWXYZ": 123456, | ||
}, | ||
Error: true, | ||
}, | ||
"NoMatch": { | ||
Value: map[string]interface{}{ | ||
"MNO": "ABC", | ||
"UVWXYZ": "UVWXYZ", | ||
}, | ||
Error: true, | ||
}, | ||
"AllGood": { | ||
Value: map[string]interface{}{ | ||
"MNO": "ABC", | ||
"UVWXYZ": "UVABY", | ||
}, | ||
Error: false, | ||
}, | ||
} | ||
|
||
fn := MapValueMatch(regexp.MustCompile(".*AB.*"), "") | ||
|
||
for tn, tc := range cases { | ||
t.Run(tn, func(t *testing.T) { | ||
_, errors := fn(tc.Value, tn) | ||
|
||
if len(errors) > 0 && !tc.Error { | ||
t.Errorf("MapValueMatch(%s) produced an unexpected error", tc.Value) | ||
} else if len(errors) == 0 && tc.Error { | ||
t.Errorf("MapValueMatch(%s) did not error", tc.Value) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func checkDiagnostics(t *testing.T, tn string, got, expected diag.Diagnostics) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added this for now. As you mention, maybe replace with a method on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is good enough for now |
||
if len(got) != len(expected) { | ||
t.Fatalf("%s: wrong number of diags, expected %d, got %d", tn, len(expected), len(got)) | ||
} | ||
for j := range got { | ||
if got[j].Severity != expected[j].Severity { | ||
t.Fatalf("%s: expected severity %v, got %v", tn, expected[j].Severity, got[j].Severity) | ||
} | ||
if !got[j].AttributePath.Equals(expected[j].AttributePath) { | ||
t.Fatalf("%s: attribute paths do not match expected: %v, got %v", tn, expected[j].AttributePath, got[j].AttributePath) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could sort here or just sort in the check/test instead. Having a deterministic user experience for the diagnostics might be nice though so sorting here is certainly appropriate.