diff --git a/helpers/validatordiag/diag.go b/helpers/validatordiag/diag.go index 66db1417..60ca1e79 100644 --- a/helpers/validatordiag/diag.go +++ b/helpers/validatordiag/diag.go @@ -9,6 +9,15 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" ) +// InvalidBlockDiagnostic returns an error Diagnostic to be used when a block is invalid +func InvalidBlockDiagnostic(path path.Path, description string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Block", + fmt.Sprintf("Block %s %s", path, description), + ) +} + // InvalidAttributeValueDiagnostic returns an error Diagnostic to be used when an attribute has an invalid value. func InvalidAttributeValueDiagnostic(path path.Path, description string, value string) diag.Diagnostic { return diag.NewAttributeErrorDiagnostic( diff --git a/listvalidator/is_required.go b/listvalidator/is_required.go new file mode 100644 index 00000000..c97e67c6 --- /dev/null +++ b/listvalidator/is_required.go @@ -0,0 +1,41 @@ +package listvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = isRequiredValidator{} + +// isRequiredValidator validates that a list has a configuration value. +type isRequiredValidator struct{} + +// Description describes the validation in plain text formatting. +func (v isRequiredValidator) Description(_ context.Context) string { + return "must have a configuration value as the provider has marked it as required" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v isRequiredValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() { + resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic( + req.Path, + v.Description(ctx), + )) + } +} + +// IsRequired returns a validator which ensures that any configured list has a value (not null). +// +// This validator is equivalent to the `Required` field on attributes and is only +// practical for use with `schema.ListNestedBlock` +func IsRequired() validator.List { + return isRequiredValidator{} +} diff --git a/listvalidator/is_required_example_test.go b/listvalidator/is_required_example_test.go new file mode 100644 index 00000000..36d41c33 --- /dev/null +++ b/listvalidator/is_required_example_test.go @@ -0,0 +1,28 @@ +package listvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleIsRequired() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Blocks: map[string]schema.Block{ + "example_block": schema.ListNestedBlock{ + Validators: []validator.List{ + // Validate this block has a value (not null). + listvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "example_string_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + } +} diff --git a/listvalidator/is_required_test.go b/listvalidator/is_required_test.go new file mode 100644 index 00000000..b549f3d9 --- /dev/null +++ b/listvalidator/is_required_test.go @@ -0,0 +1,73 @@ +package listvalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestIsRequiredValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.List + expectError bool + } + tests := map[string]testCase{ + "List null": { + val: types.ListNull( + types.StringType, + ), + expectError: true, + }, + "List unknown": { + val: types.ListUnknown( + types.StringType, + ), + expectError: false, + }, + "List empty": { + val: types.ListValueMust( + types.StringType, + []attr.Value{}, + ), + expectError: false, + }, + "List with elements": { + val: types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + }, + ), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + request := validator.ListRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.ListResponse{} + listvalidator.IsRequired().ValidateList(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/objectvalidator/is_required.go b/objectvalidator/is_required.go new file mode 100644 index 00000000..e074fd35 --- /dev/null +++ b/objectvalidator/is_required.go @@ -0,0 +1,41 @@ +package objectvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Object = isRequiredValidator{} + +// isRequiredValidator validates that an object has a configuration value. +type isRequiredValidator struct{} + +// Description describes the validation in plain text formatting. +func (v isRequiredValidator) Description(_ context.Context) string { + return "must have a configuration value as the provider has marked it as required" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v isRequiredValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if req.ConfigValue.IsNull() { + resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic( + req.Path, + v.Description(ctx), + )) + } +} + +// IsRequired returns a validator which ensures that any configured object has a value (not null). +// +// This validator is equivalent to the `Required` field on attributes and is only +// practical for use with `schema.SingleNestedBlock` +func IsRequired() validator.Object { + return isRequiredValidator{} +} diff --git a/objectvalidator/is_required_example_test.go b/objectvalidator/is_required_example_test.go new file mode 100644 index 00000000..6b049cc3 --- /dev/null +++ b/objectvalidator/is_required_example_test.go @@ -0,0 +1,26 @@ +package objectvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleIsRequired() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Blocks: map[string]schema.Block{ + "example_block": schema.SingleNestedBlock{ + Validators: []validator.Object{ + // Validate this block has a value (not null). + objectvalidator.IsRequired(), + }, + Attributes: map[string]schema.Attribute{ + "example_string_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + } +} diff --git a/objectvalidator/is_required_test.go b/objectvalidator/is_required_test.go new file mode 100644 index 00000000..e4fd7bd3 --- /dev/null +++ b/objectvalidator/is_required_test.go @@ -0,0 +1,83 @@ +package objectvalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestIsRequiredValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.Object + expectError bool + } + tests := map[string]testCase{ + "Object null": { + val: types.ObjectNull( + map[string]attr.Type{ + "field1": types.StringType, + }, + ), + expectError: true, + }, + "Object unknown": { + val: types.ObjectUnknown( + map[string]attr.Type{ + "field1": types.StringType, + }, + ), + expectError: false, + }, + "Object empty": { + val: types.ObjectValueMust( + map[string]attr.Type{ + "field1": types.StringType, + }, + map[string]attr.Value{ + "field1": types.StringNull(), + }, + ), + expectError: false, + }, + "Object with elements": { + val: types.ObjectValueMust( + map[string]attr.Type{ + "field1": types.StringType, + }, + map[string]attr.Value{ + "field1": types.StringValue("value1"), + }, + ), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + request := validator.ObjectRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.ObjectResponse{} + objectvalidator.IsRequired().ValidateObject(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/setvalidator/is_required.go b/setvalidator/is_required.go new file mode 100644 index 00000000..d831563c --- /dev/null +++ b/setvalidator/is_required.go @@ -0,0 +1,41 @@ +package setvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Set = isRequiredValidator{} + +// isRequiredValidator validates that a set has a configuration value. +type isRequiredValidator struct{} + +// Description describes the validation in plain text formatting. +func (v isRequiredValidator) Description(_ context.Context) string { + return "must have a configuration value as the provider has marked it as required" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v isRequiredValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if req.ConfigValue.IsNull() { + resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic( + req.Path, + v.Description(ctx), + )) + } +} + +// IsRequired returns a validator which ensures that any configured set has a value (not null). +// +// This validator is equivalent to the `Required` field on attributes and is only +// practical for use with `schema.SetNestedBlock` +func IsRequired() validator.Set { + return isRequiredValidator{} +} diff --git a/setvalidator/is_required_example_test.go b/setvalidator/is_required_example_test.go new file mode 100644 index 00000000..7447cb3b --- /dev/null +++ b/setvalidator/is_required_example_test.go @@ -0,0 +1,28 @@ +package setvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleIsRequired() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Blocks: map[string]schema.Block{ + "example_block": schema.SetNestedBlock{ + Validators: []validator.Set{ + // Validate this block has a value (not null). + setvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "example_string_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + } +} diff --git a/setvalidator/is_required_test.go b/setvalidator/is_required_test.go new file mode 100644 index 00000000..b5e8356f --- /dev/null +++ b/setvalidator/is_required_test.go @@ -0,0 +1,73 @@ +package setvalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestIsRequiredValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.Set + expectError bool + } + tests := map[string]testCase{ + "Set null": { + val: types.SetNull( + types.StringType, + ), + expectError: true, + }, + "Set unknown": { + val: types.SetUnknown( + types.StringType, + ), + expectError: false, + }, + "Set empty": { + val: types.SetValueMust( + types.StringType, + []attr.Value{}, + ), + expectError: false, + }, + "Set with elements": { + val: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + }, + ), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + request := validator.SetRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.SetResponse{} + setvalidator.IsRequired().ValidateSet(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) + } + }) + } +}