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

Splitting OneOf and NoneOf by "case sensitivity" #45

Merged
merged 2 commits into from
Jun 27, 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
4 changes: 0 additions & 4 deletions .changelog/42.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,3 @@ int64validator: 2 new validation functions, `OneOf()` and `NoneOf()`
```release-note:feature
numbervalidator: New package that starts with 2 validation functions, `OneOf()` and `NoneOf()`
```

```release-note:enhancement
stringvalidator: 2 new validation functions, `OneOf()` and `NoneOf()`, that offer case-sensitivity control
```
3 changes: 3 additions & 0 deletions .changelog/45.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
stringvalidator: 4 new validation functions, `OneOf()` and `NoneOf()` (case sensitive), and `OneOfCaseInsensitive()` and `NoneOfCaseInsensitive()` (case insensitive)
```
25 changes: 9 additions & 16 deletions stringvalidator/acceptable_strings_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,27 @@ import (
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)

// acceptableStringsAttributeValidator is the underlying struct implementing OneOf and NoneOf.
type acceptableStringsAttributeValidator struct {
// acceptableStringsCaseInsensitiveAttributeValidator is the underlying struct implementing OneOf and NoneOf.
type acceptableStringsCaseInsensitiveAttributeValidator struct {
acceptableStrings []string
caseSensitive bool
shouldMatch bool
}

var _ tfsdk.AttributeValidator = (*acceptableStringsAttributeValidator)(nil)
var _ tfsdk.AttributeValidator = (*acceptableStringsCaseInsensitiveAttributeValidator)(nil)

func (av *acceptableStringsAttributeValidator) Description(ctx context.Context) string {
func (av *acceptableStringsCaseInsensitiveAttributeValidator) Description(ctx context.Context) string {
return av.MarkdownDescription(ctx)
}

func (av *acceptableStringsAttributeValidator) MarkdownDescription(_ context.Context) string {
func (av *acceptableStringsCaseInsensitiveAttributeValidator) MarkdownDescription(_ context.Context) string {
if av.shouldMatch {
return fmt.Sprintf("String must match one of: %q", av.acceptableStrings)
} else {
return fmt.Sprintf("String must match none of: %q", av.acceptableStrings)
}
}

func (av *acceptableStringsAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) {
func (av *acceptableStringsCaseInsensitiveAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) {
value, ok := validateString(ctx, req, res)
if !ok {
return
Expand All @@ -46,16 +45,10 @@ func (av *acceptableStringsAttributeValidator) Validate(ctx context.Context, req
}
}

func (av *acceptableStringsAttributeValidator) isAcceptableValue(v string) bool {
func (av *acceptableStringsCaseInsensitiveAttributeValidator) isAcceptableValue(v string) bool {
for _, acceptableV := range av.acceptableStrings {
if av.caseSensitive {
if v == acceptableV {
return true
}
} else {
if strings.EqualFold(v, acceptableV) {
return true
}
if strings.EqualFold(v, acceptableV) {
return true
}
}

Expand Down
21 changes: 16 additions & 5 deletions stringvalidator/none_of.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package stringvalidator

import (
"github.com/hashicorp/terraform-plugin-framework-validators/internal/primitivevalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// NoneOf checks that the string held in the attribute
// is none of the given `unacceptableStrings`.
//
// String comparison case sensitiveness is controlled by the `caseSensitive` argument.
func NoneOf(caseSensitive bool, unacceptableStrings ...string) tfsdk.AttributeValidator {
return &acceptableStringsAttributeValidator{
func NoneOf(unacceptableStrings ...string) tfsdk.AttributeValidator {
unacceptableStringValues := make([]attr.Value, 0, len(unacceptableStrings))
for _, s := range unacceptableStrings {
unacceptableStringValues = append(unacceptableStringValues, types.String{Value: s})
}

return primitivevalidator.NoneOf(unacceptableStringValues...)
}

// NoneOfCaseInsensitive checks that the string held in the attribute
// is none of the given `unacceptableStrings`, irrespective of case sensitivity.
func NoneOfCaseInsensitive(unacceptableStrings ...string) tfsdk.AttributeValidator {
return &acceptableStringsCaseInsensitiveAttributeValidator{
unacceptableStrings,
caseSensitive,
false,
}
}
172 changes: 161 additions & 11 deletions stringvalidator/none_of_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,24 @@ func TestNoneOfValidator(t *testing.T) {
"simple-match": {
in: types.String{Value: "foo"},
validator: stringvalidator.NoneOf(
true,
"foo",
"bar",
"baz",
),
expErrors: 1,
},
"simple-match-case-insensitive": {
"simple-mismatch-case-insensitive": {
in: types.String{Value: "foo"},
validator: stringvalidator.NoneOf(
false,
"FOO",
"bar",
"baz",
),
expErrors: 1,
expErrors: 0,
},
"simple-mismatch": {
in: types.String{Value: "foz"},
validator: stringvalidator.NoneOf(
true,
"foo",
"bar",
"baz",
Expand All @@ -67,7 +64,6 @@ func TestNoneOfValidator(t *testing.T) {
},
},
validator: stringvalidator.NoneOf(
true,
"10",
"20",
"30",
Expand All @@ -86,7 +82,6 @@ func TestNoneOfValidator(t *testing.T) {
},
},
validator: stringvalidator.NoneOf(
true,
"bob",
"alice",
"john",
Expand All @@ -106,7 +101,6 @@ func TestNoneOfValidator(t *testing.T) {
},
},
validator: stringvalidator.NoneOf(
true,
"1.1",
"10.20",
"5.4",
Expand All @@ -125,7 +119,6 @@ func TestNoneOfValidator(t *testing.T) {
},
},
validator: stringvalidator.NoneOf(
true,
"Bob Parr",
"40",
"1200 Park Avenue Emeryville",
Expand All @@ -136,7 +129,6 @@ func TestNoneOfValidator(t *testing.T) {
"skip-validation-on-null": {
in: types.String{Null: true},
validator: stringvalidator.NoneOf(
true,
"foo",
"bar",
"baz",
Expand All @@ -146,7 +138,165 @@ func TestNoneOfValidator(t *testing.T) {
"skip-validation-on-unknown": {
in: types.String{Unknown: true},
validator: stringvalidator.NoneOf(
true,
"foo",
"bar",
"baz",
),
expErrors: 0,
},
}

for name, test := range testCases {
name, test := name, test
t.Run(name, func(t *testing.T) {
req := tfsdk.ValidateAttributeRequest{
AttributeConfig: test.in,
}
res := tfsdk.ValidateAttributeResponse{}
test.validator.Validate(context.TODO(), req, &res)

if test.expErrors > 0 && !res.Diagnostics.HasError() {
t.Fatalf("expected %d error(s), got none", test.expErrors)
}

if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) {
t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics)
}

if test.expErrors == 0 && res.Diagnostics.HasError() {
t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics)
}
})
}
}

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

type testCase struct {
in attr.Value
validator tfsdk.AttributeValidator
expErrors int
}

objAttrTypes := map[string]attr.Type{
"Name": types.StringType,
"Age": types.StringType,
"Address": types.StringType,
}

testCases := map[string]testCase{
"simple-match": {
in: types.String{Value: "foo"},
validator: stringvalidator.NoneOfCaseInsensitive(
"foo",
"bar",
"baz",
),
expErrors: 1,
},
"simple-match-case-insensitive": {
in: types.String{Value: "foo"},
validator: stringvalidator.NoneOfCaseInsensitive(
"FOO",
"bar",
"baz",
),
expErrors: 1,
},
"simple-mismatch": {
in: types.String{Value: "foz"},
validator: stringvalidator.NoneOfCaseInsensitive(
"foo",
"bar",
"baz",
),
expErrors: 0,
},
"list-not-allowed": {
in: types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "10"},
types.String{Value: "20"},
types.String{Value: "30"},
},
},
validator: stringvalidator.NoneOfCaseInsensitive(
"10",
"20",
"30",
"40",
"50",
),
expErrors: 1,
},
"set-not-allowed": {
in: types.Set{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "foo"},
types.String{Value: "bar"},
types.String{Value: "baz"},
},
},
validator: stringvalidator.NoneOfCaseInsensitive(
"bob",
"alice",
"john",
"foo",
"bar",
"baz",
),
expErrors: 1,
},
"map-not-allowed": {
in: types.Map{
ElemType: types.StringType,
Elems: map[string]attr.Value{
"one.one": types.String{Value: "1.1"},
"ten.twenty": types.String{Value: "10.20"},
"five.four": types.String{Value: "5.4"},
},
},
validator: stringvalidator.NoneOfCaseInsensitive(
"1.1",
"10.20",
"5.4",
"geronimo",
"bob",
),
expErrors: 1,
},
"object-not-allowed": {
in: types.Object{
AttrTypes: objAttrTypes,
Attrs: map[string]attr.Value{
"Name": types.String{Value: "Bob Parr"},
"Age": types.String{Value: "40"},
"Address": types.String{Value: "1200 Park Avenue Emeryville"},
},
},
validator: stringvalidator.NoneOfCaseInsensitive(
"Bob Parr",
"40",
"1200 Park Avenue Emeryville",
"123",
),
expErrors: 1,
},
"skip-validation-on-null": {
in: types.String{Null: true},
validator: stringvalidator.NoneOfCaseInsensitive(
"foo",
"bar",
"baz",
),
expErrors: 0,
},
"skip-validation-on-unknown": {
in: types.String{Unknown: true},
validator: stringvalidator.NoneOfCaseInsensitive(
"foo",
"bar",
"baz",
Expand Down
21 changes: 16 additions & 5 deletions stringvalidator/one_of.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package stringvalidator

import (
"github.com/hashicorp/terraform-plugin-framework-validators/internal/primitivevalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// OneOf checks that the string held in the attribute
// is one of the given `acceptableStrings`.
//
// String comparison case sensitiveness is controlled by the `caseSensitive` argument.
func OneOf(caseSensitive bool, acceptableStrings ...string) tfsdk.AttributeValidator {
return &acceptableStringsAttributeValidator{
func OneOf(acceptableStrings ...string) tfsdk.AttributeValidator {
acceptableStringValues := make([]attr.Value, 0, len(acceptableStrings))
for _, s := range acceptableStrings {
acceptableStringValues = append(acceptableStringValues, types.String{Value: s})
}

return primitivevalidator.OneOf(acceptableStringValues...)
}

// OneOfCaseInsensitive checks that the string held in the attribute
// is one of the given `acceptableStrings`, irrespective of case sensitivity.
func OneOfCaseInsensitive(acceptableStrings ...string) tfsdk.AttributeValidator {
return &acceptableStringsCaseInsensitiveAttributeValidator{
acceptableStrings,
caseSensitive,
true,
}
}
Loading