From 63cbbd2a51a0b881bbca9439b23d78bf6296d011 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Wed, 25 May 2022 09:10:52 -0400 Subject: [PATCH 1/4] Add stringvalidator.RegexMatches. --- stringvalidator/regex_matches.go | 62 +++++++++++++++++++++++++ stringvalidator/regex_matches_test.go | 65 +++++++++++++++++++++++++++ validatordiag/diag.go | 9 ++++ 3 files changed, 136 insertions(+) create mode 100644 stringvalidator/regex_matches.go create mode 100644 stringvalidator/regex_matches_test.go diff --git a/stringvalidator/regex_matches.go b/stringvalidator/regex_matches.go new file mode 100644 index 00000000..f5ed9031 --- /dev/null +++ b/stringvalidator/regex_matches.go @@ -0,0 +1,62 @@ +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. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func RegexMatches(regexp *regexp.Regexp, message string) tfsdk.AttributeValidator { + return regexMatchesValidator{ + regexp: regexp, + message: message, + } +} diff --git a/stringvalidator/regex_matches_test.go b/stringvalidator/regex_matches_test.go new file mode 100644 index 00000000..2f4ce12b --- /dev/null +++ b/stringvalidator/regex_matches_test.go @@ -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 + } + 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) { + 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) + } + }) + } +} diff --git a/validatordiag/diag.go b/validatordiag/diag.go index f165cdd0..c43567f2 100644 --- a/validatordiag/diag.go +++ b/validatordiag/diag.go @@ -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 == "" { From cd4c95a3db207d166f5c57371b67f89993f5b3e4 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Wed, 25 May 2022 09:13:52 -0400 Subject: [PATCH 2/4] Add CHANGELOG entry. --- .changelog/23.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/23.txt diff --git a/.changelog/23.txt b/.changelog/23.txt new file mode 100644 index 00000000..f2c4b9d0 --- /dev/null +++ b/.changelog/23.txt @@ -0,0 +1,3 @@ +```release-note:feature +Introduced `stringvalidator.RegexMatches()` validation function +``` \ No newline at end of file From 5b05416ef7c3181b3ef6cb44cdcf64238314c2c4 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Wed, 25 May 2022 09:22:31 -0400 Subject: [PATCH 3/4] Add documentation link to re2 syntax. --- stringvalidator/regex_matches.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stringvalidator/regex_matches.go b/stringvalidator/regex_matches.go index f5ed9031..1fd595a1 100644 --- a/stringvalidator/regex_matches.go +++ b/stringvalidator/regex_matches.go @@ -51,7 +51,7 @@ func (validator regexMatchesValidator) Validate(ctx context.Context, request tfs // attribute value: // // - Is a string. -// - Matches the given regular expression. +// - Matches the given regular expression https://github.com/google/re2/wiki/Syntax. // // Null (unconfigured) and unknown (known after apply) values are skipped. func RegexMatches(regexp *regexp.Regexp, message string) tfsdk.AttributeValidator { From 01f38b0d4b0f5ac93ea29db2af7d5fac11199c8c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Wed, 25 May 2022 09:33:30 -0400 Subject: [PATCH 4/4] Document optional error message. --- stringvalidator/regex_matches.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stringvalidator/regex_matches.go b/stringvalidator/regex_matches.go index 1fd595a1..1d3fbf9f 100644 --- a/stringvalidator/regex_matches.go +++ b/stringvalidator/regex_matches.go @@ -54,6 +54,8 @@ func (validator regexMatchesValidator) Validate(ctx context.Context, request tfs // - 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,