Skip to content

String RegexMatches validator #23

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

Merged
merged 4 commits into from
May 25, 2022
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
3 changes: 3 additions & 0 deletions .changelog/23.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Introduced `stringvalidator.RegexMatches()` validation function
```
64 changes: 64 additions & 0 deletions stringvalidator/regex_matches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package stringvalidator

import (
"context"
"fmt"
"regexp"

"github.com/hashicorp/terraform-plugin-framework-validators/validatordiag"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)

var _ tfsdk.AttributeValidator = regexMatchesValidator{}

// regexMatchesValidator validates that a string Attribute's value matches the specified regular expression.
type regexMatchesValidator struct {
regexp *regexp.Regexp
message string
}

// Description describes the validation in plain text formatting.
func (validator regexMatchesValidator) Description(_ context.Context) string {
if validator.message != "" {
return validator.message
}
return fmt.Sprintf("value must match regular expression '%s'", validator.regexp)
}

// MarkdownDescription describes the validation in Markdown formatting.
func (validator regexMatchesValidator) MarkdownDescription(ctx context.Context) string {
return validator.Description(ctx)
}

// Validate performs the validation.
func (validator regexMatchesValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) {
s, ok := validateString(ctx, request, response)

if !ok {
return
}

if ok := validator.regexp.MatchString(s); !ok {
response.Diagnostics.Append(validatordiag.AttributeValueMatchesDiagnostic(
request.AttributePath,
validator.Description(ctx),
s,
))
}
}

// RegexMatches returns an AttributeValidator which ensures that any configured
// attribute value:
//
// - Is a string.
// - Matches the given regular expression https://github.com/google/re2/wiki/Syntax.
//
// Null (unconfigured) and unknown (known after apply) values are skipped.
// Optionally an error message can be provided to return something friendlier
// than "value must match regular expression 'regexp'".
func RegexMatches(regexp *regexp.Regexp, message string) tfsdk.AttributeValidator {
return regexMatchesValidator{
regexp: regexp,
message: message,
}
}
65 changes: 65 additions & 0 deletions stringvalidator/regex_matches_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package stringvalidator

import (
"context"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestRegexMatchesValidator(t *testing.T) {
t.Parallel()

type testCase struct {
val attr.Value
regexp *regexp.Regexp
expectError bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but it'd be great to switch at least this unit test to checking contents to ensure a given message flows to the returned error diagnostic.

}
tests := map[string]testCase{
"not a String": {
val: types.Bool{Value: true},
expectError: true,
},
"unknown String": {
val: types.String{Unknown: true},
regexp: regexp.MustCompile(`^o[j-l]?$`),
},
"null String": {
val: types.String{Null: true},
regexp: regexp.MustCompile(`^o[j-l]?$`),
},
"valid String": {
val: types.String{Value: "ok"},
regexp: regexp.MustCompile(`^o[j-l]?$`),
},
"invalid String": {
val: types.String{Value: "not ok"},
regexp: regexp.MustCompile(`^o[j-l]?$`),
expectError: true,
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but I just realized that we can add t.Parallel() here for the sub tests 👍

request := tfsdk.ValidateAttributeRequest{
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
AttributeConfig: test.val,
}
response := tfsdk.ValidateAttributeResponse{}
RegexMatches(test.regexp, "").Validate(context.TODO(), request, &response)

if !response.Diagnostics.HasError() && test.expectError {
t.Fatal("expected error, got no error")
}

if response.Diagnostics.HasError() && !test.expectError {
t.Fatalf("got unexpected error: %s", response.Diagnostics)
}
})
}
}
9 changes: 9 additions & 0 deletions validatordiag/diag.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ func AttributeValueLengthDiagnostic(path *tftypes.AttributePath, description str
)
}

// AttributeValueMatchesDiagnostic returns an error Diagnostic to be used when an attribute's value has an invalid match.
func AttributeValueMatchesDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic {
return diag.NewAttributeErrorDiagnostic(
path,
"Invalid Attribute Value Match",
capitalize(description)+", got: "+value,
)
}

// capitalize will uppercase the first letter in a UTF-8 string.
func capitalize(str string) string {
if str == "" {
Expand Down