Skip to content

stringvalidator: Add UTF-8 character count validators, clarify original length validators #87

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 2 commits into from
Dec 20, 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/87.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
stringvalidator: Added `UTF8LengthAtLeast`, `UTF8LengthAtMost`, and `UTF8LengthBetween` validators
```
11 changes: 5 additions & 6 deletions stringvalidator/length_at_least.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ func (v lengthAtLeastValidator) ValidateString(ctx context.Context, request vali
}
}

// LengthAtLeast returns an AttributeValidator which ensures that any configured
// attribute value:
// LengthAtLeast returns an validator which ensures that any configured
// attribute value is of single-byte character length greater than or equal
// to the given minimum. Null (unconfigured) and unknown (known after apply)
// values are skipped.
//
// - Is a string.
// - Is of length greater than or equal to the given minimum.
//
// Null (unconfigured) and unknown (known after apply) values are skipped.
// Use UTF8LengthAtLeast for checking multiple-byte characters.
func LengthAtLeast(minLength int) validator.String {
if minLength < 0 {
return nil
Expand Down
13 changes: 9 additions & 4 deletions stringvalidator/length_at_least_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,28 @@ func TestLengthAtLeastValidator(t *testing.T) {
expectError bool
}
tests := map[string]testCase{
"unknown String": {
"unknown": {
val: types.StringUnknown(),
minLength: 1,
},
"null String": {
"null": {
val: types.StringNull(),
minLength: 1,
},
"valid String": {
"valid": {
val: types.StringValue("ok"),
minLength: 1,
},
"too short String": {
"too short": {
val: types.StringValue(""),
minLength: 1,
expectError: true,
},
"multiple byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("⇄"),
minLength: 2,
},
}

for name, test := range tests {
Expand Down
11 changes: 5 additions & 6 deletions stringvalidator/length_at_most.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ func (v lengthAtMostValidator) ValidateString(ctx context.Context, request valid
}
}

// LengthAtMost returns an AttributeValidator which ensures that any configured
// attribute value:
// LengthAtMost returns an validator which ensures that any configured
// attribute value is of single-byte character length less than or equal
// to the given maximum. Null (unconfigured) and unknown (known after apply)
// values are skipped.
//
// - Is a string.
// - Is of length less than or equal to the given maximum.
//
// Null (unconfigured) and unknown (known after apply) values are skipped.
// Use UTF8LengthAtMost for checking multiple-byte characters.
func LengthAtMost(maxLength int) validator.String {
if maxLength < 0 {
return nil
Expand Down
14 changes: 10 additions & 4 deletions stringvalidator/length_at_most_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,29 @@ func TestLengthAtMostValidator(t *testing.T) {
expectError bool
}
tests := map[string]testCase{
"unknown String": {
"unknown": {
val: types.StringUnknown(),
maxLength: 1,
},
"null String": {
"null": {
val: types.StringNull(),
maxLength: 1,
},
"valid String": {
"valid": {
val: types.StringValue("ok"),
maxLength: 2,
},
"too long String": {
"too long": {
val: types.StringValue("not ok"),
maxLength: 5,
expectError: true,
},
"multiple byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("⇄"),
maxLength: 2,
expectError: true,
},
}

for name, test := range tests {
Expand Down
11 changes: 5 additions & 6 deletions stringvalidator/length_between.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ func (v lengthBetweenValidator) ValidateString(ctx context.Context, request vali
}
}

// LengthBetween returns an AttributeValidator which ensures that any configured
// attribute value:
// LengthBetween returns an validator which ensures that any configured
// attribute value is of single-byte character length greater than the given
// minimum and less than the given maximum. Null (unconfigured) and unknown
// (known after apply) values are skipped.
//
// - Is a string.
// - Is of length greater than the given minimum and less than the given maximum.
//
// Null (unconfigured) and unknown (known after apply) values are skipped.
// Use UTF8LengthBetween for checking multiple-byte characters.
func LengthBetween(minLength, maxLength int) validator.String {
if minLength < 0 || maxLength < 0 || minLength > maxLength {
return nil
Expand Down
16 changes: 11 additions & 5 deletions stringvalidator/length_between_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,39 @@ func TestLengthBetweenValidator(t *testing.T) {
expectError bool
}
tests := map[string]testCase{
"unknown String": {
"unknown": {
val: types.StringUnknown(),
minLength: 1,
maxLength: 3,
},
"null String": {
"null": {
val: types.StringNull(),
minLength: 1,
maxLength: 3,
},
"valid String": {
"valid": {
val: types.StringValue("ok"),
minLength: 1,
maxLength: 3,
},
"too long String": {
"too long": {
val: types.StringValue("not ok"),
minLength: 1,
maxLength: 3,
expectError: true,
},
"too short String": {
"too short": {
val: types.StringValue(""),
minLength: 1,
maxLength: 3,
expectError: true,
},
"multiple byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("⇄"),
minLength: 2,
maxLength: 4,
},
}

for name, test := range tests {
Expand Down
65 changes: 65 additions & 0 deletions stringvalidator/utf8_length_at_least.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package stringvalidator

import (
"context"
"fmt"
"unicode/utf8"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"

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

var _ validator.String = utf8LengthAtLeastValidator{}

// utf8LengthAtLeastValidator implements the validator.
type utf8LengthAtLeastValidator struct {
minLength int
}

// Description describes the validation in plain text formatting.
func (validator utf8LengthAtLeastValidator) Description(_ context.Context) string {
return fmt.Sprintf("UTF-8 character count must be at least %d", validator.minLength)
}

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

// Validate performs the validation.
func (v utf8LengthAtLeastValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

value := request.ConfigValue.ValueString()

count := utf8.RuneCountInString(value)

if count < v.minLength {
response.Diagnostics.Append(validatordiag.InvalidAttributeValueLengthDiagnostic(
request.Path,
v.Description(ctx),
fmt.Sprintf("%d", count),
))

return
}
}

// UTF8LengthAtLeast returns an validator which ensures that any configured
// attribute value is of UTF-8 character count greater than or equal to the
// given minimum. Null (unconfigured) and unknown (known after apply) values
// are skipped.
//
// Use LengthAtLeast for checking single-byte character counts.
func UTF8LengthAtLeast(minLength int) validator.String {
if minLength < 0 {
return nil
}

return utf8LengthAtLeastValidator{
minLength: minLength,
}
}
22 changes: 22 additions & 0 deletions stringvalidator/utf8_length_at_least_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package stringvalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

func ExampleUTF8LengthAtLeast() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attr": schema.StringAttribute{
Required: true,
Validators: []validator.String{
// Validate UTF-8 character count must be at least 3 characters.
stringvalidator.UTF8LengthAtLeast(3),
},
},
},
}
}
84 changes: 84 additions & 0 deletions stringvalidator/utf8_length_at_least_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package stringvalidator_test

import (
"context"
"testing"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"

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

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

type testCase struct {
val types.String
minLength int
expectError bool
}
tests := map[string]testCase{
"unknown": {
val: types.StringUnknown(),
minLength: 1,
},
"null": {
val: types.StringNull(),
minLength: 1,
},
"valid single byte characters": {
val: types.StringValue("ok"),
minLength: 1,
},
"valid mixed byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("test⇄test"),
minLength: 9,
},
"valid multiple byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("⇄"),
minLength: 1,
},
"invalid single byte characters": {
val: types.StringValue("ok"),
minLength: 3,
expectError: true,
},
"invalid mixed byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("test⇄test"),
minLength: 10,
expectError: true,
},
"invalid multiple byte characters": {
// Rightwards Arrow Over Leftwards Arrow (U+21C4; 3 bytes)
val: types.StringValue("⇄"),
minLength: 2,
expectError: true,
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
request := validator.StringRequest{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: test.val,
}
response := validator.StringResponse{}
stringvalidator.UTF8LengthAtLeast(test.minLength).ValidateString(context.Background(), 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)
}
})
}
}
Loading