diff --git a/.changelog/42.txt b/.changelog/42.txt new file mode 100644 index 00000000..ff2acc66 --- /dev/null +++ b/.changelog/42.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +floatvalidator: 2 new validation functions, `OneOf()` and `NoneOf()` +``` + +```release-note:enhancement +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 +``` diff --git a/.gitignore b/.gitignore index 8c095eee..6ba0a499 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -# Jetbrains IDEs +# JetBrains IDEs project files .idea/ *.iws diff --git a/float64validator/at_least.go b/float64validator/at_least.go index 2a5c89fe..546e74b3 100644 --- a/float64validator/at_least.go +++ b/float64validator/at_least.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator atLeastValidator) Validate(ctx context.Context, request tfsdk.Va } if f < validator.min { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%f", f), diff --git a/float64validator/at_least_test.go b/float64validator/at_least_test.go index eec6e429..2848a263 100644 --- a/float64validator/at_least_test.go +++ b/float64validator/at_least_test.go @@ -1,9 +1,10 @@ -package float64validator +package float64validator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -58,7 +59,7 @@ func TestAtLeastValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - AtLeast(test.min).Validate(context.TODO(), request, &response) + float64validator.AtLeast(test.min).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/float64validator/at_most.go b/float64validator/at_most.go index 79c41a11..b79868e3 100644 --- a/float64validator/at_most.go +++ b/float64validator/at_most.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator atMostValidator) Validate(ctx context.Context, request tfsdk.Val } if f > validator.max { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%f", f), diff --git a/float64validator/at_most_test.go b/float64validator/at_most_test.go index 0ac5e1b5..d9c3d44c 100644 --- a/float64validator/at_most_test.go +++ b/float64validator/at_most_test.go @@ -1,9 +1,10 @@ -package float64validator +package float64validator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -58,7 +59,7 @@ func TestAtMostValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - AtMost(test.max).Validate(context.TODO(), request, &response) + float64validator.AtMost(test.max).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/float64validator/between.go b/float64validator/between.go index 40c50d14..440a1dd0 100644 --- a/float64validator/between.go +++ b/float64validator/between.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator betweenValidator) Validate(ctx context.Context, request tfsdk.Va } if f < validator.min || f > validator.max { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%f", f), diff --git a/float64validator/between_test.go b/float64validator/between_test.go index 745666f2..6effb36d 100644 --- a/float64validator/between_test.go +++ b/float64validator/between_test.go @@ -1,9 +1,10 @@ -package float64validator +package float64validator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -76,7 +77,7 @@ func TestBetweenValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - Between(test.min, test.max).Validate(context.TODO(), request, &response) + float64validator.Between(test.min, test.max).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/float64validator/none_of.go b/float64validator/none_of.go new file mode 100644 index 00000000..85aeced7 --- /dev/null +++ b/float64validator/none_of.go @@ -0,0 +1,19 @@ +package float64validator + +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 float64 held in the attribute +// is none of the given `unacceptableFloats`. +func NoneOf(unacceptableFloats ...float64) tfsdk.AttributeValidator { + unacceptableFloatValues := make([]attr.Value, 0, len(unacceptableFloats)) + for _, f := range unacceptableFloats { + unacceptableFloatValues = append(unacceptableFloatValues, types.Float64{Value: f}) + } + + return primitivevalidator.NoneOf(unacceptableFloatValues...) +} diff --git a/float64validator/none_of_test.go b/float64validator/none_of_test.go new file mode 100644 index 00000000..102f779b --- /dev/null +++ b/float64validator/none_of_test.go @@ -0,0 +1,85 @@ +package float64validator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.Float64{Value: 123.456}, + validator: float64validator.NoneOf( + 123.456, + 234.567, + 8910.11, + 1213.1415, + ), + expErrors: 1, + }, + "simple-mismatch": { + in: types.Float64{Value: 123.456}, + validator: float64validator.NoneOf( + 234.567, + 8910.11, + 1213.1415, + ), + expErrors: 0, + }, + "skip-validation-on-null": { + in: types.Float64{Null: true}, + validator: float64validator.NoneOf( + 234.567, + 8910.11, + 1213.1415, + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.Float64{Unknown: true}, + validator: float64validator.NoneOf( + 234.567, + 8910.11, + 1213.1415, + ), + 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) + } + }) + } +} diff --git a/float64validator/one_of.go b/float64validator/one_of.go new file mode 100644 index 00000000..cd5fba49 --- /dev/null +++ b/float64validator/one_of.go @@ -0,0 +1,19 @@ +package float64validator + +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 float64 held in the attribute +// is one of the given `acceptableFloats`. +func OneOf(acceptableFloats ...float64) tfsdk.AttributeValidator { + acceptableFloatValues := make([]attr.Value, 0, len(acceptableFloats)) + for _, f := range acceptableFloats { + acceptableFloatValues = append(acceptableFloatValues, types.Float64{Value: f}) + } + + return primitivevalidator.OneOf(acceptableFloatValues...) +} diff --git a/float64validator/one_of_test.go b/float64validator/one_of_test.go new file mode 100644 index 00000000..afe79b3b --- /dev/null +++ b/float64validator/one_of_test.go @@ -0,0 +1,85 @@ +package float64validator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.Float64{Value: 123.456}, + validator: float64validator.OneOf( + 123.456, + 234.567, + 8910.11, + 1213.1415, + ), + expErrors: 0, + }, + "simple-mismatch": { + in: types.Float64{Value: 123.456}, + validator: float64validator.OneOf( + 234.567, + 8910.11, + 1213.1415, + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.Float64{Null: true}, + validator: float64validator.OneOf( + 234.567, + 8910.11, + 1213.1415, + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.Float64{Unknown: true}, + validator: float64validator.OneOf( + 234.567, + 8910.11, + 1213.1415, + ), + 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) + } + }) + } +} diff --git a/float64validator/type_validation.go b/float64validator/type_validation.go index d4d99473..d02e2183 100644 --- a/float64validator/type_validation.go +++ b/float64validator/type_validation.go @@ -3,25 +3,28 @@ package float64validator import ( "context" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) // validateFloat ensures that the request contains a Float64 value. func validateFloat(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) (float64, bool) { - var n types.Float64 - - diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &n) - - if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) - - return 0, false + t := request.AttributeConfig.Type(ctx) + if t != types.Float64Type { + response.Diagnostics.Append(validatordiag.InvalidAttributeTypeDiagnostic( + request.AttributePath, + "Expected value of type float64", + t.String(), + )) + return 0.0, false } - if n.Unknown || n.Null { - return 0, false + f := request.AttributeConfig.(types.Float64) + + if f.Unknown || f.Null { + return 0.0, false } - return n.Value, true + return f.Value, true } diff --git a/helpers/validatordiag/diag.go b/helpers/validatordiag/diag.go new file mode 100644 index 00000000..4ea742e4 --- /dev/null +++ b/helpers/validatordiag/diag.go @@ -0,0 +1,91 @@ +package validatordiag + +import ( + "unicode" + "unicode/utf8" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// InvalidAttributeValueDiagnostic returns an error Diagnostic to be used when an attribute has an invalid value. +func InvalidAttributeValueDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Attribute Value", + capitalize(description)+", got: "+value, + ) +} + +// InvalidAttributeValueLengthDiagnostic returns an error Diagnostic to be used when an attribute's value has an invalid length. +func InvalidAttributeValueLengthDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Attribute Value Length", + capitalize(description)+", got: "+value, + ) +} + +// InvalidAttributeValueMatchDiagnostic returns an error Diagnostic to be used when an attribute's value has an invalid match. +func InvalidAttributeValueMatchDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Attribute Value Match", + capitalize(description)+", got: "+value, + ) +} + +// InvalidAttributeSchemaDiagnostic returns an error Diagnostic to be used when a schemavalidator of attributes is invalid. +func InvalidAttributeSchemaDiagnostic(path *tftypes.AttributePath, description string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Attribute Combination", + capitalize(description), + ) +} + +// InvalidAttributeTypeDiagnostic returns an error Diagnostic to be used when an attribute has an invalid type. +func InvalidAttributeTypeDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Attribute Type", + capitalize(description)+", got: "+value, + ) +} + +// ErrorsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityError. +func ErrorsCount(diags diag.Diagnostics) int { + count := 0 + + for _, d := range diags { + if diag.SeverityError == d.Severity() { + count++ + } + } + + return count +} + +// WarningsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityWarning. +func WarningsCount(diags diag.Diagnostics) int { + count := 0 + + for _, d := range diags { + if diag.SeverityWarning == d.Severity() { + count++ + } + } + + return count +} + +// capitalize will uppercase the first letter in a UTF-8 string. +func capitalize(str string) string { + if str == "" { + return "" + } + + firstRune, size := utf8.DecodeRuneInString(str) + + return string(unicode.ToUpper(firstRune)) + str[size:] +} diff --git a/validatordiag/diag_test.go b/helpers/validatordiag/diag_test.go similarity index 100% rename from validatordiag/diag_test.go rename to helpers/validatordiag/diag_test.go diff --git a/validatordiag/doc.go b/helpers/validatordiag/doc.go similarity index 78% rename from validatordiag/doc.go rename to helpers/validatordiag/doc.go index 372bd460..c31457a0 100644 --- a/validatordiag/doc.go +++ b/helpers/validatordiag/doc.go @@ -1,3 +1,2 @@ -// Package validatordiag provides diagnostics helpers for validator -// implementations. +// Package validatordiag provides diagnostics helpers for validator implementations. package validatordiag diff --git a/int64validator/at_least.go b/int64validator/at_least.go index 894aa789..d371a098 100644 --- a/int64validator/at_least.go +++ b/int64validator/at_least.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator atLeastValidator) Validate(ctx context.Context, request tfsdk.Va } if i < validator.min { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", i), diff --git a/int64validator/at_least_test.go b/int64validator/at_least_test.go index 5763a12e..72c939f4 100644 --- a/int64validator/at_least_test.go +++ b/int64validator/at_least_test.go @@ -1,9 +1,10 @@ -package int64validator +package int64validator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -54,7 +55,7 @@ func TestAtLeastValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - AtLeast(test.min).Validate(context.TODO(), request, &response) + int64validator.AtLeast(test.min).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/at_most.go b/int64validator/at_most.go index 6fe9954a..050ebe7c 100644 --- a/int64validator/at_most.go +++ b/int64validator/at_most.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator atMostValidator) Validate(ctx context.Context, request tfsdk.Val } if i > validator.max { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", i), diff --git a/int64validator/at_most_test.go b/int64validator/at_most_test.go index 5302eed0..7e54edf5 100644 --- a/int64validator/at_most_test.go +++ b/int64validator/at_most_test.go @@ -1,9 +1,10 @@ -package int64validator +package int64validator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -54,7 +55,7 @@ func TestAtMostValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - AtMost(test.max).Validate(context.TODO(), request, &response) + int64validator.AtMost(test.max).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/between.go b/int64validator/between.go index cb590f1a..e093f203 100644 --- a/int64validator/between.go +++ b/int64validator/between.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator betweenValidator) Validate(ctx context.Context, request tfsdk.Va } if i < validator.min || i > validator.max { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", i), diff --git a/int64validator/between_test.go b/int64validator/between_test.go index f0126ee7..119ec104 100644 --- a/int64validator/between_test.go +++ b/int64validator/between_test.go @@ -1,9 +1,10 @@ -package int64validator +package int64validator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -71,7 +72,7 @@ func TestBetweenValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - Between(test.min, test.max).Validate(context.TODO(), request, &response) + int64validator.Between(test.min, test.max).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/none_of.go b/int64validator/none_of.go new file mode 100644 index 00000000..fedff940 --- /dev/null +++ b/int64validator/none_of.go @@ -0,0 +1,19 @@ +package int64validator + +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 int64 held in the attribute +// is none of the given `unacceptableInts`. +func NoneOf(unacceptableInts ...int64) tfsdk.AttributeValidator { + unacceptableIntValues := make([]attr.Value, 0, len(unacceptableInts)) + for _, i := range unacceptableInts { + unacceptableIntValues = append(unacceptableIntValues, types.Int64{Value: i}) + } + + return primitivevalidator.NoneOf(unacceptableIntValues...) +} diff --git a/int64validator/none_of_test.go b/int64validator/none_of_test.go new file mode 100644 index 00000000..3d7dd0ee --- /dev/null +++ b/int64validator/none_of_test.go @@ -0,0 +1,85 @@ +package int64validator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.Int64{Value: 123}, + validator: int64validator.NoneOf( + 123, + 234, + 8910, + 1213, + ), + expErrors: 1, + }, + "simple-mismatch": { + in: types.Int64{Value: 123}, + validator: int64validator.NoneOf( + 234, + 8910, + 1213, + ), + expErrors: 0, + }, + "skip-validation-on-null": { + in: types.Int64{Null: true}, + validator: int64validator.NoneOf( + 234, + 8910, + 1213, + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.Int64{Unknown: true}, + validator: int64validator.NoneOf( + 234, + 8910, + 1213, + ), + 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) + } + }) + } +} diff --git a/int64validator/one_of.go b/int64validator/one_of.go new file mode 100644 index 00000000..13fa3df5 --- /dev/null +++ b/int64validator/one_of.go @@ -0,0 +1,19 @@ +package int64validator + +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 int64 held in the attribute +// is one of the given `acceptableInts`. +func OneOf(acceptableInts ...int64) tfsdk.AttributeValidator { + acceptableIntValues := make([]attr.Value, 0, len(acceptableInts)) + for _, i := range acceptableInts { + acceptableIntValues = append(acceptableIntValues, types.Int64{Value: i}) + } + + return primitivevalidator.OneOf(acceptableIntValues...) +} diff --git a/int64validator/one_of_test.go b/int64validator/one_of_test.go new file mode 100644 index 00000000..95517cb1 --- /dev/null +++ b/int64validator/one_of_test.go @@ -0,0 +1,85 @@ +package int64validator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.Int64{Value: 123}, + validator: int64validator.OneOf( + 123, + 234, + 8910, + 1213, + ), + expErrors: 0, + }, + "simple-mismatch": { + in: types.Int64{Value: 123}, + validator: int64validator.OneOf( + 234, + 8910, + 1213, + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.Int64{Null: true}, + validator: int64validator.OneOf( + 234, + 8910, + 1213, + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.Int64{Unknown: true}, + validator: int64validator.OneOf( + 234, + 8910, + 1213, + ), + 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) + } + }) + } +} diff --git a/int64validator/type_validation.go b/int64validator/type_validation.go index a7b10b44..383ac6e5 100644 --- a/int64validator/type_validation.go +++ b/int64validator/type_validation.go @@ -3,25 +3,28 @@ package int64validator import ( "context" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) // validateInt ensures that the request contains an Int64 value. func validateInt(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) (int64, bool) { - var n types.Int64 - - diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &n) - - if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) - + t := request.AttributeConfig.Type(ctx) + if t != types.Int64Type { + response.Diagnostics.Append(validatordiag.InvalidAttributeTypeDiagnostic( + request.AttributePath, + "Expected value of type int64", + t.String(), + )) return 0, false } - if n.Unknown || n.Null { + i := request.AttributeConfig.(types.Int64) + + if i.Unknown || i.Null { return 0, false } - return n.Value, true + return i.Value, true } diff --git a/internal/primitivevalidator/acceptable_values_validator.go b/internal/primitivevalidator/acceptable_values_validator.go new file mode 100644 index 00000000..9312afdb --- /dev/null +++ b/internal/primitivevalidator/acceptable_values_validator.go @@ -0,0 +1,70 @@ +package primitivevalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// acceptablePrimitiveValuesAttributeValidator is the underlying struct implementing OneOf and NoneOf. +type acceptablePrimitiveValuesAttributeValidator struct { + acceptableValues []attr.Value + shouldMatch bool +} + +var _ tfsdk.AttributeValidator = (*acceptablePrimitiveValuesAttributeValidator)(nil) + +func (av *acceptablePrimitiveValuesAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av *acceptablePrimitiveValuesAttributeValidator) MarkdownDescription(_ context.Context) string { + if av.shouldMatch { + return fmt.Sprintf("Value must be one of: %q", av.acceptableValues) + } else { + return fmt.Sprintf("Value must be none of: %q", av.acceptableValues) + } + +} + +func (av *acceptablePrimitiveValuesAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + if req.AttributeConfig.IsNull() || req.AttributeConfig.IsUnknown() { + return + } + + var value attr.Value + switch typedAttributeConfig := req.AttributeConfig.(type) { + case types.String, types.Bool, types.Int64, types.Float64, types.Number: + value = typedAttributeConfig + default: + res.Diagnostics.AddAttributeError( + req.AttributePath, + "This validator should be used against primitive types (String, Bool, Number, Int64, Float64).", + "This is always indicative of a bug within the provider.", + ) + return + } + + if av.shouldMatch && !av.isAcceptableValue(value) || //< EITHER should match but it does not + !av.shouldMatch && av.isAcceptableValue(value) { //< OR should not match but it does + res.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( + req.AttributePath, + av.Description(ctx), + value.String(), + )) + } +} + +func (av *acceptablePrimitiveValuesAttributeValidator) isAcceptableValue(v attr.Value) bool { + for _, acceptableV := range av.acceptableValues { + if v.Equal(acceptableV) { + return true + } + } + + return false +} diff --git a/internal/primitivevalidator/none_of.go b/internal/primitivevalidator/none_of.go new file mode 100644 index 00000000..cca7c804 --- /dev/null +++ b/internal/primitivevalidator/none_of.go @@ -0,0 +1,19 @@ +package primitivevalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// NoneOf checks that the value held in the attribute +// is none of the given `unacceptableValues`. +// +// This validator can be used only against primitives like +// `types.String`, `types.Number`, `types.Int64`, +// `types.Float64` and `types.Bool`. +func NoneOf(unacceptableValues ...attr.Value) tfsdk.AttributeValidator { + return &acceptablePrimitiveValuesAttributeValidator{ + acceptableValues: unacceptableValues, + shouldMatch: false, + } +} diff --git a/internal/primitivevalidator/none_of_test.go b/internal/primitivevalidator/none_of_test.go new file mode 100644 index 00000000..c0329660 --- /dev/null +++ b/internal/primitivevalidator/none_of_test.go @@ -0,0 +1,193 @@ +package primitivevalidator_test + +import ( + "context" + "math" + "math/big" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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" +) + +func TestNoneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + objPersonAttrTypes := map[string]attr.Type{ + "Name": types.StringType, + "Age": types.Int64Type, + } + objAttrTypes := map[string]attr.Type{ + "Person": types.ObjectType{ + AttrTypes: objPersonAttrTypes, + }, + "Address": types.StringType, + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.String{Value: "foo"}, + validator: primitivevalidator.NoneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 1, + }, + "simple-mismatch": { + in: types.String{Value: "foz"}, + validator: primitivevalidator.NoneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 0, + }, + "mixed": { + in: types.Float64{Value: 1.234}, + validator: primitivevalidator.NoneOf( + types.String{Value: "foo"}, + types.Int64{Value: 567}, + types.Float64{Value: 1.234}, + ), + expErrors: 1, + }, + "list-not-allowed": { + in: types.List{ + ElemType: types.Int64Type, + Elems: []attr.Value{ + types.Int64{Value: 10}, + types.Int64{Value: 20}, + types.Int64{Value: 30}, + }, + }, + validator: primitivevalidator.NoneOf( + types.Int64{Value: 10}, + types.Int64{Value: 20}, + types.Int64{Value: 30}, + types.Int64{Value: 40}, + types.Int64{Value: 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: primitivevalidator.NoneOf( + types.String{Value: "bob"}, + types.String{Value: "alice"}, + types.String{Value: "john"}, + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 1, + }, + "map-not-allowed": { + in: types.Map{ + ElemType: types.NumberType, + Elems: map[string]attr.Value{ + "one.one": types.Number{Value: big.NewFloat(1.1)}, + "ten.twenty": types.Number{Value: big.NewFloat(10.20)}, + "five.four": types.Number{Value: big.NewFloat(5.4)}, + }, + }, + validator: primitivevalidator.NoneOf( + types.Number{Value: big.NewFloat(1.1)}, + types.Number{Value: big.NewFloat(math.MaxFloat64)}, + types.Number{Value: big.NewFloat(math.SmallestNonzeroFloat64)}, + types.Number{Value: big.NewFloat(10.20)}, + types.Number{Value: big.NewFloat(5.4)}, + ), + expErrors: 1, + }, + "object-not-allowed": { + in: types.Object{ + AttrTypes: objAttrTypes, + Attrs: map[string]attr.Value{ + "Person": types.Object{ + AttrTypes: objPersonAttrTypes, + Attrs: map[string]attr.Value{ + "Name": types.String{Value: "Bob Parr"}, + "Age": types.Int64{Value: 40}, + }, + }, + "Address": types.String{Value: "1200 Park Avenue Emeryville"}, + }, + }, + validator: primitivevalidator.NoneOf( + types.Object{ + AttrTypes: map[string]attr.Type{}, + Attrs: map[string]attr.Value{}, + }, + types.Object{ + AttrTypes: objPersonAttrTypes, + Attrs: map[string]attr.Value{ + "Name": types.String{Value: "Bob Parr"}, + "Age": types.Int64{Value: 40}, + }, + }, + types.String{Value: "1200 Park Avenue Emeryville"}, + types.Int64{Value: 123}, + types.String{Value: "Bob Parr"}, + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.String{Null: true}, + validator: primitivevalidator.NoneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.String{Unknown: true}, + validator: primitivevalidator.NoneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "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) + } + }) + } +} diff --git a/internal/primitivevalidator/one_of.go b/internal/primitivevalidator/one_of.go new file mode 100644 index 00000000..7bacc6d2 --- /dev/null +++ b/internal/primitivevalidator/one_of.go @@ -0,0 +1,19 @@ +package primitivevalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// OneOf checks that the value held in the attribute +// is one of the given `acceptableValues`. +// +// This validator can be used only against primitives like +// `types.String`, `types.Number`, `types.Int64`, +// `types.Float64` and `types.Bool`. +func OneOf(acceptableValues ...attr.Value) tfsdk.AttributeValidator { + return &acceptablePrimitiveValuesAttributeValidator{ + acceptableValues: acceptableValues, + shouldMatch: true, + } +} diff --git a/internal/primitivevalidator/one_of_test.go b/internal/primitivevalidator/one_of_test.go new file mode 100644 index 00000000..d132aff0 --- /dev/null +++ b/internal/primitivevalidator/one_of_test.go @@ -0,0 +1,193 @@ +package primitivevalidator_test + +import ( + "context" + "math" + "math/big" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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" +) + +func TestOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + objPersonAttrTypes := map[string]attr.Type{ + "Name": types.StringType, + "Age": types.Int64Type, + } + objAttrTypes := map[string]attr.Type{ + "Person": types.ObjectType{ + AttrTypes: objPersonAttrTypes, + }, + "Address": types.StringType, + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.String{Value: "foo"}, + validator: primitivevalidator.OneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 0, + }, + "simple-mismatch": { + in: types.String{Value: "foz"}, + validator: primitivevalidator.OneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 1, + }, + "mixed": { + in: types.Float64{Value: 1.234}, + validator: primitivevalidator.OneOf( + types.String{Value: "foo"}, + types.Int64{Value: 567}, + types.Float64{Value: 1.234}, + ), + expErrors: 0, + }, + "list-not-allowed": { + in: types.List{ + ElemType: types.Int64Type, + Elems: []attr.Value{ + types.Int64{Value: 10}, + types.Int64{Value: 20}, + types.Int64{Value: 30}, + }, + }, + validator: primitivevalidator.OneOf( + types.Int64{Value: 10}, + types.Int64{Value: 20}, + types.Int64{Value: 30}, + types.Int64{Value: 40}, + types.Int64{Value: 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: primitivevalidator.OneOf( + types.String{Value: "bob"}, + types.String{Value: "alice"}, + types.String{Value: "john"}, + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 1, + }, + "map-not-allowed": { + in: types.Map{ + ElemType: types.NumberType, + Elems: map[string]attr.Value{ + "one.one": types.Number{Value: big.NewFloat(1.1)}, + "ten.twenty": types.Number{Value: big.NewFloat(10.20)}, + "five.four": types.Number{Value: big.NewFloat(5.4)}, + }, + }, + validator: primitivevalidator.OneOf( + types.Number{Value: big.NewFloat(1.1)}, + types.Number{Value: big.NewFloat(math.MaxFloat64)}, + types.Number{Value: big.NewFloat(math.SmallestNonzeroFloat64)}, + types.Number{Value: big.NewFloat(10.20)}, + types.Number{Value: big.NewFloat(5.4)}, + ), + expErrors: 1, + }, + "object-not-allowed": { + in: types.Object{ + AttrTypes: objAttrTypes, + Attrs: map[string]attr.Value{ + "Person": types.Object{ + AttrTypes: objPersonAttrTypes, + Attrs: map[string]attr.Value{ + "Name": types.String{Value: "Bob Parr"}, + "Age": types.Int64{Value: 40}, + }, + }, + "Address": types.String{Value: "1200 Park Avenue Emeryville"}, + }, + }, + validator: primitivevalidator.OneOf( + types.Object{ + AttrTypes: map[string]attr.Type{}, + Attrs: map[string]attr.Value{}, + }, + types.Object{ + AttrTypes: objPersonAttrTypes, + Attrs: map[string]attr.Value{ + "Name": types.String{Value: "Bob Parr"}, + "Age": types.Int64{Value: 40}, + }, + }, + types.String{Value: "1200 Park Avenue Emeryville"}, + types.Int64{Value: 123}, + types.String{Value: "Bob Parr"}, + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.String{Null: true}, + validator: primitivevalidator.OneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "baz"}, + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.String{Unknown: true}, + validator: primitivevalidator.OneOf( + types.String{Value: "foo"}, + types.String{Value: "bar"}, + types.String{Value: "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) + } + }) + } +} diff --git a/listvalidator/size_at_least.go b/listvalidator/size_at_least.go index 372a3785..f7724e2e 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeAtLeastValidator{} @@ -34,7 +33,7 @@ func (v sizeAtLeastValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } if len(elems) < v.min { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/listvalidator/size_at_most.go b/listvalidator/size_at_most.go index 42bbc8be..eb767290 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeAtMostValidator{} @@ -34,7 +33,7 @@ func (v sizeAtMostValidator) Validate(ctx context.Context, req tfsdk.ValidateAtt } if len(elems) > v.max { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/listvalidator/size_between.go b/listvalidator/size_between.go index 3e3ded12..7b7e0672 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeBetweenValidator{} @@ -36,7 +35,7 @@ func (v sizeBetweenValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } if len(elems) < v.min || len(elems) > v.max { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/mapvalidator/size_at_least.go b/mapvalidator/size_at_least.go index c46918a1..04bb6417 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeAtLeastValidator{} @@ -34,7 +33,7 @@ func (v sizeAtLeastValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } if len(elems) < v.min { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/mapvalidator/size_at_most.go b/mapvalidator/size_at_most.go index 994a8c86..db6f6d75 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeAtMostValidator{} @@ -34,7 +33,7 @@ func (v sizeAtMostValidator) Validate(ctx context.Context, req tfsdk.ValidateAtt } if len(elems) > v.max { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/mapvalidator/size_between.go b/mapvalidator/size_between.go index 96be810f..e7d284e1 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeBetweenValidator{} @@ -36,7 +35,7 @@ func (v sizeBetweenValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } if len(elems) < v.min || len(elems) > v.max { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/numbervalidator/doc.go b/numbervalidator/doc.go new file mode 100644 index 00000000..43f291aa --- /dev/null +++ b/numbervalidator/doc.go @@ -0,0 +1,2 @@ +// Package numbervalidator provides validators for types.Number attributes. +package numbervalidator diff --git a/numbervalidator/none_of.go b/numbervalidator/none_of.go new file mode 100644 index 00000000..0650a16a --- /dev/null +++ b/numbervalidator/none_of.go @@ -0,0 +1,21 @@ +package numbervalidator + +import ( + "math/big" + + "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 *big.Float held in the attribute +// is none of the given `unacceptableFloats`. +func NoneOf(unacceptableFloats ...*big.Float) tfsdk.AttributeValidator { + unacceptableFloatValues := make([]attr.Value, 0, len(unacceptableFloats)) + for _, f := range unacceptableFloats { + unacceptableFloatValues = append(unacceptableFloatValues, types.Number{Value: f}) + } + + return primitivevalidator.NoneOf(unacceptableFloatValues...) +} diff --git a/numbervalidator/none_of_test.go b/numbervalidator/none_of_test.go new file mode 100644 index 00000000..b9eb386f --- /dev/null +++ b/numbervalidator/none_of_test.go @@ -0,0 +1,86 @@ +package numbervalidator_test + +import ( + "context" + "math/big" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.Number{Value: big.NewFloat(123.456)}, + validator: numbervalidator.NoneOf( + big.NewFloat(123.456), + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + expErrors: 1, + }, + "simple-mismatch": { + in: types.Number{Value: big.NewFloat(123.456)}, + validator: numbervalidator.NoneOf( + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + expErrors: 0, + }, + "skip-validation-on-null": { + in: types.Number{Null: true}, + validator: numbervalidator.NoneOf( + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.Number{Unknown: true}, + validator: numbervalidator.NoneOf( + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + 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) + } + }) + } +} diff --git a/numbervalidator/one_of.go b/numbervalidator/one_of.go new file mode 100644 index 00000000..f18fa3ab --- /dev/null +++ b/numbervalidator/one_of.go @@ -0,0 +1,21 @@ +package numbervalidator + +import ( + "math/big" + + "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 *big.Float held in the attribute +// is one of the given `acceptableFloats`. +func OneOf(acceptableFloats ...*big.Float) tfsdk.AttributeValidator { + acceptableFloatValues := make([]attr.Value, 0, len(acceptableFloats)) + for _, f := range acceptableFloats { + acceptableFloatValues = append(acceptableFloatValues, types.Number{Value: f}) + } + + return primitivevalidator.OneOf(acceptableFloatValues...) +} diff --git a/numbervalidator/one_of_test.go b/numbervalidator/one_of_test.go new file mode 100644 index 00000000..f90da2f4 --- /dev/null +++ b/numbervalidator/one_of_test.go @@ -0,0 +1,86 @@ +package numbervalidator_test + +import ( + "context" + "math/big" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in attr.Value + validator tfsdk.AttributeValidator + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.Number{Value: big.NewFloat(123.456)}, + validator: numbervalidator.OneOf( + big.NewFloat(123.456), + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + expErrors: 0, + }, + "simple-mismatch": { + in: types.Number{Value: big.NewFloat(123.456)}, + validator: numbervalidator.OneOf( + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.Number{Null: true}, + validator: numbervalidator.OneOf( + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.Number{Unknown: true}, + validator: numbervalidator.OneOf( + big.NewFloat(234.567), + big.NewFloat(8910.11), + big.NewFloat(1213.1415), + ), + 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) + } + }) + } +} diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go index c3d99e3b..78caea18 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeAtLeastValidator{} @@ -34,7 +33,7 @@ func (v sizeAtLeastValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } if len(elems) < v.min { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go index 8fe1cc91..fe296bdb 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeAtMostValidator{} @@ -34,7 +33,7 @@ func (v sizeAtMostValidator) Validate(ctx context.Context, req tfsdk.ValidateAtt } if len(elems) > v.max { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go index 894d327a..79ea8065 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = sizeBetweenValidator{} @@ -36,7 +35,7 @@ func (v sizeBetweenValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } if len(elems) < v.min || len(elems) > v.max { - resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( req.AttributePath, v.Description(ctx), fmt.Sprintf("%d", len(elems)), diff --git a/stringvalidator/acceptable_strings_validator.go b/stringvalidator/acceptable_strings_validator.go new file mode 100644 index 00000000..7f1e9592 --- /dev/null +++ b/stringvalidator/acceptable_strings_validator.go @@ -0,0 +1,63 @@ +package stringvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// acceptableStringsAttributeValidator is the underlying struct implementing OneOf and NoneOf. +type acceptableStringsAttributeValidator struct { + acceptableStrings []string + caseSensitive bool + shouldMatch bool +} + +var _ tfsdk.AttributeValidator = (*acceptableStringsAttributeValidator)(nil) + +func (av *acceptableStringsAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av *acceptableStringsAttributeValidator) 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) { + value, ok := validateString(ctx, req, res) + if !ok { + return + } + + if av.shouldMatch && !av.isAcceptableValue(value) || //< EITHER should match but it does not + !av.shouldMatch && av.isAcceptableValue(value) { //< OR should not match but it does + res.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( + req.AttributePath, + av.Description(ctx), + value, + )) + } +} + +func (av *acceptableStringsAttributeValidator) 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 + } + } + } + + return false +} diff --git a/stringvalidator/length_at_least.go b/stringvalidator/length_at_least.go index cbbce1f0..f6a2dfdb 100644 --- a/stringvalidator/length_at_least.go +++ b/stringvalidator/length_at_least.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator lengthAtLeastValidator) Validate(ctx context.Context, request tf } if l := len(s); l < validator.minLength { - response.Diagnostics.Append(validatordiag.AttributeValueLengthDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueLengthDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", l), diff --git a/stringvalidator/length_at_least_test.go b/stringvalidator/length_at_least_test.go index 3620c6d6..d6c32769 100644 --- a/stringvalidator/length_at_least_test.go +++ b/stringvalidator/length_at_least_test.go @@ -1,9 +1,10 @@ -package stringvalidator +package stringvalidator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -50,7 +51,7 @@ func TestLengthAtLeastValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - LengthAtLeast(test.minLength).Validate(context.TODO(), request, &response) + stringvalidator.LengthAtLeast(test.minLength).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/stringvalidator/length_at_most.go b/stringvalidator/length_at_most.go index d9e8c28d..96ff04eb 100644 --- a/stringvalidator/length_at_most.go +++ b/stringvalidator/length_at_most.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator lengthAtMostValidator) Validate(ctx context.Context, request tfs } if l := len(s); l > validator.maxLength { - response.Diagnostics.Append(validatordiag.AttributeValueLengthDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueLengthDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", l), diff --git a/stringvalidator/length_at_most_test.go b/stringvalidator/length_at_most_test.go index df337644..ef293c94 100644 --- a/stringvalidator/length_at_most_test.go +++ b/stringvalidator/length_at_most_test.go @@ -1,9 +1,10 @@ -package stringvalidator +package stringvalidator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -50,7 +51,7 @@ func TestLengthAtMostValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - LengthAtMost(test.maxLength).Validate(context.TODO(), request, &response) + stringvalidator.LengthAtMost(test.maxLength).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/stringvalidator/length_between.go b/stringvalidator/length_between.go index 5656103b..7b8e0090 100644 --- a/stringvalidator/length_between.go +++ b/stringvalidator/length_between.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -34,7 +34,7 @@ func (validator lengthBetweenValidator) Validate(ctx context.Context, request tf } if l := len(s); l < validator.minLength || l > validator.maxLength { - response.Diagnostics.Append(validatordiag.AttributeValueLengthDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueLengthDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", l), diff --git a/stringvalidator/length_between_test.go b/stringvalidator/length_between_test.go index a07e0c22..ab9052f0 100644 --- a/stringvalidator/length_between_test.go +++ b/stringvalidator/length_between_test.go @@ -1,9 +1,10 @@ -package stringvalidator +package stringvalidator_test import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -61,7 +62,7 @@ func TestLengthBetweenValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - LengthBetween(test.minLength, test.maxLength).Validate(context.TODO(), request, &response) + stringvalidator.LengthBetween(test.minLength, test.maxLength).Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/stringvalidator/none_of.go b/stringvalidator/none_of.go new file mode 100644 index 00000000..0fbc057a --- /dev/null +++ b/stringvalidator/none_of.go @@ -0,0 +1,17 @@ +package stringvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// 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{ + unacceptableStrings, + caseSensitive, + false, + } +} diff --git a/stringvalidator/none_of_test.go b/stringvalidator/none_of_test.go new file mode 100644 index 00000000..536640cb --- /dev/null +++ b/stringvalidator/none_of_test.go @@ -0,0 +1,180 @@ +package stringvalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoneOfValidator(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.NoneOf( + true, + "foo", + "bar", + "baz", + ), + expErrors: 1, + }, + "simple-match-case-insensitive": { + in: types.String{Value: "foo"}, + validator: stringvalidator.NoneOf( + false, + "FOO", + "bar", + "baz", + ), + expErrors: 1, + }, + "simple-mismatch": { + in: types.String{Value: "foz"}, + validator: stringvalidator.NoneOf( + true, + "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.NoneOf( + true, + "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.NoneOf( + true, + "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.NoneOf( + true, + "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.NoneOf( + true, + "Bob Parr", + "40", + "1200 Park Avenue Emeryville", + "123", + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.String{Null: true}, + validator: stringvalidator.NoneOf( + true, + "foo", + "bar", + "baz", + ), + expErrors: 0, + }, + "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) + } + }) + } +} diff --git a/stringvalidator/one_of.go b/stringvalidator/one_of.go new file mode 100644 index 00000000..d02df037 --- /dev/null +++ b/stringvalidator/one_of.go @@ -0,0 +1,17 @@ +package stringvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// 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{ + acceptableStrings, + caseSensitive, + true, + } +} diff --git a/stringvalidator/one_of_test.go b/stringvalidator/one_of_test.go new file mode 100644 index 00000000..d62b1949 --- /dev/null +++ b/stringvalidator/one_of_test.go @@ -0,0 +1,180 @@ +package stringvalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestOneOfValidator(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.OneOf( + true, + "foo", + "bar", + "baz", + ), + expErrors: 0, + }, + "simple-match-case-insensitive": { + in: types.String{Value: "foo"}, + validator: stringvalidator.OneOf( + false, + "FOO", + "bar", + "baz", + ), + expErrors: 0, + }, + "simple-mismatch": { + in: types.String{Value: "foz"}, + validator: stringvalidator.OneOf( + true, + "foo", + "bar", + "baz", + ), + expErrors: 1, + }, + "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.OneOf( + true, + "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.OneOf( + true, + "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.OneOf( + true, + "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.OneOf( + true, + "Bob Parr", + "40", + "1200 Park Avenue Emeryville", + "123", + ), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.String{Null: true}, + validator: stringvalidator.OneOf( + true, + "foo", + "bar", + "baz", + ), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.String{Unknown: true}, + validator: stringvalidator.OneOf( + 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) + } + }) + } +} diff --git a/stringvalidator/regex_matches.go b/stringvalidator/regex_matches.go index 1d3fbf9f..03b1f51f 100644 --- a/stringvalidator/regex_matches.go +++ b/stringvalidator/regex_matches.go @@ -5,7 +5,7 @@ import ( "fmt" "regexp" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -39,7 +39,7 @@ func (validator regexMatchesValidator) Validate(ctx context.Context, request tfs } if ok := validator.regexp.MatchString(s); !ok { - response.Diagnostics.Append(validatordiag.AttributeValueMatchesDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( request.AttributePath, validator.Description(ctx), s, diff --git a/stringvalidator/regex_matches_test.go b/stringvalidator/regex_matches_test.go index 2f4ce12b..f0516810 100644 --- a/stringvalidator/regex_matches_test.go +++ b/stringvalidator/regex_matches_test.go @@ -1,10 +1,11 @@ -package stringvalidator +package stringvalidator_test import ( "context" "regexp" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -51,7 +52,7 @@ func TestRegexMatchesValidator(t *testing.T) { AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} - RegexMatches(test.regexp, "").Validate(context.TODO(), request, &response) + stringvalidator.RegexMatches(test.regexp, "").Validate(context.TODO(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/stringvalidator/type_validation.go b/stringvalidator/type_validation.go index 2a0ec479..8b8ecf2b 100644 --- a/stringvalidator/type_validation.go +++ b/stringvalidator/type_validation.go @@ -3,22 +3,25 @@ package stringvalidator import ( "context" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) // validateString ensures that the request contains a String value. func validateString(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) (string, bool) { - var s types.String - - diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &s) - - if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) - + t := request.AttributeConfig.Type(ctx) + if t != types.StringType { + response.Diagnostics.Append(validatordiag.InvalidAttributeTypeDiagnostic( + request.AttributePath, + "Expected value of type string", + t.String(), + )) return "", false } + s := request.AttributeConfig.(types.String) + if s.Unknown || s.Null { return "", false } diff --git a/validatordiag/diag.go b/validatordiag/diag.go deleted file mode 100644 index c43567f2..00000000 --- a/validatordiag/diag.go +++ /dev/null @@ -1,47 +0,0 @@ -package validatordiag - -import ( - "unicode" - "unicode/utf8" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -// AttributeValueDiagnostic returns an error Diagnostic to be used when an attribute has an invalid value. -func AttributeValueDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { - return diag.NewAttributeErrorDiagnostic( - path, - "Invalid Attribute Value", - capitalize(description)+", got: "+value, - ) -} - -// AttributeValueLengthDiagnostic returns an error Diagnostic to be used when an attribute's value has an invalid length. -func AttributeValueLengthDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { - return diag.NewAttributeErrorDiagnostic( - path, - "Invalid Attribute Value Length", - capitalize(description)+", got: "+value, - ) -} - -// 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 == "" { - return "" - } - - firstRune, size := utf8.DecodeRuneInString(str) - - return string(unicode.ToUpper(firstRune)) + str[size:] -}