Skip to content
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

Merged
merged 6 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions helper/validation/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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{})) {
Copy link
Contributor

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.

len := len(key)
if len < min || len > max {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Copy link
Contributor

@appilon appilon Apr 21, 2020

Choose a reason for hiding this comment

The 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 var diags diag.Diagnostics at the top and append to the slice, returning diags at the end. You could exit at the first error or actually accumulate all of them to return. Of course in situations where you cannot continue you should exit early, but in situations such as this were we can actually successfully collect multiple errors you have the ability to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Diagnostic for every key (in this function) that is in error.
This will complicate the Equals helper or expanded equivalent as for example map iteration is non-deterministic and the Diagnostics entries will potentially be in different orders.
OK, I think I've just talked myself into sorting the keys and then iterating the map in a known order and accumulating all errors - The user's experience will be better in this case.

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 SchemaValidateDiagFunc which tests if the provided value
// 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.SchemaValidateDiagFunc {
return func(v interface{}, path cty.Path) diag.Diagnostics {
var diags diag.Diagnostics

for _, key := range sortedKeys(v.(map[string]interface{})) {
if ok := r.MatchString(key); !ok {
var detail string
if message == "" {
detail = fmt.Sprintf("Map key expected to match regular expression %q: %s", r, key)
} else {
detail = fmt.Sprintf("%s: %s", message, key)
}

diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Invalid map key",
Detail: detail,
AttributePath: append(path, cty.IndexStep{Key: cty.StringVal(key)}),
})
}
}

return diags
}
}

// MapValueMatch returns a SchemaValidateDiagFunc 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.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
}

if ok := r.MatchString(val.(string)); !ok {
var detail string
if message == "" {
detail = fmt.Sprintf("Map value expected to match regular expression %q: %s => %v", r, key, val)
} else {
detail = fmt.Sprintf("%s: %s => %v", message, key, val)
}

diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Invalid map value",
Detail: detail,
AttributePath: append(path, cty.IndexStep{Key: cty.StringVal(key)}),
})
}
}

return diags
}
}

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
}
Loading