Skip to content

Commit 8561023

Browse files
author
Ivan De Marino
committed
Adding OneOf and NoneOf to stringvalidator
It's a string-specific version of `genericvalidator.OneOf/NoneOf`, that adds support for case-sensitiveness.
1 parent 6729922 commit 8561023

File tree

8 files changed

+885
-2
lines changed

8 files changed

+885
-2
lines changed

genericvalidator/none_of_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestNoneOfValidator(t *testing.T) {
4343
),
4444
expErrors: 1,
4545
},
46-
"simple-mismatch-match": {
46+
"simple-mismatch": {
4747
in: types.String{Value: "foz"},
4848
validator: genericvalidator.NoneOf(
4949
types.String{Value: "foo"},

genericvalidator/one_of_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestOneOfValidator(t *testing.T) {
4242
types.String{Value: "baz"},
4343
),
4444
},
45-
"simple-mismatch-match": {
45+
"simple-mismatch": {
4646
in: types.String{Value: "foz"},
4747
validator: genericvalidator.OneOf(
4848
types.String{Value: "foo"},

helpers/validatordiag/diag.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ func InvalidSchemaDiagnostic(path *tftypes.AttributePath, description string) di
4444
)
4545
}
4646

47+
// InvalidTypeDiagnostic returns an error Diagnostic to be used when an attribute has an invalid type.
48+
func InvalidTypeDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic {
49+
return diag.NewAttributeErrorDiagnostic(
50+
path,
51+
"Invalid Attribute Type",
52+
capitalize(description)+", got: "+value,
53+
)
54+
}
55+
4756
// ErrorsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityError.
4857
func ErrorsCount(diags diag.Diagnostics) int {
4958
count := 0
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package stringvalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
9+
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
)
13+
14+
// acceptableStringsAttributeValidator is the underlying struct implementing OneOf and NoneOf.
15+
type acceptableStringsAttributeValidator struct {
16+
acceptableStringValues []types.String
17+
caseSensitive bool
18+
shouldMatch bool
19+
}
20+
21+
var _ tfsdk.AttributeValidator = (*acceptableStringsAttributeValidator)(nil)
22+
23+
func (av *acceptableStringsAttributeValidator) Description(ctx context.Context) string {
24+
return av.MarkdownDescription(ctx)
25+
}
26+
27+
func (av *acceptableStringsAttributeValidator) MarkdownDescription(_ context.Context) string {
28+
if av.shouldMatch {
29+
return fmt.Sprintf("String(s) must match one of: %q", av.acceptableStringValues)
30+
} else {
31+
return fmt.Sprintf("String(s) must match none of: %q", av.acceptableStringValues)
32+
}
33+
34+
}
35+
36+
func (av *acceptableStringsAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) {
37+
if req.AttributeConfig.IsNull() || req.AttributeConfig.IsUnknown() {
38+
return
39+
}
40+
41+
// Gather the `values` to validate:
42+
// singleton slice for primitives,
43+
// a collection for the other possible `types.*`.
44+
var values []attr.Value
45+
switch typedAttributeConfig := req.AttributeConfig.(type) {
46+
case types.List:
47+
values = typedAttributeConfig.Elems
48+
case types.Map:
49+
values = make([]attr.Value, 0, len(typedAttributeConfig.Elems))
50+
for _, v := range typedAttributeConfig.Elems {
51+
values = append(values, v)
52+
}
53+
case types.Set:
54+
values = typedAttributeConfig.Elems
55+
case types.Object:
56+
values = make([]attr.Value, 0, len(typedAttributeConfig.Attrs))
57+
for _, v := range typedAttributeConfig.Attrs {
58+
values = append(values, v)
59+
}
60+
default:
61+
values = []attr.Value{typedAttributeConfig}
62+
}
63+
64+
for _, v := range values {
65+
// Before we go any further, we need to confirm the values we gathered are indeed strings
66+
vType := v.Type(ctx)
67+
if vType != types.StringType {
68+
res.Diagnostics.Append(validatordiag.InvalidTypeDiagnostic(
69+
req.AttributePath,
70+
fmt.Sprintf("Expected value %q of type %s", v.String(), types.StringType),
71+
v.Type(ctx).String(),
72+
))
73+
continue
74+
}
75+
76+
vStr := v.(types.String)
77+
78+
if av.shouldMatch && !av.isAcceptableValue(vStr) || //< EITHER should match but it does not
79+
!av.shouldMatch && av.isAcceptableValue(vStr) { //< OR should not match but it does
80+
res.Diagnostics.Append(validatordiag.InvalidValueMatchDiagnostic(
81+
req.AttributePath,
82+
av.Description(ctx),
83+
v.String(),
84+
))
85+
}
86+
}
87+
}
88+
89+
func (av *acceptableStringsAttributeValidator) isAcceptableValue(v types.String) bool {
90+
for _, acceptableV := range av.acceptableStringValues {
91+
if av.caseSensitive {
92+
if v.Equal(acceptableV) {
93+
return true
94+
}
95+
} else {
96+
// Before we proceed, we make lower-cased copies of the values involved
97+
vLower := types.String{
98+
Value: strings.ToLower(v.Value),
99+
Unknown: v.Unknown,
100+
Null: v.Null,
101+
}
102+
acceptableVLower := types.String{
103+
Value: strings.ToLower(acceptableV.Value),
104+
Unknown: v.Unknown,
105+
Null: v.Null,
106+
}
107+
108+
if vLower.Equal(acceptableVLower) {
109+
return true
110+
}
111+
}
112+
}
113+
114+
return false
115+
}

stringvalidator/none_of.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package stringvalidator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
5+
"github.com/hashicorp/terraform-plugin-framework/types"
6+
)
7+
8+
// NoneOf checks that string(s) held in the attribute
9+
// is (are) none of the given `acceptableStrings`.
10+
//
11+
// This validator can be used `types.String`, as well as
12+
// collections (`types.List`, `types.Set`, `types.Map` and `types.Object`)
13+
// that contain `types.String`.
14+
//
15+
// For key/value collections, the validator will be applied only to the values.
16+
//
17+
// String comparison case sensitiveness is controlled by the `caseSensitive` argument.
18+
func NoneOf(caseSensitive bool, acceptableStrings ...string) tfsdk.AttributeValidator {
19+
acceptableStringValues := make([]types.String, 0, len(acceptableStrings))
20+
for _, s := range acceptableStrings {
21+
acceptableStringValues = append(acceptableStringValues, types.String{Value: s})
22+
}
23+
24+
return &acceptableStringsAttributeValidator{
25+
acceptableStringValues: acceptableStringValues,
26+
caseSensitive: caseSensitive,
27+
shouldMatch: false,
28+
}
29+
}

0 commit comments

Comments
 (0)