Skip to content

Commit

Permalink
Splitting OneOf and NoneOf by "case sensitivity" (#45)
Browse files Browse the repository at this point in the history
* Splitting `OneOf` and `NoneOf` by "case sensitivity"
  * `OneOf` and `NoneOf` are case sensitive
  * `OneOfCaseInsensitive` and `NoneOfCaseInsensitive` are ... case insensitive instead
* Changelog entries
  • Loading branch information
Ivan De Marino authored Jun 27, 2022
1 parent 668b4ce commit 40680ac
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 52 deletions.
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

0 comments on commit 40680ac

Please sign in to comment.