Skip to content

Commit c6cc80d

Browse files
authored
String RegexMatches validator (#23)
Reference: #10
1 parent 19aa7ab commit c6cc80d

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

.changelog/23.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 `stringvalidator.RegexMatches()` validation function
3+
```

stringvalidator/regex_matches.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package stringvalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
8+
"github.com/hashicorp/terraform-plugin-framework-validators/validatordiag"
9+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
10+
)
11+
12+
var _ tfsdk.AttributeValidator = regexMatchesValidator{}
13+
14+
// regexMatchesValidator validates that a string Attribute's value matches the specified regular expression.
15+
type regexMatchesValidator struct {
16+
regexp *regexp.Regexp
17+
message string
18+
}
19+
20+
// Description describes the validation in plain text formatting.
21+
func (validator regexMatchesValidator) Description(_ context.Context) string {
22+
if validator.message != "" {
23+
return validator.message
24+
}
25+
return fmt.Sprintf("value must match regular expression '%s'", validator.regexp)
26+
}
27+
28+
// MarkdownDescription describes the validation in Markdown formatting.
29+
func (validator regexMatchesValidator) MarkdownDescription(ctx context.Context) string {
30+
return validator.Description(ctx)
31+
}
32+
33+
// Validate performs the validation.
34+
func (validator regexMatchesValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) {
35+
s, ok := validateString(ctx, request, response)
36+
37+
if !ok {
38+
return
39+
}
40+
41+
if ok := validator.regexp.MatchString(s); !ok {
42+
response.Diagnostics.Append(validatordiag.AttributeValueMatchesDiagnostic(
43+
request.AttributePath,
44+
validator.Description(ctx),
45+
s,
46+
))
47+
}
48+
}
49+
50+
// RegexMatches returns an AttributeValidator which ensures that any configured
51+
// attribute value:
52+
//
53+
// - Is a string.
54+
// - Matches the given regular expression https://github.com/google/re2/wiki/Syntax.
55+
//
56+
// Null (unconfigured) and unknown (known after apply) values are skipped.
57+
// Optionally an error message can be provided to return something friendlier
58+
// than "value must match regular expression 'regexp'".
59+
func RegexMatches(regexp *regexp.Regexp, message string) tfsdk.AttributeValidator {
60+
return regexMatchesValidator{
61+
regexp: regexp,
62+
message: message,
63+
}
64+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package stringvalidator
2+
3+
import (
4+
"context"
5+
"regexp"
6+
"testing"
7+
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 TestRegexMatchesValidator(t *testing.T) {
15+
t.Parallel()
16+
17+
type testCase struct {
18+
val attr.Value
19+
regexp *regexp.Regexp
20+
expectError bool
21+
}
22+
tests := map[string]testCase{
23+
"not a String": {
24+
val: types.Bool{Value: true},
25+
expectError: true,
26+
},
27+
"unknown String": {
28+
val: types.String{Unknown: true},
29+
regexp: regexp.MustCompile(`^o[j-l]?$`),
30+
},
31+
"null String": {
32+
val: types.String{Null: true},
33+
regexp: regexp.MustCompile(`^o[j-l]?$`),
34+
},
35+
"valid String": {
36+
val: types.String{Value: "ok"},
37+
regexp: regexp.MustCompile(`^o[j-l]?$`),
38+
},
39+
"invalid String": {
40+
val: types.String{Value: "not ok"},
41+
regexp: regexp.MustCompile(`^o[j-l]?$`),
42+
expectError: true,
43+
},
44+
}
45+
46+
for name, test := range tests {
47+
name, test := name, test
48+
t.Run(name, func(t *testing.T) {
49+
request := tfsdk.ValidateAttributeRequest{
50+
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
51+
AttributeConfig: test.val,
52+
}
53+
response := tfsdk.ValidateAttributeResponse{}
54+
RegexMatches(test.regexp, "").Validate(context.TODO(), request, &response)
55+
56+
if !response.Diagnostics.HasError() && test.expectError {
57+
t.Fatal("expected error, got no error")
58+
}
59+
60+
if response.Diagnostics.HasError() && !test.expectError {
61+
t.Fatalf("got unexpected error: %s", response.Diagnostics)
62+
}
63+
})
64+
}
65+
}

validatordiag/diag.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ func AttributeValueLengthDiagnostic(path *tftypes.AttributePath, description str
2626
)
2727
}
2828

29+
// AttributeValueMatchesDiagnostic returns an error Diagnostic to be used when an attribute's value has an invalid match.
30+
func AttributeValueMatchesDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic {
31+
return diag.NewAttributeErrorDiagnostic(
32+
path,
33+
"Invalid Attribute Value Match",
34+
capitalize(description)+", got: "+value,
35+
)
36+
}
37+
2938
// capitalize will uppercase the first letter in a UTF-8 string.
3039
func capitalize(str string) string {
3140
if str == "" {

0 commit comments

Comments
 (0)