From 348962f2e117cf70f8a9a05d5f0d3c383c7339b3 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 16 Aug 2023 12:55:48 -0400 Subject: [PATCH 1/8] Add `All`, `Any`, and `AnyWithAllWarnings` validators to `boolvalidator` package --- boolvalidator/all.go | 54 ++++++++++++++++ boolvalidator/all_example_test.go | 47 ++++++++++++++ boolvalidator/any.go | 62 ++++++++++++++++++ boolvalidator/any_example_test.go | 39 +++++++++++ boolvalidator/any_with_all_warnings.go | 64 +++++++++++++++++++ .../any_with_all_warnings_example_test.go | 39 +++++++++++ 6 files changed, 305 insertions(+) create mode 100644 boolvalidator/all.go create mode 100644 boolvalidator/all_example_test.go create mode 100644 boolvalidator/any.go create mode 100644 boolvalidator/any_example_test.go create mode 100644 boolvalidator/any_with_all_warnings.go create mode 100644 boolvalidator/any_with_all_warnings_example_test.go diff --git a/boolvalidator/all.go b/boolvalidator/all.go new file mode 100644 index 0000000..650887d --- /dev/null +++ b/boolvalidator/all.go @@ -0,0 +1,54 @@ +package boolvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// All returns a validator which ensures that any configured attribute value +// validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...validator.Bool) validator.Bool { + return allValidator{ + validators: validators, + } +} + +var _ validator.Bool = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []validator.Bool +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateBool performs the validation. +func (v allValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + for _, subValidator := range v.validators { + validateResp := &validator.BoolResponse{} + + subValidator.ValidateBool(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/boolvalidator/all_example_test.go b/boolvalidator/all_example_test.go new file mode 100644 index 0000000..8333a9a --- /dev/null +++ b/boolvalidator/all_example_test.go @@ -0,0 +1,47 @@ +package boolvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" +) + +func ExampleAll() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.BoolAttribute{ + Required: true, + Validators: []validator.Bool{ + // Validate that this attribute must either: + // - be set with other_attrA or + // - be set with other_attrB AND other_attrC + boolvalidator.Any( + boolvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("other_attrA"), + }...), + boolvalidator.All( + boolvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attrB"), + }...), + boolvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attrC"), + }...), + ), + ), + }, + }, + "other_attrA": schema.StringAttribute{ + Optional: true, + }, + "other_attrB": schema.StringAttribute{ + Optional: true, + }, + "other_attrC": schema.StringAttribute{ + Optional: true, + }, + }, + } +} diff --git a/boolvalidator/any.go b/boolvalidator/any.go new file mode 100644 index 0000000..e775020 --- /dev/null +++ b/boolvalidator/any.go @@ -0,0 +1,62 @@ +package boolvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// Any returns a validator which ensures that any configured attribute value +// passes at least one of the given validators. +// +// To prevent practitioner confusion should non-passing validators have +// conflicting logic, only warnings from the passing validator are returned. +// Use AnyWithAllWarnings() to return warnings from non-passing validators +// as well. +func Any(validators ...validator.Bool) validator.Bool { + return anyValidator{ + validators: validators, + } +} + +var _ validator.Bool = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []validator.Bool +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateBool performs the validation. +func (v anyValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + for _, subValidator := range v.validators { + validateResp := &validator.BoolResponse{} + + subValidator.ValidateBool(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/boolvalidator/any_example_test.go b/boolvalidator/any_example_test.go new file mode 100644 index 0000000..9eaafd1 --- /dev/null +++ b/boolvalidator/any_example_test.go @@ -0,0 +1,39 @@ +package boolvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" +) + +func ExampleAny() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.BoolAttribute{ + Required: true, + Validators: []validator.Bool{ + // Validate that this attribute must either: + // - be set with other_attrA or + // - be set with other_attrB + boolvalidator.Any( + boolvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attrA"), + }...), + boolvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attrB"), + }...), + ), + }, + }, + "other_attrA": schema.StringAttribute{ + Optional: true, + }, + "other_attrB": schema.StringAttribute{ + Optional: true, + }, + }, + } +} diff --git a/boolvalidator/any_with_all_warnings.go b/boolvalidator/any_with_all_warnings.go new file mode 100644 index 0000000..7fea7b8 --- /dev/null +++ b/boolvalidator/any_with_all_warnings.go @@ -0,0 +1,64 @@ +package boolvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// AnyWithAllWarnings returns a validator which ensures that any configured +// attribute value passes at least one of the given validators. This validator +// returns all warnings, including failed validators. +// +// Use Any() to return warnings only from the passing validator. +func AnyWithAllWarnings(validators ...validator.Bool) validator.Bool { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ validator.Bool = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []validator.Bool +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateBool performs the validation. +func (v anyWithAllWarningsValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &validator.BoolResponse{} + + subValidator.ValidateBool(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/boolvalidator/any_with_all_warnings_example_test.go b/boolvalidator/any_with_all_warnings_example_test.go new file mode 100644 index 0000000..b65a2c9 --- /dev/null +++ b/boolvalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,39 @@ +package boolvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" +) + +func ExampleAnyWithAllWarnings() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.BoolAttribute{ + Required: true, + Validators: []validator.Bool{ + // Validate that this attribute must either: + // - be set with other_attrA or + // - be set with other_attrB + boolvalidator.AnyWithAllWarnings( + boolvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attrA"), + }...), + boolvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attrB"), + }...), + ), + }, + }, + "other_attrA": schema.StringAttribute{ + Optional: true, + }, + "other_attrB": schema.StringAttribute{ + Optional: true, + }, + }, + } +} From e501b78cd07d6b27d3e4df714514c50d6027b34d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 16 Aug 2023 12:56:44 -0400 Subject: [PATCH 2/8] Add `All`, `Any`, and `AnyWithAllWarnings` validators to `datasourcevalidator` package --- datasourcevalidator/all.go | 54 +++++ datasourcevalidator/all_example_test.go | 36 +++ datasourcevalidator/all_test.go | 175 ++++++++++++++ datasourcevalidator/any.go | 62 +++++ datasourcevalidator/any_example_test.go | 29 +++ datasourcevalidator/any_test.go | 152 +++++++++++++ datasourcevalidator/any_with_all_warnings.go | 64 ++++++ .../any_with_all_warnings_example_test.go | 29 +++ .../any_with_all_warnings_test.go | 213 ++++++++++++++++++ internal/testvalidator/warning.go | 32 ++- 10 files changed, 837 insertions(+), 9 deletions(-) create mode 100644 datasourcevalidator/all.go create mode 100644 datasourcevalidator/all_example_test.go create mode 100644 datasourcevalidator/all_test.go create mode 100644 datasourcevalidator/any.go create mode 100644 datasourcevalidator/any_example_test.go create mode 100644 datasourcevalidator/any_test.go create mode 100644 datasourcevalidator/any_with_all_warnings.go create mode 100644 datasourcevalidator/any_with_all_warnings_example_test.go create mode 100644 datasourcevalidator/any_with_all_warnings_test.go diff --git a/datasourcevalidator/all.go b/datasourcevalidator/all.go new file mode 100644 index 0000000..64d2c78 --- /dev/null +++ b/datasourcevalidator/all.go @@ -0,0 +1,54 @@ +package datasourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +// All returns a validator which ensures that any configured attribute value +// validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...datasource.ConfigValidator) datasource.ConfigValidator { + return allValidator{ + validators: validators, + } +} + +var _ datasource.ConfigValidator = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []datasource.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateDataSource performs the validation. +func (v allValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &datasource.ValidateConfigResponse{} + + subValidator.ValidateDataSource(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/datasourcevalidator/all_example_test.go b/datasourcevalidator/all_example_test.go new file mode 100644 index 0000000..e107b6c --- /dev/null +++ b/datasourcevalidator/all_example_test.go @@ -0,0 +1,36 @@ +package datasourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" +) + +func ExampleAll() { + // Used inside a datasource.DataSource type ConfigValidators method + _ = []datasource.ConfigValidator{ + // Validate that the configuration has either: + // - only one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value OR + // - at least one of the schema defined attributes named attr3 and attr4 + // has a known, non-null value AND attr3 and attr5 are not both configured + // with known, non-null values. + datasourcevalidator.Any( + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + datasourcevalidator.All( + datasourcevalidator.AtLeastOneOf( + path.MatchRoot("attr3"), + path.MatchRoot("attr4"), + ), + datasourcevalidator.Conflicting( + path.MatchRoot("attr3"), + path.MatchRoot("attr5"), + ), + ), + ), + } +} diff --git a/datasourcevalidator/all_test.go b/datasourcevalidator/all_test.go new file mode 100644 index 0000000..090cca1 --- /dev/null +++ b/datasourcevalidator/all_test.go @@ -0,0 +1,175 @@ +package datasourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" +) + +func TestAllValidatorValidateDataSource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []datasource.ConfigValidator + req datasource.ValidateConfigRequest + expected *datasource.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + datasourcevalidator.All( + datasourcevalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + datasourcevalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + datasourcevalidator.All( + datasourcevalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + datasourcevalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.WithPath(path.Root("test3"), + diag.NewErrorDiagnostic( + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test3,test5]", + )), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &datasource.ValidateConfigResponse{} + + datasourcevalidator.Any(testCase.validators...).ValidateDataSource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasourcevalidator/any.go b/datasourcevalidator/any.go new file mode 100644 index 0000000..2f5115f --- /dev/null +++ b/datasourcevalidator/any.go @@ -0,0 +1,62 @@ +package datasourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +// Any returns a validator which ensures that any configured attribute value +// passes at least one of the given validators. +// +// To prevent practitioner confusion should non-passing validators have +// conflicting logic, only warnings from the passing validator are returned. +// Use AnyWithAllWarnings() to return warnings from non-passing validators +// as well. +func Any(validators ...datasource.ConfigValidator) datasource.ConfigValidator { + return anyValidator{ + validators: validators, + } +} + +var _ datasource.ConfigValidator = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []datasource.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateDataSource performs the validation. +func (v anyValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &datasource.ValidateConfigResponse{} + + subValidator.ValidateDataSource(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/datasourcevalidator/any_example_test.go b/datasourcevalidator/any_example_test.go new file mode 100644 index 0000000..01c3006 --- /dev/null +++ b/datasourcevalidator/any_example_test.go @@ -0,0 +1,29 @@ +package datasourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" +) + +func ExampleAny() { + // Used inside a datasource.DataSource type ConfigValidators method + _ = []datasource.ConfigValidator{ + // Validate that the configuration has either: + // - only one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value OR + // - only one of the schema defined attributes named attr3 + // and attr4 has a known, non-null value + datasourcevalidator.Any( + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("attr3"), + path.MatchRoot("attr4"), + ), + ), + } +} diff --git a/datasourcevalidator/any_test.go b/datasourcevalidator/any_test.go new file mode 100644 index 0000000..746dc7c --- /dev/null +++ b/datasourcevalidator/any_test.go @@ -0,0 +1,152 @@ +package datasourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" +) + +func TestAnyValidatorValidateDataSource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []datasource.ConfigValidator + req datasource.ValidateConfigRequest + expected *datasource.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &datasource.ValidateConfigResponse{} + + datasourcevalidator.Any(testCase.validators...).ValidateDataSource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasourcevalidator/any_with_all_warnings.go b/datasourcevalidator/any_with_all_warnings.go new file mode 100644 index 0000000..d13bd49 --- /dev/null +++ b/datasourcevalidator/any_with_all_warnings.go @@ -0,0 +1,64 @@ +package datasourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +// AnyWithAllWarnings returns a validator which ensures that any configured +// attribute value passes at least one of the given validators. This validator +// returns all warnings, including failed validators. +// +// Use Any() to return warnings only from the passing validator. +func AnyWithAllWarnings(validators ...datasource.ConfigValidator) datasource.ConfigValidator { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ datasource.ConfigValidator = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []datasource.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateDataSource performs the validation. +func (v anyWithAllWarningsValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &datasource.ValidateConfigResponse{} + + subValidator.ValidateDataSource(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/datasourcevalidator/any_with_all_warnings_example_test.go b/datasourcevalidator/any_with_all_warnings_example_test.go new file mode 100644 index 0000000..768cb0c --- /dev/null +++ b/datasourcevalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,29 @@ +package datasourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" +) + +func ExampleAnyWithAllWarnings() { + // Used inside a datasource.DataSource type ConfigValidators method + _ = []datasource.ConfigValidator{ + // Validate that the configuration has either: + // - only one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value OR + // - only one of the schema defined attributes named attr3 + // and attr4 has a known, non-null value + datasourcevalidator.AnyWithAllWarnings( + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("attr3"), + path.MatchRoot("attr4"), + ), + ), + } +} diff --git a/datasourcevalidator/any_with_all_warnings_test.go b/datasourcevalidator/any_with_all_warnings_test.go new file mode 100644 index 0000000..ee3f9b0 --- /dev/null +++ b/datasourcevalidator/any_with_all_warnings_test.go @@ -0,0 +1,213 @@ +package datasourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/internal/testvalidator" +) + +func TestAnyWithAllWarningsValidatorValidateDataSource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []datasource.ConfigValidator + req datasource.ValidateConfigRequest + expected *datasource.ValidateConfigResponse + }{ + "valid": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{}, + }, + "valid with warning": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.All( + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + testvalidator.WarningDataSource("failing warning summary", "failing warning details"), + ), + datasourcevalidator.All( + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + testvalidator.WarningDataSource("passing warning summary", "passing warning details"), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("failing warning summary", "failing warning details"), + diag.NewWarningDiagnostic("passing warning summary", "passing warning details"), + }, + }, + }, + "invalid": { + validators: []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: datasource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &datasource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &datasource.ValidateConfigResponse{} + + datasourcevalidator.AnyWithAllWarnings(testCase.validators...).ValidateDataSource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/testvalidator/warning.go b/internal/testvalidator/warning.go index 1710f70..34f08e3 100644 --- a/internal/testvalidator/warning.go +++ b/internal/testvalidator/warning.go @@ -6,6 +6,7 @@ package testvalidator import ( "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -17,6 +18,14 @@ func WarningBool(summary string, detail string) validator.Bool { } } +// WarningDataSource returns a validator which returns a warning diagnostic. +func WarningDataSource(summary string, detail string) datasource.ConfigValidator { + return WarningValidator{ + Summary: summary, + Detail: detail, + } +} + // WarningFloat64 returns a validator which returns a warning diagnostic. func WarningFloat64(summary string, detail string) validator.Float64 { return WarningValidator{ @@ -82,15 +91,16 @@ func WarningString(summary string, detail string) validator.String { } var ( - _ validator.Bool = WarningValidator{} - _ validator.Float64 = WarningValidator{} - _ validator.Int64 = WarningValidator{} - _ validator.List = WarningValidator{} - _ validator.Map = WarningValidator{} - _ validator.Number = WarningValidator{} - _ validator.Object = WarningValidator{} - _ validator.Set = WarningValidator{} - _ validator.String = WarningValidator{} + _ validator.Bool = WarningValidator{} + _ datasource.ConfigValidator = WarningValidator{} + _ validator.Float64 = WarningValidator{} + _ validator.Int64 = WarningValidator{} + _ validator.List = WarningValidator{} + _ validator.Map = WarningValidator{} + _ validator.Number = WarningValidator{} + _ validator.Object = WarningValidator{} + _ validator.Set = WarningValidator{} + _ validator.String = WarningValidator{} ) type WarningValidator struct { @@ -110,6 +120,10 @@ func (v WarningValidator) ValidateBool(ctx context.Context, request validator.Bo response.Diagnostics.AddWarning(v.Summary, v.Detail) } +func (v WarningValidator) ValidateDataSource(ctx context.Context, request datasource.ValidateConfigRequest, response *datasource.ValidateConfigResponse) { + response.Diagnostics.AddWarning(v.Summary, v.Detail) +} + func (v WarningValidator) ValidateFloat64(ctx context.Context, request validator.Float64Request, response *validator.Float64Response) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } From 120a0a69323f952ff5f8b72e28f3c6053b325118 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 17 Aug 2023 17:21:23 -0400 Subject: [PATCH 3/8] Simplify example tests --- boolvalidator/all_example_test.go | 27 +++---------------- boolvalidator/any_example_test.go | 19 +------------ .../any_with_all_warnings_example_test.go | 19 +------------ datasourcevalidator/all_example_test.go | 27 +++++-------------- datasourcevalidator/any_example_test.go | 17 +----------- .../any_with_all_warnings_example_test.go | 17 +----------- 6 files changed, 13 insertions(+), 113 deletions(-) diff --git a/boolvalidator/all_example_test.go b/boolvalidator/all_example_test.go index 8333a9a..5b09cc7 100644 --- a/boolvalidator/all_example_test.go +++ b/boolvalidator/all_example_test.go @@ -2,7 +2,6 @@ package boolvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" @@ -15,33 +14,13 @@ func ExampleAll() { "example_attr": schema.BoolAttribute{ Required: true, Validators: []validator.Bool{ - // Validate that this attribute must either: - // - be set with other_attrA or - // - be set with other_attrB AND other_attrC + // This attribute must satisfy either All validator. boolvalidator.Any( - boolvalidator.AtLeastOneOf(path.Expressions{ - path.MatchRoot("other_attrA"), - }...), - boolvalidator.All( - boolvalidator.AlsoRequires(path.Expressions{ - path.MatchRoot("other_attrB"), - }...), - boolvalidator.AlsoRequires(path.Expressions{ - path.MatchRoot("other_attrC"), - }...), - ), + boolvalidator.All( /* ... */ ), + boolvalidator.All( /* ... */ ), ), }, }, - "other_attrA": schema.StringAttribute{ - Optional: true, - }, - "other_attrB": schema.StringAttribute{ - Optional: true, - }, - "other_attrC": schema.StringAttribute{ - Optional: true, - }, }, } } diff --git a/boolvalidator/any_example_test.go b/boolvalidator/any_example_test.go index 9eaafd1..7ad1b23 100644 --- a/boolvalidator/any_example_test.go +++ b/boolvalidator/any_example_test.go @@ -2,7 +2,6 @@ package boolvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" @@ -15,25 +14,9 @@ func ExampleAny() { "example_attr": schema.BoolAttribute{ Required: true, Validators: []validator.Bool{ - // Validate that this attribute must either: - // - be set with other_attrA or - // - be set with other_attrB - boolvalidator.Any( - boolvalidator.AlsoRequires(path.Expressions{ - path.MatchRoot("other_attrA"), - }...), - boolvalidator.AlsoRequires(path.Expressions{ - path.MatchRoot("other_attrB"), - }...), - ), + boolvalidator.Any( /* ... */ ), }, }, - "other_attrA": schema.StringAttribute{ - Optional: true, - }, - "other_attrB": schema.StringAttribute{ - Optional: true, - }, }, } } diff --git a/boolvalidator/any_with_all_warnings_example_test.go b/boolvalidator/any_with_all_warnings_example_test.go index b65a2c9..befeda8 100644 --- a/boolvalidator/any_with_all_warnings_example_test.go +++ b/boolvalidator/any_with_all_warnings_example_test.go @@ -2,7 +2,6 @@ package boolvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" @@ -15,25 +14,9 @@ func ExampleAnyWithAllWarnings() { "example_attr": schema.BoolAttribute{ Required: true, Validators: []validator.Bool{ - // Validate that this attribute must either: - // - be set with other_attrA or - // - be set with other_attrB - boolvalidator.AnyWithAllWarnings( - boolvalidator.AlsoRequires(path.Expressions{ - path.MatchRoot("other_attrA"), - }...), - boolvalidator.AlsoRequires(path.Expressions{ - path.MatchRoot("other_attrB"), - }...), - ), + boolvalidator.AnyWithAllWarnings( /* ... */ ), }, }, - "other_attrA": schema.StringAttribute{ - Optional: true, - }, - "other_attrB": schema.StringAttribute{ - Optional: true, - }, }, } } diff --git a/datasourcevalidator/all_example_test.go b/datasourcevalidator/all_example_test.go index e107b6c..0a93cb8 100644 --- a/datasourcevalidator/all_example_test.go +++ b/datasourcevalidator/all_example_test.go @@ -1,8 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" ) @@ -10,27 +12,10 @@ import ( func ExampleAll() { // Used inside a datasource.DataSource type ConfigValidators method _ = []datasource.ConfigValidator{ - // Validate that the configuration has either: - // - only one of the schema defined attributes named attr1 - // and attr2 has a known, non-null value OR - // - at least one of the schema defined attributes named attr3 and attr4 - // has a known, non-null value AND attr3 and attr5 are not both configured - // with known, non-null values. + // The configuration must satisfy either All validator. datasourcevalidator.Any( - datasourcevalidator.ExactlyOneOf( - path.MatchRoot("attr1"), - path.MatchRoot("attr2"), - ), - datasourcevalidator.All( - datasourcevalidator.AtLeastOneOf( - path.MatchRoot("attr3"), - path.MatchRoot("attr4"), - ), - datasourcevalidator.Conflicting( - path.MatchRoot("attr3"), - path.MatchRoot("attr5"), - ), - ), + datasourcevalidator.All( /* ... */ ), + datasourcevalidator.All( /* ... */ ), ), } } diff --git a/datasourcevalidator/any_example_test.go b/datasourcevalidator/any_example_test.go index 01c3006..a376e0a 100644 --- a/datasourcevalidator/any_example_test.go +++ b/datasourcevalidator/any_example_test.go @@ -2,7 +2,6 @@ package datasourcevalidator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" ) @@ -10,20 +9,6 @@ import ( func ExampleAny() { // Used inside a datasource.DataSource type ConfigValidators method _ = []datasource.ConfigValidator{ - // Validate that the configuration has either: - // - only one of the schema defined attributes named attr1 - // and attr2 has a known, non-null value OR - // - only one of the schema defined attributes named attr3 - // and attr4 has a known, non-null value - datasourcevalidator.Any( - datasourcevalidator.ExactlyOneOf( - path.MatchRoot("attr1"), - path.MatchRoot("attr2"), - ), - datasourcevalidator.ExactlyOneOf( - path.MatchRoot("attr3"), - path.MatchRoot("attr4"), - ), - ), + datasourcevalidator.Any( /* ... */ ), } } diff --git a/datasourcevalidator/any_with_all_warnings_example_test.go b/datasourcevalidator/any_with_all_warnings_example_test.go index 768cb0c..f07d10b 100644 --- a/datasourcevalidator/any_with_all_warnings_example_test.go +++ b/datasourcevalidator/any_with_all_warnings_example_test.go @@ -2,7 +2,6 @@ package datasourcevalidator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" ) @@ -10,20 +9,6 @@ import ( func ExampleAnyWithAllWarnings() { // Used inside a datasource.DataSource type ConfigValidators method _ = []datasource.ConfigValidator{ - // Validate that the configuration has either: - // - only one of the schema defined attributes named attr1 - // and attr2 has a known, non-null value OR - // - only one of the schema defined attributes named attr3 - // and attr4 has a known, non-null value - datasourcevalidator.AnyWithAllWarnings( - datasourcevalidator.ExactlyOneOf( - path.MatchRoot("attr1"), - path.MatchRoot("attr2"), - ), - datasourcevalidator.ExactlyOneOf( - path.MatchRoot("attr3"), - path.MatchRoot("attr4"), - ), - ), + datasourcevalidator.Any( /* ... */ ), } } From 9f3c656b8f73d7f5f929a78640396fca6ad83be5 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 17 Aug 2023 17:23:28 -0400 Subject: [PATCH 4/8] Add `All`, `Any`, and `AnyWithAllWarnings` validators to `providervalidator` package --- internal/testvalidator/warning.go | 16 +- providervalidator/all.go | 54 +++++ providervalidator/all_example_test.go | 18 ++ providervalidator/all_test.go | 175 ++++++++++++++ providervalidator/any.go | 62 +++++ providervalidator/any_example_test.go | 14 ++ providervalidator/any_test.go | 152 +++++++++++++ providervalidator/any_with_all_warnings.go | 64 ++++++ .../any_with_all_warnings_example_test.go | 14 ++ .../any_with_all_warnings_test.go | 213 ++++++++++++++++++ 10 files changed, 781 insertions(+), 1 deletion(-) create mode 100644 providervalidator/all.go create mode 100644 providervalidator/all_example_test.go create mode 100644 providervalidator/all_test.go create mode 100644 providervalidator/any.go create mode 100644 providervalidator/any_example_test.go create mode 100644 providervalidator/any_test.go create mode 100644 providervalidator/any_with_all_warnings.go create mode 100644 providervalidator/any_with_all_warnings_example_test.go create mode 100644 providervalidator/any_with_all_warnings_test.go diff --git a/internal/testvalidator/warning.go b/internal/testvalidator/warning.go index 34f08e3..5f1d8b0 100644 --- a/internal/testvalidator/warning.go +++ b/internal/testvalidator/warning.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -74,6 +75,14 @@ func WarningObject(summary string, detail string) validator.Object { } } +// WarningProvider returns a validator which returns a warning diagnostic. +func WarningProvider(summary string, detail string) provider.ConfigValidator { + return WarningValidator{ + Summary: summary, + Detail: detail, + } +} + // WarningSet returns a validator which returns a warning diagnostic. func WarningSet(summary string, detail string) validator.Set { return WarningValidator{ @@ -91,8 +100,9 @@ func WarningString(summary string, detail string) validator.String { } var ( - _ validator.Bool = WarningValidator{} _ datasource.ConfigValidator = WarningValidator{} + _ provider.ConfigValidator = WarningValidator{} + _ validator.Bool = WarningValidator{} _ validator.Float64 = WarningValidator{} _ validator.Int64 = WarningValidator{} _ validator.List = WarningValidator{} @@ -148,6 +158,10 @@ func (v WarningValidator) ValidateObject(ctx context.Context, request validator. response.Diagnostics.AddWarning(v.Summary, v.Detail) } +func (v WarningValidator) ValidateProvider(ctx context.Context, request provider.ValidateConfigRequest, response *provider.ValidateConfigResponse) { + response.Diagnostics.AddWarning(v.Summary, v.Detail) +} + func (v WarningValidator) ValidateSet(ctx context.Context, request validator.SetRequest, response *validator.SetResponse) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } diff --git a/providervalidator/all.go b/providervalidator/all.go new file mode 100644 index 0000000..1dfc273 --- /dev/null +++ b/providervalidator/all.go @@ -0,0 +1,54 @@ +package providervalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// All returns a validator which ensures that any configured attribute value +// validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...provider.ConfigValidator) provider.ConfigValidator { + return allValidator{ + validators: validators, + } +} + +var _ provider.ConfigValidator = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []provider.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateProvider performs the validation. +func (v allValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &provider.ValidateConfigResponse{} + + subValidator.ValidateProvider(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/providervalidator/all_example_test.go b/providervalidator/all_example_test.go new file mode 100644 index 0000000..57d7193 --- /dev/null +++ b/providervalidator/all_example_test.go @@ -0,0 +1,18 @@ +package providervalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/provider" + + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" +) + +func ExampleAll() { + // Used inside a provider.Provider type ConfigValidators method + _ = []provider.ConfigValidator{ + // The configuration must satisfy either All validator. + providervalidator.Any( + providervalidator.All( /* ... */ ), + providervalidator.All( /* ... */ ), + ), + } +} diff --git a/providervalidator/all_test.go b/providervalidator/all_test.go new file mode 100644 index 0000000..6e16679 --- /dev/null +++ b/providervalidator/all_test.go @@ -0,0 +1,175 @@ +package providervalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" +) + +func TestAllValidatorValidateProvider(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []provider.ConfigValidator + req provider.ValidateConfigRequest + expected *provider.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []provider.ConfigValidator{ + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + providervalidator.All( + providervalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + providervalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []provider.ConfigValidator{ + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + providervalidator.All( + providervalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + providervalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.WithPath(path.Root("test3"), + diag.NewErrorDiagnostic( + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test3,test5]", + )), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &provider.ValidateConfigResponse{} + + providervalidator.Any(testCase.validators...).ValidateProvider(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/providervalidator/any.go b/providervalidator/any.go new file mode 100644 index 0000000..c7ade49 --- /dev/null +++ b/providervalidator/any.go @@ -0,0 +1,62 @@ +package providervalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// Any returns a validator which ensures that any configured attribute value +// passes at least one of the given validators. +// +// To prevent practitioner confusion should non-passing validators have +// conflicting logic, only warnings from the passing validator are returned. +// Use AnyWithAllWarnings() to return warnings from non-passing validators +// as well. +func Any(validators ...provider.ConfigValidator) provider.ConfigValidator { + return anyValidator{ + validators: validators, + } +} + +var _ provider.ConfigValidator = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []provider.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateProvider performs the validation. +func (v anyValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &provider.ValidateConfigResponse{} + + subValidator.ValidateProvider(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/providervalidator/any_example_test.go b/providervalidator/any_example_test.go new file mode 100644 index 0000000..162b375 --- /dev/null +++ b/providervalidator/any_example_test.go @@ -0,0 +1,14 @@ +package providervalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/provider" + + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" +) + +func ExampleAny() { + // Used inside a provider.Provider type ConfigValidators method + _ = []provider.ConfigValidator{ + providervalidator.Any( /* ... */ ), + } +} diff --git a/providervalidator/any_test.go b/providervalidator/any_test.go new file mode 100644 index 0000000..394ebf4 --- /dev/null +++ b/providervalidator/any_test.go @@ -0,0 +1,152 @@ +package providervalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" +) + +func TestAnyValidatorValidateProvider(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []provider.ConfigValidator + req provider.ValidateConfigRequest + expected *provider.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []provider.ConfigValidator{ + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + providervalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []provider.ConfigValidator{ + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + providervalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &provider.ValidateConfigResponse{} + + providervalidator.Any(testCase.validators...).ValidateProvider(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/providervalidator/any_with_all_warnings.go b/providervalidator/any_with_all_warnings.go new file mode 100644 index 0000000..df12dfa --- /dev/null +++ b/providervalidator/any_with_all_warnings.go @@ -0,0 +1,64 @@ +package providervalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// AnyWithAllWarnings returns a validator which ensures that any configured +// attribute value passes at least one of the given validators. This validator +// returns all warnings, including failed validators. +// +// Use Any() to return warnings only from the passing validator. +func AnyWithAllWarnings(validators ...provider.ConfigValidator) provider.ConfigValidator { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ provider.ConfigValidator = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []provider.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateProvider performs the validation. +func (v anyWithAllWarningsValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &provider.ValidateConfigResponse{} + + subValidator.ValidateProvider(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/providervalidator/any_with_all_warnings_example_test.go b/providervalidator/any_with_all_warnings_example_test.go new file mode 100644 index 0000000..4439078 --- /dev/null +++ b/providervalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,14 @@ +package providervalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/provider" + + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" +) + +func ExampleAnyWithAllWarnings() { + // Used inside a provider.Provider type ConfigValidators method + _ = []provider.ConfigValidator{ + providervalidator.AnyWithAllWarnings( /* ... */ ), + } +} diff --git a/providervalidator/any_with_all_warnings_test.go b/providervalidator/any_with_all_warnings_test.go new file mode 100644 index 0000000..ff00f38 --- /dev/null +++ b/providervalidator/any_with_all_warnings_test.go @@ -0,0 +1,213 @@ +package providervalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/testvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" +) + +func TestAnyWithAllWarningsValidatorValidateProvider(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []provider.ConfigValidator + req provider.ValidateConfigRequest + expected *provider.ValidateConfigResponse + }{ + "valid": { + validators: []provider.ConfigValidator{ + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + providervalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{}, + }, + "valid with warning": { + validators: []provider.ConfigValidator{ + providervalidator.All( + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + testvalidator.WarningProvider("failing warning summary", "failing warning details"), + ), + providervalidator.All( + providervalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + testvalidator.WarningProvider("passing warning summary", "passing warning details"), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("failing warning summary", "failing warning details"), + diag.NewWarningDiagnostic("passing warning summary", "passing warning details"), + }, + }, + }, + "invalid": { + validators: []provider.ConfigValidator{ + providervalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + providervalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: provider.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &provider.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &provider.ValidateConfigResponse{} + + providervalidator.AnyWithAllWarnings(testCase.validators...).ValidateProvider(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 235a1992d800ade69372bdd25aebb5752fb5e39e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 17 Aug 2023 17:51:13 -0400 Subject: [PATCH 5/8] Add `All`, `Any`, and `AnyWithAllWarnings` validators to `resourcevalidator` package --- internal/testvalidator/warning.go | 14 ++ resourcevalidator/all.go | 54 +++++ resourcevalidator/all_example_test.go | 18 ++ resourcevalidator/all_test.go | 175 ++++++++++++++ resourcevalidator/any.go | 62 +++++ resourcevalidator/any_example_test.go | 14 ++ resourcevalidator/any_test.go | 152 +++++++++++++ resourcevalidator/any_with_all_warnings.go | 64 ++++++ .../any_with_all_warnings_example_test.go | 14 ++ .../any_with_all_warnings_test.go | 213 ++++++++++++++++++ 10 files changed, 780 insertions(+) create mode 100644 resourcevalidator/all.go create mode 100644 resourcevalidator/all_example_test.go create mode 100644 resourcevalidator/all_test.go create mode 100644 resourcevalidator/any.go create mode 100644 resourcevalidator/any_example_test.go create mode 100644 resourcevalidator/any_test.go create mode 100644 resourcevalidator/any_with_all_warnings.go create mode 100644 resourcevalidator/any_with_all_warnings_example_test.go create mode 100644 resourcevalidator/any_with_all_warnings_test.go diff --git a/internal/testvalidator/warning.go b/internal/testvalidator/warning.go index 5f1d8b0..cb9376d 100644 --- a/internal/testvalidator/warning.go +++ b/internal/testvalidator/warning.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -83,6 +84,14 @@ func WarningProvider(summary string, detail string) provider.ConfigValidator { } } +// WarningResource returns a validator which returns a warning diagnostic. +func WarningResource(summary string, detail string) resource.ConfigValidator { + return WarningValidator{ + Summary: summary, + Detail: detail, + } +} + // WarningSet returns a validator which returns a warning diagnostic. func WarningSet(summary string, detail string) validator.Set { return WarningValidator{ @@ -102,6 +111,7 @@ func WarningString(summary string, detail string) validator.String { var ( _ datasource.ConfigValidator = WarningValidator{} _ provider.ConfigValidator = WarningValidator{} + _ resource.ConfigValidator = WarningValidator{} _ validator.Bool = WarningValidator{} _ validator.Float64 = WarningValidator{} _ validator.Int64 = WarningValidator{} @@ -162,6 +172,10 @@ func (v WarningValidator) ValidateProvider(ctx context.Context, request provider response.Diagnostics.AddWarning(v.Summary, v.Detail) } +func (v WarningValidator) ValidateResource(ctx context.Context, request resource.ValidateConfigRequest, response *resource.ValidateConfigResponse) { + response.Diagnostics.AddWarning(v.Summary, v.Detail) +} + func (v WarningValidator) ValidateSet(ctx context.Context, request validator.SetRequest, response *validator.SetResponse) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } diff --git a/resourcevalidator/all.go b/resourcevalidator/all.go new file mode 100644 index 0000000..f6dee02 --- /dev/null +++ b/resourcevalidator/all.go @@ -0,0 +1,54 @@ +package resourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// All returns a validator which ensures that any configured attribute value +// validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...resource.ConfigValidator) resource.ConfigValidator { + return allValidator{ + validators: validators, + } +} + +var _ resource.ConfigValidator = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []resource.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateResource performs the validation. +func (v allValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &resource.ValidateConfigResponse{} + + subValidator.ValidateResource(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/resourcevalidator/all_example_test.go b/resourcevalidator/all_example_test.go new file mode 100644 index 0000000..e7d7b17 --- /dev/null +++ b/resourcevalidator/all_example_test.go @@ -0,0 +1,18 @@ +package resourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func ExampleAll() { + // Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + // The configuration must satisfy either All validator. + resourcevalidator.Any( + resourcevalidator.All( /* ... */ ), + resourcevalidator.All( /* ... */ ), + ), + } +} diff --git a/resourcevalidator/all_test.go b/resourcevalidator/all_test.go new file mode 100644 index 0000000..cd0e6ff --- /dev/null +++ b/resourcevalidator/all_test.go @@ -0,0 +1,175 @@ +package resourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func TestAllValidatorValidateResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []resource.ConfigValidator + req resource.ValidateConfigRequest + expected *resource.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + resourcevalidator.All( + resourcevalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + resourcevalidator.All( + resourcevalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.WithPath(path.Root("test3"), + diag.NewErrorDiagnostic( + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test3,test5]", + )), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &resource.ValidateConfigResponse{} + + resourcevalidator.Any(testCase.validators...).ValidateResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resourcevalidator/any.go b/resourcevalidator/any.go new file mode 100644 index 0000000..aa1047c --- /dev/null +++ b/resourcevalidator/any.go @@ -0,0 +1,62 @@ +package resourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// Any returns a validator which ensures that any configured attribute value +// passes at least one of the given validators. +// +// To prevent practitioner confusion should non-passing validators have +// conflicting logic, only warnings from the passing validator are returned. +// Use AnyWithAllWarnings() to return warnings from non-passing validators +// as well. +func Any(validators ...resource.ConfigValidator) resource.ConfigValidator { + return anyValidator{ + validators: validators, + } +} + +var _ resource.ConfigValidator = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []resource.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateResource performs the validation. +func (v anyValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &resource.ValidateConfigResponse{} + + subValidator.ValidateResource(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/resourcevalidator/any_example_test.go b/resourcevalidator/any_example_test.go new file mode 100644 index 0000000..9dc7ea6 --- /dev/null +++ b/resourcevalidator/any_example_test.go @@ -0,0 +1,14 @@ +package resourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func ExampleAny() { + // Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + resourcevalidator.Any( /* ... */ ), + } +} diff --git a/resourcevalidator/any_test.go b/resourcevalidator/any_test.go new file mode 100644 index 0000000..983dbbb --- /dev/null +++ b/resourcevalidator/any_test.go @@ -0,0 +1,152 @@ +package resourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func TestAnyValidatorValidateResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []resource.ConfigValidator + req resource.ValidateConfigRequest + expected *resource.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &resource.ValidateConfigResponse{} + + resourcevalidator.Any(testCase.validators...).ValidateResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resourcevalidator/any_with_all_warnings.go b/resourcevalidator/any_with_all_warnings.go new file mode 100644 index 0000000..28e0ab2 --- /dev/null +++ b/resourcevalidator/any_with_all_warnings.go @@ -0,0 +1,64 @@ +package resourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// AnyWithAllWarnings returns a validator which ensures that any configured +// attribute value passes at least one of the given validators. This validator +// returns all warnings, including failed validators. +// +// Use Any() to return warnings only from the passing validator. +func AnyWithAllWarnings(validators ...resource.ConfigValidator) resource.ConfigValidator { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ resource.ConfigValidator = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []resource.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateResource performs the validation. +func (v anyWithAllWarningsValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &resource.ValidateConfigResponse{} + + subValidator.ValidateResource(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/resourcevalidator/any_with_all_warnings_example_test.go b/resourcevalidator/any_with_all_warnings_example_test.go new file mode 100644 index 0000000..bbd8203 --- /dev/null +++ b/resourcevalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,14 @@ +package resourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func ExampleAnyWithAllWarnings() { + // Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + resourcevalidator.AnyWithAllWarnings( /* ... */ ), + } +} diff --git a/resourcevalidator/any_with_all_warnings_test.go b/resourcevalidator/any_with_all_warnings_test.go new file mode 100644 index 0000000..fa1d88b --- /dev/null +++ b/resourcevalidator/any_with_all_warnings_test.go @@ -0,0 +1,213 @@ +package resourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/testvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func TestAnyWithAllWarningsValidatorValidateResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []resource.ConfigValidator + req resource.ValidateConfigRequest + expected *resource.ValidateConfigResponse + }{ + "valid": { + validators: []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{}, + }, + "valid with warning": { + validators: []resource.ConfigValidator{ + resourcevalidator.All( + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + testvalidator.WarningResource("failing warning summary", "failing warning details"), + ), + resourcevalidator.All( + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + testvalidator.WarningResource("passing warning summary", "passing warning details"), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("failing warning summary", "failing warning details"), + diag.NewWarningDiagnostic("passing warning summary", "passing warning details"), + }, + }, + }, + "invalid": { + validators: []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + resourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &resource.ValidateConfigResponse{} + + resourcevalidator.AnyWithAllWarnings(testCase.validators...).ValidateResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From e512b6d1159076ccd5453e7cb821b5dc2a72ddd7 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 18 Aug 2023 18:21:22 -0400 Subject: [PATCH 6/8] Add copyright headers --- boolvalidator/all.go | 3 +++ boolvalidator/all_example_test.go | 3 +++ boolvalidator/any.go | 3 +++ boolvalidator/any_example_test.go | 3 +++ boolvalidator/any_with_all_warnings.go | 3 +++ boolvalidator/any_with_all_warnings_example_test.go | 3 +++ datasourcevalidator/all.go | 3 +++ datasourcevalidator/all_test.go | 3 +++ datasourcevalidator/any.go | 3 +++ datasourcevalidator/any_example_test.go | 3 +++ datasourcevalidator/any_test.go | 3 +++ datasourcevalidator/any_with_all_warnings.go | 3 +++ datasourcevalidator/any_with_all_warnings_example_test.go | 3 +++ datasourcevalidator/any_with_all_warnings_test.go | 3 +++ providervalidator/all.go | 3 +++ providervalidator/all_example_test.go | 3 +++ providervalidator/all_test.go | 3 +++ providervalidator/any.go | 3 +++ providervalidator/any_example_test.go | 3 +++ providervalidator/any_test.go | 3 +++ providervalidator/any_with_all_warnings.go | 3 +++ providervalidator/any_with_all_warnings_example_test.go | 3 +++ providervalidator/any_with_all_warnings_test.go | 3 +++ resourcevalidator/all.go | 3 +++ resourcevalidator/all_example_test.go | 3 +++ resourcevalidator/all_test.go | 3 +++ resourcevalidator/any.go | 3 +++ resourcevalidator/any_example_test.go | 3 +++ resourcevalidator/any_test.go | 3 +++ resourcevalidator/any_with_all_warnings.go | 3 +++ resourcevalidator/any_with_all_warnings_example_test.go | 3 +++ resourcevalidator/any_with_all_warnings_test.go | 3 +++ 32 files changed, 96 insertions(+) diff --git a/boolvalidator/all.go b/boolvalidator/all.go index 650887d..5b58b38 100644 --- a/boolvalidator/all.go +++ b/boolvalidator/all.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package boolvalidator import ( diff --git a/boolvalidator/all_example_test.go b/boolvalidator/all_example_test.go index 5b09cc7..2e03e58 100644 --- a/boolvalidator/all_example_test.go +++ b/boolvalidator/all_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package boolvalidator_test import ( diff --git a/boolvalidator/any.go b/boolvalidator/any.go index e775020..4025d0c 100644 --- a/boolvalidator/any.go +++ b/boolvalidator/any.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package boolvalidator import ( diff --git a/boolvalidator/any_example_test.go b/boolvalidator/any_example_test.go index 7ad1b23..00be1e6 100644 --- a/boolvalidator/any_example_test.go +++ b/boolvalidator/any_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package boolvalidator_test import ( diff --git a/boolvalidator/any_with_all_warnings.go b/boolvalidator/any_with_all_warnings.go index 7fea7b8..446489f 100644 --- a/boolvalidator/any_with_all_warnings.go +++ b/boolvalidator/any_with_all_warnings.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package boolvalidator import ( diff --git a/boolvalidator/any_with_all_warnings_example_test.go b/boolvalidator/any_with_all_warnings_example_test.go index befeda8..e9643d4 100644 --- a/boolvalidator/any_with_all_warnings_example_test.go +++ b/boolvalidator/any_with_all_warnings_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package boolvalidator_test import ( diff --git a/datasourcevalidator/all.go b/datasourcevalidator/all.go index 64d2c78..88576ca 100644 --- a/datasourcevalidator/all.go +++ b/datasourcevalidator/all.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator import ( diff --git a/datasourcevalidator/all_test.go b/datasourcevalidator/all_test.go index 090cca1..95e3d17 100644 --- a/datasourcevalidator/all_test.go +++ b/datasourcevalidator/all_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator_test import ( diff --git a/datasourcevalidator/any.go b/datasourcevalidator/any.go index 2f5115f..7097b6b 100644 --- a/datasourcevalidator/any.go +++ b/datasourcevalidator/any.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator import ( diff --git a/datasourcevalidator/any_example_test.go b/datasourcevalidator/any_example_test.go index a376e0a..7b22018 100644 --- a/datasourcevalidator/any_example_test.go +++ b/datasourcevalidator/any_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator_test import ( diff --git a/datasourcevalidator/any_test.go b/datasourcevalidator/any_test.go index 746dc7c..aeb607a 100644 --- a/datasourcevalidator/any_test.go +++ b/datasourcevalidator/any_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator_test import ( diff --git a/datasourcevalidator/any_with_all_warnings.go b/datasourcevalidator/any_with_all_warnings.go index d13bd49..93c41c4 100644 --- a/datasourcevalidator/any_with_all_warnings.go +++ b/datasourcevalidator/any_with_all_warnings.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator import ( diff --git a/datasourcevalidator/any_with_all_warnings_example_test.go b/datasourcevalidator/any_with_all_warnings_example_test.go index f07d10b..f68d40f 100644 --- a/datasourcevalidator/any_with_all_warnings_example_test.go +++ b/datasourcevalidator/any_with_all_warnings_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator_test import ( diff --git a/datasourcevalidator/any_with_all_warnings_test.go b/datasourcevalidator/any_with_all_warnings_test.go index ee3f9b0..e342aee 100644 --- a/datasourcevalidator/any_with_all_warnings_test.go +++ b/datasourcevalidator/any_with_all_warnings_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasourcevalidator_test import ( diff --git a/providervalidator/all.go b/providervalidator/all.go index 1dfc273..5914d70 100644 --- a/providervalidator/all.go +++ b/providervalidator/all.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator import ( diff --git a/providervalidator/all_example_test.go b/providervalidator/all_example_test.go index 57d7193..9335ef2 100644 --- a/providervalidator/all_example_test.go +++ b/providervalidator/all_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator_test import ( diff --git a/providervalidator/all_test.go b/providervalidator/all_test.go index 6e16679..a431e13 100644 --- a/providervalidator/all_test.go +++ b/providervalidator/all_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator_test import ( diff --git a/providervalidator/any.go b/providervalidator/any.go index c7ade49..0519bd1 100644 --- a/providervalidator/any.go +++ b/providervalidator/any.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator import ( diff --git a/providervalidator/any_example_test.go b/providervalidator/any_example_test.go index 162b375..a30f92d 100644 --- a/providervalidator/any_example_test.go +++ b/providervalidator/any_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator_test import ( diff --git a/providervalidator/any_test.go b/providervalidator/any_test.go index 394ebf4..e5557e4 100644 --- a/providervalidator/any_test.go +++ b/providervalidator/any_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator_test import ( diff --git a/providervalidator/any_with_all_warnings.go b/providervalidator/any_with_all_warnings.go index df12dfa..6d181c9 100644 --- a/providervalidator/any_with_all_warnings.go +++ b/providervalidator/any_with_all_warnings.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator import ( diff --git a/providervalidator/any_with_all_warnings_example_test.go b/providervalidator/any_with_all_warnings_example_test.go index 4439078..b0409e5 100644 --- a/providervalidator/any_with_all_warnings_example_test.go +++ b/providervalidator/any_with_all_warnings_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator_test import ( diff --git a/providervalidator/any_with_all_warnings_test.go b/providervalidator/any_with_all_warnings_test.go index ff00f38..e67d05e 100644 --- a/providervalidator/any_with_all_warnings_test.go +++ b/providervalidator/any_with_all_warnings_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package providervalidator_test import ( diff --git a/resourcevalidator/all.go b/resourcevalidator/all.go index f6dee02..db9197a 100644 --- a/resourcevalidator/all.go +++ b/resourcevalidator/all.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator import ( diff --git a/resourcevalidator/all_example_test.go b/resourcevalidator/all_example_test.go index e7d7b17..abba5de 100644 --- a/resourcevalidator/all_example_test.go +++ b/resourcevalidator/all_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator_test import ( diff --git a/resourcevalidator/all_test.go b/resourcevalidator/all_test.go index cd0e6ff..a55dfb5 100644 --- a/resourcevalidator/all_test.go +++ b/resourcevalidator/all_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator_test import ( diff --git a/resourcevalidator/any.go b/resourcevalidator/any.go index aa1047c..89cb678 100644 --- a/resourcevalidator/any.go +++ b/resourcevalidator/any.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator import ( diff --git a/resourcevalidator/any_example_test.go b/resourcevalidator/any_example_test.go index 9dc7ea6..2429a45 100644 --- a/resourcevalidator/any_example_test.go +++ b/resourcevalidator/any_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator_test import ( diff --git a/resourcevalidator/any_test.go b/resourcevalidator/any_test.go index 983dbbb..8e31801 100644 --- a/resourcevalidator/any_test.go +++ b/resourcevalidator/any_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator_test import ( diff --git a/resourcevalidator/any_with_all_warnings.go b/resourcevalidator/any_with_all_warnings.go index 28e0ab2..34b6a50 100644 --- a/resourcevalidator/any_with_all_warnings.go +++ b/resourcevalidator/any_with_all_warnings.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator import ( diff --git a/resourcevalidator/any_with_all_warnings_example_test.go b/resourcevalidator/any_with_all_warnings_example_test.go index bbd8203..b1e8738 100644 --- a/resourcevalidator/any_with_all_warnings_example_test.go +++ b/resourcevalidator/any_with_all_warnings_example_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator_test import ( diff --git a/resourcevalidator/any_with_all_warnings_test.go b/resourcevalidator/any_with_all_warnings_test.go index fa1d88b..13bbd35 100644 --- a/resourcevalidator/any_with_all_warnings_test.go +++ b/resourcevalidator/any_with_all_warnings_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resourcevalidator_test import ( From da4d74e617979366cac71f9c777640344f8b716f Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 18 Aug 2023 18:58:51 -0400 Subject: [PATCH 7/8] Add changelog entries --- .changes/unreleased/ENHANCEMENTS-20230818-185606.yaml | 5 +++++ .changes/unreleased/ENHANCEMENTS-20230818-185653.yaml | 5 +++++ .changes/unreleased/ENHANCEMENTS-20230818-185720.yaml | 5 +++++ .changes/unreleased/ENHANCEMENTS-20230818-185806.yaml | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 .changes/unreleased/ENHANCEMENTS-20230818-185606.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20230818-185653.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20230818-185720.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20230818-185806.yaml diff --git a/.changes/unreleased/ENHANCEMENTS-20230818-185606.yaml b/.changes/unreleased/ENHANCEMENTS-20230818-185606.yaml new file mode 100644 index 0000000..4e97cc1 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20230818-185606.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'boolvalidator: Added `All`, `Any`, and `AnyWithAllWarnings` validators' +time: 2023-08-18T18:56:06.051472-04:00 +custom: + Issue: "158" diff --git a/.changes/unreleased/ENHANCEMENTS-20230818-185653.yaml b/.changes/unreleased/ENHANCEMENTS-20230818-185653.yaml new file mode 100644 index 0000000..6de3824 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20230818-185653.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'datasourcevalidator: Added `All`, `Any`, and `AnyWithAllWarnings` validators' +time: 2023-08-18T18:56:53.809569-04:00 +custom: + Issue: "158" diff --git a/.changes/unreleased/ENHANCEMENTS-20230818-185720.yaml b/.changes/unreleased/ENHANCEMENTS-20230818-185720.yaml new file mode 100644 index 0000000..f271666 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20230818-185720.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'providervalidator: Added `All`, `Any`, and `AnyWithAllWarnings` validators' +time: 2023-08-18T18:57:20.9318-04:00 +custom: + Issue: "158" diff --git a/.changes/unreleased/ENHANCEMENTS-20230818-185806.yaml b/.changes/unreleased/ENHANCEMENTS-20230818-185806.yaml new file mode 100644 index 0000000..5eb06fe --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20230818-185806.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'resourcevalidator: Added `All`, `Any`, and `AnyWithAllWarnings` validators' +time: 2023-08-18T18:58:06.709077-04:00 +custom: + Issue: "158" From 2e5b038ee56a01572d5e72fc2e2866019babb5c1 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 21 Aug 2023 14:13:50 -0400 Subject: [PATCH 8/8] Fix example test typo Co-authored-by: Brian Flad --- datasourcevalidator/any_with_all_warnings_example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datasourcevalidator/any_with_all_warnings_example_test.go b/datasourcevalidator/any_with_all_warnings_example_test.go index f68d40f..f007c48 100644 --- a/datasourcevalidator/any_with_all_warnings_example_test.go +++ b/datasourcevalidator/any_with_all_warnings_example_test.go @@ -12,6 +12,6 @@ import ( func ExampleAnyWithAllWarnings() { // Used inside a datasource.DataSource type ConfigValidators method _ = []datasource.ConfigValidator{ - datasourcevalidator.Any( /* ... */ ), + datasourcevalidator.AnyWithAllWarnings( /* ... */ ), } }