diff --git a/.changelog/21.txt b/.changelog/21.txt new file mode 100644 index 00000000..f603554e --- /dev/null +++ b/.changelog/21.txt @@ -0,0 +1,3 @@ +```release-note:feature +Introduced `int64validator` package with `AtLeast()`, `AtMost()`, and `Between()` validation functions +``` \ No newline at end of file diff --git a/int64validator/at_least.go b/int64validator/at_least.go new file mode 100644 index 00000000..c5938031 --- /dev/null +++ b/int64validator/at_least.go @@ -0,0 +1,58 @@ +package validate + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +var _ tfsdk.AttributeValidator = atLeastValidator{} + +// atLeastValidator validates that an integer Attribute's value is at least a certain value. +type atLeastValidator struct { + min int64 +} + +// Description describes the validation in plain text formatting. +func (validator atLeastValidator) Description(_ context.Context) string { + return fmt.Sprintf("value must be at least %d", validator.min) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator atLeastValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// Validate performs the validation. +func (validator atLeastValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { + i, ok := validateInt(ctx, request, response) + + if !ok { + return + } + + if i < validator.min { + response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + request.AttributePath, + validator.Description(ctx), + fmt.Sprintf("%d", i), + )) + + return + } +} + +// AtLeast returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is exclusively greater than the given minimum. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtLeast(min int64) tfsdk.AttributeValidator { + return atLeastValidator{ + min: min, + } +} diff --git a/int64validator/at_least_test.go b/int64validator/at_least_test.go new file mode 100644 index 00000000..4db0c898 --- /dev/null +++ b/int64validator/at_least_test.go @@ -0,0 +1,68 @@ +package validate + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtLeastValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + min int64 + expectError bool + } + tests := map[string]testCase{ + "not an Int64": { + val: types.Bool{Value: true}, + expectError: true, + }, + "unknown Int64": { + val: types.Int64{Unknown: true}, + min: 1, + }, + "null Int64": { + val: types.Int64{Null: true}, + min: 1, + }, + "valid integer as Int64": { + val: types.Int64{Value: 2}, + min: 1, + }, + "valid integer as Int64 min": { + val: types.Int64{Value: 1}, + min: 1, + }, + "too small integer as Int64": { + val: types.Int64{Value: -1}, + min: 1, + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + AtLeast(test.min).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/int64validator/at_most.go b/int64validator/at_most.go new file mode 100644 index 00000000..14f0a1eb --- /dev/null +++ b/int64validator/at_most.go @@ -0,0 +1,78 @@ +package validate + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ tfsdk.AttributeValidator = atMostValidator{} + +// atMostValidator validates that an integer Attribute's value is at most a certain value. +type atMostValidator struct { + max int64 +} + +// Description describes the validation in plain text formatting. +func (validator atMostValidator) Description(_ context.Context) string { + return fmt.Sprintf("value must be at most %d", validator.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator atMostValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// Validate performs the validation. +func (validator atMostValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { + i, ok := validateInt(ctx, request, response) + + if !ok { + return + } + + if i > validator.max { + response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + request.AttributePath, + validator.Description(ctx), + fmt.Sprintf("%d", i), + )) + + return + } +} + +// AtMost returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is exclusively less than the given maximum. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtMost(max int64) tfsdk.AttributeValidator { + return atMostValidator{ + max: max, + } +} + +// 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...) + + return 0, false + } + + if n.Unknown || n.Null { + return 0, false + } + + return n.Value, true +} diff --git a/int64validator/at_most_test.go b/int64validator/at_most_test.go new file mode 100644 index 00000000..8aa327de --- /dev/null +++ b/int64validator/at_most_test.go @@ -0,0 +1,68 @@ +package validate + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtMostValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + max int64 + expectError bool + } + tests := map[string]testCase{ + "not an Int64": { + val: types.Bool{Value: true}, + expectError: true, + }, + "unknown Int64": { + val: types.Int64{Unknown: true}, + max: 2, + }, + "null Int64": { + val: types.Int64{Null: true}, + max: 2, + }, + "valid integer as Int64": { + val: types.Int64{Value: 1}, + max: 2, + }, + "valid integer as Int64 min": { + val: types.Int64{Value: 2}, + max: 2, + }, + "too large integer as Int64": { + val: types.Int64{Value: 4}, + max: 2, + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + AtMost(test.max).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/int64validator/between.go b/int64validator/between.go new file mode 100644 index 00000000..e8fd7d02 --- /dev/null +++ b/int64validator/between.go @@ -0,0 +1,63 @@ +package validate + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +var _ tfsdk.AttributeValidator = betweenValidator{} + +// betweenValidator validates that an integer Attribute's value is in a range. +type betweenValidator struct { + min, max int64 +} + +// Description describes the validation in plain text formatting. +func (validator betweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("value must be between %d and %d", validator.min, validator.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator betweenValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// Validate performs the validation. +func (validator betweenValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { + i, ok := validateInt(ctx, request, response) + + if !ok { + return + } + + if i < validator.min || i > validator.max { + response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + request.AttributePath, + validator.Description(ctx), + fmt.Sprintf("%d", i), + )) + + return + } +} + +// Between returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is exclusively greater than the given minimum and less than the given maximum. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func Between(min, max int64) tfsdk.AttributeValidator { + if min > max { + return nil + } + + return betweenValidator{ + min: min, + max: max, + } +} diff --git a/int64validator/between_test.go b/int64validator/between_test.go new file mode 100644 index 00000000..e9106b76 --- /dev/null +++ b/int64validator/between_test.go @@ -0,0 +1,85 @@ +package validate + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestBetweenValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + min int64 + max int64 + expectError bool + } + tests := map[string]testCase{ + "not an Int64": { + val: types.Bool{Value: true}, + expectError: true, + }, + "unknown Int64": { + val: types.Int64{Unknown: true}, + min: 1, + max: 3, + }, + "null Int64": { + val: types.Int64{Null: true}, + min: 1, + max: 3, + }, + "valid integer as Int64": { + val: types.Int64{Value: 2}, + min: 1, + max: 3, + }, + "valid integer as Int64 min": { + val: types.Int64{Value: 1}, + min: 1, + max: 3, + }, + "valid integer as Int64 max": { + val: types.Int64{Value: 3}, + min: 1, + max: 3, + }, + "too small integer as Int64": { + val: types.Int64{Value: -1}, + min: 1, + max: 3, + expectError: true, + }, + "too large integer as Int64": { + val: types.Int64{Value: 42}, + min: 1, + max: 3, + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + Between(test.min, test.max).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +}