From 9b4e4c023d5049c3992455cc3056a5ae827e7faa Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Fri, 27 May 2022 16:53:39 +0100 Subject: [PATCH 01/11] Adding atLeastSumOf, atMostSumOf and equalToSumOf int64 validators (#20) --- int64validator/at_least_sum_of.go | 82 ++++++++++++++ int64validator/at_least_sum_of_test.go | 144 ++++++++++++++++++++++++ int64validator/at_most_sum_of.go | 82 ++++++++++++++ int64validator/at_most_sum_of_test.go | 144 ++++++++++++++++++++++++ int64validator/equal_to_sum_of.go | 82 ++++++++++++++ int64validator/equal_to_sum_of_test.go | 145 +++++++++++++++++++++++++ 6 files changed, 679 insertions(+) create mode 100644 int64validator/at_least_sum_of.go create mode 100644 int64validator/at_least_sum_of_test.go create mode 100644 int64validator/at_most_sum_of.go create mode 100644 int64validator/at_most_sum_of_test.go create mode 100644 int64validator/equal_to_sum_of.go create mode 100644 int64validator/equal_to_sum_of_test.go diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go new file mode 100644 index 00000000..1cdb040d --- /dev/null +++ b/int64validator/at_least_sum_of.go @@ -0,0 +1,82 @@ +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = atLeastSumOfValidator{} + +// atLeastSumOfValidator validates that an integer Attribute's value is at least the sum of one +// or more integer Attributes. +type atLeastSumOfValidator struct { + attributesToSumPaths []*tftypes.AttributePath +} + +// Description describes the validation in plain text formatting. +func (validator atLeastSumOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, path := range validator.attributesToSumPaths { + attributePaths = append(attributePaths, path.String()) + } + + return fmt.Sprintf("value must be at least sum of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator atLeastSumOfValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// Validate performs the validation. +func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { + i, ok := validateInt(ctx, request, response) + + if !ok { + return + } + + var sumOfAttribs int64 + + for _, path := range validator.attributesToSumPaths { + var attribToSum types.Int64 + + response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...) + if response.Diagnostics.HasError() { + return + } + + sumOfAttribs += attribToSum.Value + } + + if i < sumOfAttribs { + + response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + request.AttributePath, + validator.Description(ctx), + fmt.Sprintf("%d", i), + )) + + return + } +} + +// AtLeastSumOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is exclusively at least the sum of the given attributes. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtLeastSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator { + return atLeastSumOfValidator{ + attributesToSumPaths: attributesToSum, + } +} diff --git a/int64validator/at_least_sum_of_test.go b/int64validator/at_least_sum_of_test.go new file mode 100644 index 00000000..1f685b71 --- /dev/null +++ b/int64validator/at_least_sum_of_test.go @@ -0,0 +1,144 @@ +package int64validator + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtLeastSumOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + attributesToSumPaths []*tftypes.AttributePath + requestConfigRaw map[string]tftypes.Value + expectError bool + } + tests := map[string]testCase{ + "not an Int64": { + val: types.Bool{Value: true}, + expectError: true, + }, + "unknown Int64": { + val: types.Int64{Unknown: true}, + }, + "null Int64": { + val: types.Int64{Null: true}, + }, + "valid integer as Int64 less than sum of attributes": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 15), + "two": tftypes.NewValue(tftypes.Number, 15), + }, + expectError: true, + }, + "valid integer as Int64 equal to sum of attributes": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 5), + "two": tftypes.NewValue(tftypes.Number, 5), + }, + }, + "valid integer as Int64 greater than sum of attributes": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 4), + "two": tftypes.NewValue(tftypes.Number, 4), + }, + }, + "valid integer as Int64 greater than sum of attributes, when one summed attribute is null": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + }, + "valid integer as Int64 does not return error when all attributes are null": { + val: types.Int64{Null: true}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, nil), + }, + }, + "valid integer as Int64 greater than sum of attributes, when one summed attribute is unknown": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + }, + "valid integer as Int64 does not return error when all attributes are unknown": { + val: types.Int64{Unknown: true}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": {Type: types.Int64Type}, + "one": {Type: types.Int64Type}, + "two": {Type: types.Int64Type}, + }, + }, + }, + } + + response := tfsdk.ValidateAttributeResponse{} + + AtLeastSumOf(test.attributesToSumPaths).Validate(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go new file mode 100644 index 00000000..8d41f0b4 --- /dev/null +++ b/int64validator/at_most_sum_of.go @@ -0,0 +1,82 @@ +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = atMostSumOfValidator{} + +// atMostSumOfValidator validates that an integer Attribute's value is at most the sum of one +// or more integer Attributes. +type atMostSumOfValidator struct { + attributesToSumPaths []*tftypes.AttributePath +} + +// Description describes the validation in plain text formatting. +func (validator atMostSumOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, path := range validator.attributesToSumPaths { + attributePaths = append(attributePaths, path.String()) + } + + return fmt.Sprintf("value must be at most sum of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator atMostSumOfValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// Validate performs the validation. +func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { + i, ok := validateInt(ctx, request, response) + + if !ok { + return + } + + var sumOfAttribs int64 + + for _, path := range validator.attributesToSumPaths { + var attribToSum types.Int64 + + response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...) + if response.Diagnostics.HasError() { + return + } + + sumOfAttribs += attribToSum.Value + } + + if i > sumOfAttribs { + + response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + request.AttributePath, + validator.Description(ctx), + fmt.Sprintf("%d", i), + )) + + return + } +} + +// AtMostSumOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is exclusively at most the sum of the given attributes. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtMostSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator { + return atMostSumOfValidator{ + attributesToSumPaths: attributesToSum, + } +} diff --git a/int64validator/at_most_sum_of_test.go b/int64validator/at_most_sum_of_test.go new file mode 100644 index 00000000..5c393bfa --- /dev/null +++ b/int64validator/at_most_sum_of_test.go @@ -0,0 +1,144 @@ +package int64validator + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtMostSumOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + attributesToSumPaths []*tftypes.AttributePath + requestConfigRaw map[string]tftypes.Value + expectError bool + } + tests := map[string]testCase{ + "not an Int64": { + val: types.Bool{Value: true}, + expectError: true, + }, + "unknown Int64": { + val: types.Int64{Unknown: true}, + }, + "null Int64": { + val: types.Int64{Null: true}, + }, + "valid integer as Int64 more than sum of attributes": { + val: types.Int64{Value: 11}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 5), + "two": tftypes.NewValue(tftypes.Number, 5), + }, + expectError: true, + }, + "valid integer as Int64 equal to sum of attributes": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 5), + "two": tftypes.NewValue(tftypes.Number, 5), + }, + }, + "valid integer as Int64 less than sum of attributes": { + val: types.Int64{Value: 7}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 4), + "two": tftypes.NewValue(tftypes.Number, 4), + }, + }, + "valid integer as Int64 less than sum of attributes, when one summed attribute is null": { + val: types.Int64{Value: 8}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + }, + "valid integer as Int64 does not return error when all attributes are null": { + val: types.Int64{Null: true}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, nil), + }, + }, + "valid integer as Int64 less than sum of attributes, when one summed attribute is unknown": { + val: types.Int64{Value: 8}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + }, + "valid integer as Int64 does not return error when all attributes are unknown": { + val: types.Int64{Unknown: true}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": {Type: types.Int64Type}, + "one": {Type: types.Int64Type}, + "two": {Type: types.Int64Type}, + }, + }, + }, + } + + response := tfsdk.ValidateAttributeResponse{} + + AtMostSumOf(test.attributesToSumPaths).Validate(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go new file mode 100644 index 00000000..e85d26fe --- /dev/null +++ b/int64validator/equal_to_sum_of.go @@ -0,0 +1,82 @@ +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = equalToSumOfValidator{} + +// equalToSumOfValidator validates that an integer Attribute's value equals the sum of one +// or more integer Attributes. +type equalToSumOfValidator struct { + attributesToSumPaths []*tftypes.AttributePath +} + +// Description describes the validation in plain text formatting. +func (validator equalToSumOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, path := range validator.attributesToSumPaths { + attributePaths = append(attributePaths, path.String()) + } + + return fmt.Sprintf("value must be equal to the sum of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator equalToSumOfValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// Validate performs the validation. +func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { + i, ok := validateInt(ctx, request, response) + + if !ok { + return + } + + var sumOfAttribs int64 + + for _, path := range validator.attributesToSumPaths { + var attribToSum types.Int64 + + response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...) + if response.Diagnostics.HasError() { + return + } + + sumOfAttribs += attribToSum.Value + } + + if i != sumOfAttribs { + + response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + request.AttributePath, + validator.Description(ctx), + fmt.Sprintf("%d", i), + )) + + return + } +} + +// EqualToSumOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is exclusively equal to the sum of the given attributes. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func EqualToSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator { + return equalToSumOfValidator{ + attributesToSumPaths: attributesToSum, + } +} diff --git a/int64validator/equal_to_sum_of_test.go b/int64validator/equal_to_sum_of_test.go new file mode 100644 index 00000000..60a6ede3 --- /dev/null +++ b/int64validator/equal_to_sum_of_test.go @@ -0,0 +1,145 @@ +package int64validator + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestEqualToSumOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + attributesToSumPaths []*tftypes.AttributePath + requestConfigRaw map[string]tftypes.Value + expectError bool + } + tests := map[string]testCase{ + "not an Int64": { + val: types.Bool{Value: true}, + expectError: true, + }, + "unknown Int64": { + val: types.Int64{Unknown: true}, + }, + "null Int64": { + val: types.Int64{Null: true}, + }, + "valid integer as Int64 more than sum of attributes": { + val: types.Int64{Value: 11}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 5), + "two": tftypes.NewValue(tftypes.Number, 5), + }, + expectError: true, + }, + "valid integer as Int64 less than sum of attributes": { + val: types.Int64{Value: 9}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 5), + "two": tftypes.NewValue(tftypes.Number, 5), + }, + expectError: true, + }, + "valid integer as Int64 equal to sum of attributes": { + val: types.Int64{Value: 10}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, 5), + "two": tftypes.NewValue(tftypes.Number, 5), + }, + }, + "valid integer as Int64 equal to sum of attributes, when one summed attribute is null": { + val: types.Int64{Value: 8}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, 8), + }, + }, + "valid integer as Int64 does not return error when all attributes are null": { + val: types.Int64{Null: true}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, nil), + }, + }, + "valid integer as Int64 equal to sum of attributes, when one summed attribute is unknown": { + val: types.Int64{Value: 8}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, 8), + }, + }, + "valid integer as Int64 does not return error when all attributes are unknown": { + val: types.Int64{Unknown: true}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": {Type: types.Int64Type}, + "one": {Type: types.Int64Type}, + "two": {Type: types.Int64Type}, + }, + }, + }, + } + + response := tfsdk.ValidateAttributeResponse{} + + EqualToSumOf(test.attributesToSumPaths).Validate(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} From 4e3474b0aeba2fe62dfba6b04e3259a74aa29ba3 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 9 Jun 2022 15:54:53 +0100 Subject: [PATCH 02/11] Checking for summing of unknown attributes and using variadic args (#20) --- int64validator/at_least_sum_of.go | 16 ++++++++++- int64validator/at_least_sum_of_test.go | 37 +++++++++++++++++++++++++- int64validator/at_most_sum_of.go | 16 ++++++++++- int64validator/at_most_sum_of_test.go | 37 +++++++++++++++++++++++++- int64validator/equal_to_sum_of.go | 19 ++++++++++--- int64validator/equal_to_sum_of_test.go | 37 +++++++++++++++++++++++++- 6 files changed, 154 insertions(+), 8 deletions(-) diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go index 1cdb040d..a6b9b902 100644 --- a/int64validator/at_least_sum_of.go +++ b/int64validator/at_least_sum_of.go @@ -44,6 +44,7 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs } var sumOfAttribs int64 + var numUnknownAttribsToSum int for _, path := range validator.attributesToSumPaths { var attribToSum types.Int64 @@ -53,9 +54,22 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs return } + if attribToSum.Null { + continue + } + + if attribToSum.Unknown { + numUnknownAttribsToSum++ + continue + } + sumOfAttribs += attribToSum.Value } + if numUnknownAttribsToSum == len(validator.attributesToSumPaths) { + return + } + if i < sumOfAttribs { response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( @@ -75,7 +89,7 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs // - Is exclusively at least the sum of the given attributes. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeastSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator { +func AtLeastSumOf(attributesToSum ...*tftypes.AttributePath) tfsdk.AttributeValidator { return atLeastSumOfValidator{ attributesToSumPaths: attributesToSum, } diff --git a/int64validator/at_least_sum_of_test.go b/int64validator/at_least_sum_of_test.go index 1f685b71..87a73606 100644 --- a/int64validator/at_least_sum_of_test.go +++ b/int64validator/at_least_sum_of_test.go @@ -86,6 +86,18 @@ func TestAtLeastSumOfValidator(t *testing.T) { "two": tftypes.NewValue(tftypes.Number, nil), }, }, + "valid integer as Int64 returns error when all attributes to sum are null": { + val: types.Int64{Value: -1}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, nil), + }, + expectError: true, + }, "valid integer as Int64 greater than sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 10}, attributesToSumPaths: []*tftypes.AttributePath{ @@ -108,6 +120,29 @@ func TestAtLeastSumOfValidator(t *testing.T) { "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), }, }, + "valid integer as Int64 does not return error when all attributes to sum are unknown": { + val: types.Int64{Value: -1}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + }, + "error when attribute to sum is not Number": { + val: types.Int64{Value: 9}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Bool, true), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + expectError: true, + }, } for name, test := range tests { @@ -130,7 +165,7 @@ func TestAtLeastSumOfValidator(t *testing.T) { response := tfsdk.ValidateAttributeResponse{} - AtLeastSumOf(test.attributesToSumPaths).Validate(context.Background(), request, &response) + AtLeastSumOf(test.attributesToSumPaths...).Validate(context.Background(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go index 8d41f0b4..4c697035 100644 --- a/int64validator/at_most_sum_of.go +++ b/int64validator/at_most_sum_of.go @@ -44,6 +44,7 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd } var sumOfAttribs int64 + var numUnknownAttribsToSum int for _, path := range validator.attributesToSumPaths { var attribToSum types.Int64 @@ -53,9 +54,22 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd return } + if attribToSum.Null { + continue + } + + if attribToSum.Unknown { + numUnknownAttribsToSum++ + continue + } + sumOfAttribs += attribToSum.Value } + if numUnknownAttribsToSum == len(validator.attributesToSumPaths) { + return + } + if i > sumOfAttribs { response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( @@ -75,7 +89,7 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd // - Is exclusively at most the sum of the given attributes. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMostSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator { +func AtMostSumOf(attributesToSum ...*tftypes.AttributePath) tfsdk.AttributeValidator { return atMostSumOfValidator{ attributesToSumPaths: attributesToSum, } diff --git a/int64validator/at_most_sum_of_test.go b/int64validator/at_most_sum_of_test.go index 5c393bfa..457889f0 100644 --- a/int64validator/at_most_sum_of_test.go +++ b/int64validator/at_most_sum_of_test.go @@ -86,6 +86,18 @@ func TestAtMostSumOfValidator(t *testing.T) { "two": tftypes.NewValue(tftypes.Number, nil), }, }, + "valid integer as Int64 returns error when all attributes to sum are null": { + val: types.Int64{Value: 1}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, nil), + }, + expectError: true, + }, "valid integer as Int64 less than sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 8}, attributesToSumPaths: []*tftypes.AttributePath{ @@ -108,6 +120,29 @@ func TestAtMostSumOfValidator(t *testing.T) { "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), }, }, + "valid integer as Int64 does not return error when all attributes to sum are unknown": { + val: types.Int64{Value: 1}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + }, + "error when attribute to sum is not Number": { + val: types.Int64{Value: 9}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Bool, true), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + expectError: true, + }, } for name, test := range tests { @@ -130,7 +165,7 @@ func TestAtMostSumOfValidator(t *testing.T) { response := tfsdk.ValidateAttributeResponse{} - AtMostSumOf(test.attributesToSumPaths).Validate(context.Background(), request, &response) + AtMostSumOf(test.attributesToSumPaths...).Validate(context.Background(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go index e85d26fe..2c3bc246 100644 --- a/int64validator/equal_to_sum_of.go +++ b/int64validator/equal_to_sum_of.go @@ -44,6 +44,7 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs } var sumOfAttribs int64 + var numUnknownAttribsToSum int for _, path := range validator.attributesToSumPaths { var attribToSum types.Int64 @@ -53,11 +54,23 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs return } + if attribToSum.Null { + continue + } + + if attribToSum.Unknown { + numUnknownAttribsToSum++ + continue + } + sumOfAttribs += attribToSum.Value } - if i != sumOfAttribs { + if numUnknownAttribsToSum == len(validator.attributesToSumPaths) { + return + } + if i != sumOfAttribs { response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), @@ -72,10 +85,10 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs // attribute value: // // - Is a number, which can be represented by a 64-bit integer. -// - Is exclusively equal to the sum of the given attributes. +// - Is equal to the sum of the given attributes. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func EqualToSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator { +func EqualToSumOf(attributesToSum ...*tftypes.AttributePath) tfsdk.AttributeValidator { return equalToSumOfValidator{ attributesToSumPaths: attributesToSum, } diff --git a/int64validator/equal_to_sum_of_test.go b/int64validator/equal_to_sum_of_test.go index 60a6ede3..2f291789 100644 --- a/int64validator/equal_to_sum_of_test.go +++ b/int64validator/equal_to_sum_of_test.go @@ -87,6 +87,18 @@ func TestEqualToSumOfValidator(t *testing.T) { "two": tftypes.NewValue(tftypes.Number, nil), }, }, + "valid integer as Int64 returns error when all attributes to sum are null": { + val: types.Int64{Value: 1}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, nil), + "two": tftypes.NewValue(tftypes.Number, nil), + }, + expectError: true, + }, "valid integer as Int64 equal to sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 8}, attributesToSumPaths: []*tftypes.AttributePath{ @@ -109,6 +121,29 @@ func TestEqualToSumOfValidator(t *testing.T) { "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), }, }, + "valid integer as Int64 does not return error when all attributes to sum are unknown": { + val: types.Int64{Value: 1}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + }, + "error when attribute to sum is not Number": { + val: types.Int64{Value: 9}, + attributesToSumPaths: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("one"), + tftypes.NewAttributePath().WithAttributeName("two"), + }, + requestConfigRaw: map[string]tftypes.Value{ + "one": tftypes.NewValue(tftypes.Bool, true), + "two": tftypes.NewValue(tftypes.Number, 9), + }, + expectError: true, + }, } for name, test := range tests { @@ -131,7 +166,7 @@ func TestEqualToSumOfValidator(t *testing.T) { response := tfsdk.ValidateAttributeResponse{} - EqualToSumOf(test.attributesToSumPaths).Validate(context.Background(), request, &response) + EqualToSumOf(test.attributesToSumPaths...).Validate(context.Background(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") From 533f2df2f21f98ca60d1475c96b7a6a51aad3d4d Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 6 Jul 2022 10:24:31 +0100 Subject: [PATCH 03/11] Switching to using native framework path for AtLeastSumOf, AtMostSumOf and EqualToSumOf validators (#20) --- int64validator/at_least_sum_of.go | 18 +++---- int64validator/at_least_sum_of_test.go | 65 +++++++++++++------------- int64validator/at_most_sum_of.go | 18 +++---- int64validator/at_most_sum_of_test.go | 65 +++++++++++++------------- int64validator/equal_to_sum_of.go | 18 +++---- int64validator/equal_to_sum_of_test.go | 65 +++++++++++++------------- 6 files changed, 126 insertions(+), 123 deletions(-) diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go index a6b9b902..2dbd1998 100644 --- a/int64validator/at_least_sum_of.go +++ b/int64validator/at_least_sum_of.go @@ -5,11 +5,11 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ tfsdk.AttributeValidator = atLeastSumOfValidator{} @@ -17,14 +17,14 @@ var _ tfsdk.AttributeValidator = atLeastSumOfValidator{} // atLeastSumOfValidator validates that an integer Attribute's value is at least the sum of one // or more integer Attributes. type atLeastSumOfValidator struct { - attributesToSumPaths []*tftypes.AttributePath + attributesToSumPaths []path.Path } // Description describes the validation in plain text formatting. func (validator atLeastSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, path := range validator.attributesToSumPaths { - attributePaths = append(attributePaths, path.String()) + for _, p := range validator.attributesToSumPaths { + attributePaths = append(attributePaths, p.String()) } return fmt.Sprintf("value must be at least sum of %s", strings.Join(attributePaths, " + ")) @@ -46,10 +46,10 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs var sumOfAttribs int64 var numUnknownAttribsToSum int - for _, path := range validator.attributesToSumPaths { + for _, p := range validator.attributesToSumPaths { var attribToSum types.Int64 - response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...) + response.Diagnostics.Append(request.Config.GetAttribute(ctx, p, &attribToSum)...) if response.Diagnostics.HasError() { return } @@ -72,7 +72,7 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs if i < sumOfAttribs { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", i), @@ -89,7 +89,7 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs // - Is exclusively at least the sum of the given attributes. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeastSumOf(attributesToSum ...*tftypes.AttributePath) tfsdk.AttributeValidator { +func AtLeastSumOf(attributesToSum ...path.Path) tfsdk.AttributeValidator { return atLeastSumOfValidator{ attributesToSumPaths: attributesToSum, } diff --git a/int64validator/at_least_sum_of_test.go b/int64validator/at_least_sum_of_test.go index 87a73606..b827dba4 100644 --- a/int64validator/at_least_sum_of_test.go +++ b/int64validator/at_least_sum_of_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -15,7 +16,7 @@ func TestAtLeastSumOfValidator(t *testing.T) { type testCase struct { val attr.Value - attributesToSumPaths []*tftypes.AttributePath + attributesToSumPaths []path.Path requestConfigRaw map[string]tftypes.Value expectError bool } @@ -32,9 +33,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 15), @@ -44,9 +45,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -55,9 +56,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 greater than sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 4), @@ -66,9 +67,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 greater than sum of attributes, when one summed attribute is null": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -77,9 +78,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are null": { val: types.Int64{Null: true}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -88,9 +89,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 returns error when all attributes to sum are null": { val: types.Int64{Value: -1}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -100,9 +101,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 greater than sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -111,9 +112,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are unknown": { val: types.Int64{Unknown: true}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -122,9 +123,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes to sum are unknown": { val: types.Int64{Value: -1}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -133,9 +134,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "error when attribute to sum is not Number": { val: types.Int64{Value: 9}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Bool, true), @@ -149,7 +150,7 @@ func TestAtLeastSumOfValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributePath: path.Root("test"), AttributeConfig: test.val, Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go index 4c697035..6e735804 100644 --- a/int64validator/at_most_sum_of.go +++ b/int64validator/at_most_sum_of.go @@ -5,11 +5,11 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ tfsdk.AttributeValidator = atMostSumOfValidator{} @@ -17,14 +17,14 @@ var _ tfsdk.AttributeValidator = atMostSumOfValidator{} // atMostSumOfValidator validates that an integer Attribute's value is at most the sum of one // or more integer Attributes. type atMostSumOfValidator struct { - attributesToSumPaths []*tftypes.AttributePath + attributesToSumPaths []path.Path } // Description describes the validation in plain text formatting. func (validator atMostSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, path := range validator.attributesToSumPaths { - attributePaths = append(attributePaths, path.String()) + for _, p := range validator.attributesToSumPaths { + attributePaths = append(attributePaths, p.String()) } return fmt.Sprintf("value must be at most sum of %s", strings.Join(attributePaths, " + ")) @@ -46,10 +46,10 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd var sumOfAttribs int64 var numUnknownAttribsToSum int - for _, path := range validator.attributesToSumPaths { + for _, p := range validator.attributesToSumPaths { var attribToSum types.Int64 - response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...) + response.Diagnostics.Append(request.Config.GetAttribute(ctx, p, &attribToSum)...) if response.Diagnostics.HasError() { return } @@ -72,7 +72,7 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd if i > sumOfAttribs { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", i), @@ -89,7 +89,7 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd // - Is exclusively at most the sum of the given attributes. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMostSumOf(attributesToSum ...*tftypes.AttributePath) tfsdk.AttributeValidator { +func AtMostSumOf(attributesToSum ...path.Path) tfsdk.AttributeValidator { return atMostSumOfValidator{ attributesToSumPaths: attributesToSum, } diff --git a/int64validator/at_most_sum_of_test.go b/int64validator/at_most_sum_of_test.go index 457889f0..cb06237d 100644 --- a/int64validator/at_most_sum_of_test.go +++ b/int64validator/at_most_sum_of_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -15,7 +16,7 @@ func TestAtMostSumOfValidator(t *testing.T) { type testCase struct { val attr.Value - attributesToSumPaths []*tftypes.AttributePath + attributesToSumPaths []path.Path requestConfigRaw map[string]tftypes.Value expectError bool } @@ -32,9 +33,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 more than sum of attributes": { val: types.Int64{Value: 11}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -44,9 +45,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -55,9 +56,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes": { val: types.Int64{Value: 7}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 4), @@ -66,9 +67,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes, when one summed attribute is null": { val: types.Int64{Value: 8}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -77,9 +78,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are null": { val: types.Int64{Null: true}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -88,9 +89,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 returns error when all attributes to sum are null": { val: types.Int64{Value: 1}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -100,9 +101,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 8}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -111,9 +112,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are unknown": { val: types.Int64{Unknown: true}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -122,9 +123,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes to sum are unknown": { val: types.Int64{Value: 1}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -133,9 +134,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "error when attribute to sum is not Number": { val: types.Int64{Value: 9}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Bool, true), @@ -149,7 +150,7 @@ func TestAtMostSumOfValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributePath: path.Root("test"), AttributeConfig: test.val, Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go index 2c3bc246..9cb07121 100644 --- a/int64validator/equal_to_sum_of.go +++ b/int64validator/equal_to_sum_of.go @@ -5,11 +5,11 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ tfsdk.AttributeValidator = equalToSumOfValidator{} @@ -17,14 +17,14 @@ var _ tfsdk.AttributeValidator = equalToSumOfValidator{} // equalToSumOfValidator validates that an integer Attribute's value equals the sum of one // or more integer Attributes. type equalToSumOfValidator struct { - attributesToSumPaths []*tftypes.AttributePath + attributesToSumPaths []path.Path } // Description describes the validation in plain text formatting. func (validator equalToSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, path := range validator.attributesToSumPaths { - attributePaths = append(attributePaths, path.String()) + for _, p := range validator.attributesToSumPaths { + attributePaths = append(attributePaths, p.String()) } return fmt.Sprintf("value must be equal to the sum of %s", strings.Join(attributePaths, " + ")) @@ -46,10 +46,10 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs var sumOfAttribs int64 var numUnknownAttribsToSum int - for _, path := range validator.attributesToSumPaths { + for _, p := range validator.attributesToSumPaths { var attribToSum types.Int64 - response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...) + response.Diagnostics.Append(request.Config.GetAttribute(ctx, p, &attribToSum)...) if response.Diagnostics.HasError() { return } @@ -71,7 +71,7 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs } if i != sumOfAttribs { - response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), fmt.Sprintf("%d", i), @@ -88,7 +88,7 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs // - Is equal to the sum of the given attributes. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func EqualToSumOf(attributesToSum ...*tftypes.AttributePath) tfsdk.AttributeValidator { +func EqualToSumOf(attributesToSum ...path.Path) tfsdk.AttributeValidator { return equalToSumOfValidator{ attributesToSumPaths: attributesToSum, } diff --git a/int64validator/equal_to_sum_of_test.go b/int64validator/equal_to_sum_of_test.go index 2f291789..15191fde 100644 --- a/int64validator/equal_to_sum_of_test.go +++ b/int64validator/equal_to_sum_of_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -15,7 +16,7 @@ func TestEqualToSumOfValidator(t *testing.T) { type testCase struct { val attr.Value - attributesToSumPaths []*tftypes.AttributePath + attributesToSumPaths []path.Path requestConfigRaw map[string]tftypes.Value expectError bool } @@ -32,9 +33,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 more than sum of attributes": { val: types.Int64{Value: 11}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -44,9 +45,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes": { val: types.Int64{Value: 9}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -56,9 +57,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -67,9 +68,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes, when one summed attribute is null": { val: types.Int64{Value: 8}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -78,9 +79,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are null": { val: types.Int64{Null: true}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -89,9 +90,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 returns error when all attributes to sum are null": { val: types.Int64{Value: 1}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -101,9 +102,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 8}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -112,9 +113,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are unknown": { val: types.Int64{Unknown: true}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -123,9 +124,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes to sum are unknown": { val: types.Int64{Value: 1}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -134,9 +135,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "error when attribute to sum is not Number": { val: types.Int64{Value: 9}, - attributesToSumPaths: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("one"), - tftypes.NewAttributePath().WithAttributeName("two"), + attributesToSumPaths: []path.Path{ + path.Root("one"), + path.Root("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Bool, true), @@ -150,7 +151,7 @@ func TestEqualToSumOfValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributePath: path.Root("test"), AttributeConfig: test.val, Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), From f37d1cb43d1b73dab746b75ce73a5fcb463c2413 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 13 Jul 2022 11:32:44 +0100 Subject: [PATCH 04/11] Switching to using path expressions for AtLeastSumOf, AtMostSumOf and EqualToSumOf validators (#20) --- int64validator/at_least_sum_of.go | 56 +++++++++++-------- int64validator/at_least_sum_of_test.go | 75 +++++++++++++------------- int64validator/at_most_sum_of.go | 56 +++++++++++-------- int64validator/at_most_sum_of_test.go | 75 +++++++++++++------------- int64validator/equal_to_sum_of.go | 55 +++++++++++-------- int64validator/equal_to_sum_of_test.go | 75 +++++++++++++------------- 6 files changed, 213 insertions(+), 179 deletions(-) diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go index 2dbd1998..c50d886d 100644 --- a/int64validator/at_least_sum_of.go +++ b/int64validator/at_least_sum_of.go @@ -15,15 +15,15 @@ import ( var _ tfsdk.AttributeValidator = atLeastSumOfValidator{} // atLeastSumOfValidator validates that an integer Attribute's value is at least the sum of one -// or more integer Attributes. +// or more integer Attributes retrieved via the given path expressions. type atLeastSumOfValidator struct { - attributesToSumPaths []path.Path + attributesToSumPathExpressions path.Expressions } // Description describes the validation in plain text formatting. func (validator atLeastSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, p := range validator.attributesToSumPaths { + for _, p := range validator.attributesToSumPathExpressions { attributePaths = append(attributePaths, p.String()) } @@ -46,32 +46,44 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs var sumOfAttribs int64 var numUnknownAttribsToSum int - for _, p := range validator.attributesToSumPaths { - var attribToSum types.Int64 + for _, expression := range validator.attributesToSumPathExpressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) - response.Diagnostics.Append(request.Config.GetAttribute(ctx, p, &attribToSum)...) - if response.Diagnostics.HasError() { - return - } - - if attribToSum.Null { + // Collect all errors + if diags.HasError() { continue } - if attribToSum.Unknown { - numUnknownAttribsToSum++ - continue - } + for _, mp := range matchedPaths { + var attribToSum types.Int64 + + diags := request.Config.GetAttribute(ctx, mp, &attribToSum) + response.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } - sumOfAttribs += attribToSum.Value + if attribToSum.IsNull() { + continue + } + + if attribToSum.IsUnknown() { + numUnknownAttribsToSum++ + continue + } + + sumOfAttribs += attribToSum.Value + } } - if numUnknownAttribsToSum == len(validator.attributesToSumPaths) { + if numUnknownAttribsToSum == len(validator.attributesToSumPathExpressions) { return } if i < sumOfAttribs { - response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), @@ -86,11 +98,9 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs // attribute value: // // - Is a number, which can be represented by a 64-bit integer. -// - Is exclusively at least the sum of the given attributes. +// - Is at least the sum of the attributes retrieved via the given path expression(s). // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeastSumOf(attributesToSum ...path.Path) tfsdk.AttributeValidator { - return atLeastSumOfValidator{ - attributesToSumPaths: attributesToSum, - } +func AtLeastSumOf(attributesToSumPathExpressions ...path.Expression) tfsdk.AttributeValidator { + return atLeastSumOfValidator{attributesToSumPathExpressions} } diff --git a/int64validator/at_least_sum_of_test.go b/int64validator/at_least_sum_of_test.go index b827dba4..fdfc0c89 100644 --- a/int64validator/at_least_sum_of_test.go +++ b/int64validator/at_least_sum_of_test.go @@ -15,10 +15,10 @@ func TestAtLeastSumOfValidator(t *testing.T) { t.Parallel() type testCase struct { - val attr.Value - attributesToSumPaths []path.Path - requestConfigRaw map[string]tftypes.Value - expectError bool + val attr.Value + attributesToSumExpressions path.Expressions + requestConfigRaw map[string]tftypes.Value + expectError bool } tests := map[string]testCase{ "not an Int64": { @@ -33,9 +33,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 15), @@ -45,9 +45,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -56,9 +56,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 greater than sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 4), @@ -67,9 +67,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 greater than sum of attributes, when one summed attribute is null": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -78,9 +78,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are null": { val: types.Int64{Null: true}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -89,9 +89,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 returns error when all attributes to sum are null": { val: types.Int64{Value: -1}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -101,9 +101,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 greater than sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -112,9 +112,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are unknown": { val: types.Int64{Unknown: true}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -123,9 +123,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes to sum are unknown": { val: types.Int64{Value: -1}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -134,9 +134,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { }, "error when attribute to sum is not Number": { val: types.Int64{Value: 9}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Bool, true), @@ -150,8 +150,9 @@ func TestAtLeastSumOfValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), Schema: tfsdk.Schema{ @@ -166,7 +167,7 @@ func TestAtLeastSumOfValidator(t *testing.T) { response := tfsdk.ValidateAttributeResponse{} - AtLeastSumOf(test.attributesToSumPaths...).Validate(context.Background(), request, &response) + AtLeastSumOf(test.attributesToSumExpressions...).Validate(context.Background(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go index 6e735804..1bf27585 100644 --- a/int64validator/at_most_sum_of.go +++ b/int64validator/at_most_sum_of.go @@ -15,15 +15,15 @@ import ( var _ tfsdk.AttributeValidator = atMostSumOfValidator{} // atMostSumOfValidator validates that an integer Attribute's value is at most the sum of one -// or more integer Attributes. +// or more integer Attributes retrieved via the given path expressions. type atMostSumOfValidator struct { - attributesToSumPaths []path.Path + attributesToSumPathExpressions path.Expressions } // Description describes the validation in plain text formatting. func (validator atMostSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, p := range validator.attributesToSumPaths { + for _, p := range validator.attributesToSumPathExpressions { attributePaths = append(attributePaths, p.String()) } @@ -46,32 +46,44 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd var sumOfAttribs int64 var numUnknownAttribsToSum int - for _, p := range validator.attributesToSumPaths { - var attribToSum types.Int64 + for _, expression := range validator.attributesToSumPathExpressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) - response.Diagnostics.Append(request.Config.GetAttribute(ctx, p, &attribToSum)...) - if response.Diagnostics.HasError() { - return - } - - if attribToSum.Null { + // Collect all errors + if diags.HasError() { continue } - if attribToSum.Unknown { - numUnknownAttribsToSum++ - continue - } + for _, mp := range matchedPaths { + var attribToSum types.Int64 + + diags := request.Config.GetAttribute(ctx, mp, &attribToSum) + response.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } - sumOfAttribs += attribToSum.Value + if attribToSum.IsNull() { + continue + } + + if attribToSum.IsUnknown() { + numUnknownAttribsToSum++ + continue + } + + sumOfAttribs += attribToSum.Value + } } - if numUnknownAttribsToSum == len(validator.attributesToSumPaths) { + if numUnknownAttribsToSum == len(validator.attributesToSumPathExpressions) { return } if i > sumOfAttribs { - response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, validator.Description(ctx), @@ -86,11 +98,9 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd // attribute value: // // - Is a number, which can be represented by a 64-bit integer. -// - Is exclusively at most the sum of the given attributes. +// - Is at most the sum of the given attributes retrieved via the given path expression(s). // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMostSumOf(attributesToSum ...path.Path) tfsdk.AttributeValidator { - return atMostSumOfValidator{ - attributesToSumPaths: attributesToSum, - } +func AtMostSumOf(attributesToSumPathExpressions ...path.Expression) tfsdk.AttributeValidator { + return atMostSumOfValidator{attributesToSumPathExpressions} } diff --git a/int64validator/at_most_sum_of_test.go b/int64validator/at_most_sum_of_test.go index cb06237d..7b65287d 100644 --- a/int64validator/at_most_sum_of_test.go +++ b/int64validator/at_most_sum_of_test.go @@ -15,10 +15,10 @@ func TestAtMostSumOfValidator(t *testing.T) { t.Parallel() type testCase struct { - val attr.Value - attributesToSumPaths []path.Path - requestConfigRaw map[string]tftypes.Value - expectError bool + val attr.Value + attributesToSumPathExpressions path.Expressions + requestConfigRaw map[string]tftypes.Value + expectError bool } tests := map[string]testCase{ "not an Int64": { @@ -33,9 +33,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 more than sum of attributes": { val: types.Int64{Value: 11}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -45,9 +45,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -56,9 +56,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes": { val: types.Int64{Value: 7}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 4), @@ -67,9 +67,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes, when one summed attribute is null": { val: types.Int64{Value: 8}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -78,9 +78,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are null": { val: types.Int64{Null: true}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -89,9 +89,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 returns error when all attributes to sum are null": { val: types.Int64{Value: 1}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -101,9 +101,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 8}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -112,9 +112,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are unknown": { val: types.Int64{Unknown: true}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -123,9 +123,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes to sum are unknown": { val: types.Int64{Value: 1}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -134,9 +134,9 @@ func TestAtMostSumOfValidator(t *testing.T) { }, "error when attribute to sum is not Number": { val: types.Int64{Value: 9}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Bool, true), @@ -150,8 +150,9 @@ func TestAtMostSumOfValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), Schema: tfsdk.Schema{ @@ -166,7 +167,7 @@ func TestAtMostSumOfValidator(t *testing.T) { response := tfsdk.ValidateAttributeResponse{} - AtMostSumOf(test.attributesToSumPaths...).Validate(context.Background(), request, &response) + AtMostSumOf(test.attributesToSumPathExpressions...).Validate(context.Background(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go index 9cb07121..e9c78748 100644 --- a/int64validator/equal_to_sum_of.go +++ b/int64validator/equal_to_sum_of.go @@ -15,15 +15,15 @@ import ( var _ tfsdk.AttributeValidator = equalToSumOfValidator{} // equalToSumOfValidator validates that an integer Attribute's value equals the sum of one -// or more integer Attributes. +// or more integer Attributes retrieved via the given path expressions. type equalToSumOfValidator struct { - attributesToSumPaths []path.Path + attributesToSumPathExpressions path.Expressions } // Description describes the validation in plain text formatting. func (validator equalToSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, p := range validator.attributesToSumPaths { + for _, p := range validator.attributesToSumPathExpressions { attributePaths = append(attributePaths, p.String()) } @@ -46,27 +46,40 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs var sumOfAttribs int64 var numUnknownAttribsToSum int - for _, p := range validator.attributesToSumPaths { - var attribToSum types.Int64 + for _, expression := range validator.attributesToSumPathExpressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) - response.Diagnostics.Append(request.Config.GetAttribute(ctx, p, &attribToSum)...) - if response.Diagnostics.HasError() { - return - } - - if attribToSum.Null { + // Collect all errors + if diags.HasError() { continue } - if attribToSum.Unknown { - numUnknownAttribsToSum++ - continue - } + for _, mp := range matchedPaths { + var attribToSum types.Int64 + + diags := request.Config.GetAttribute(ctx, mp, &attribToSum) + response.Diagnostics.Append(diags...) - sumOfAttribs += attribToSum.Value + // Collect all errors + if diags.HasError() { + continue + } + + if attribToSum.IsNull() { + continue + } + + if attribToSum.IsUnknown() { + numUnknownAttribsToSum++ + continue + } + + sumOfAttribs += attribToSum.Value + } } - if numUnknownAttribsToSum == len(validator.attributesToSumPaths) { + if numUnknownAttribsToSum == len(validator.attributesToSumPathExpressions) { return } @@ -85,11 +98,9 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs // attribute value: // // - Is a number, which can be represented by a 64-bit integer. -// - Is equal to the sum of the given attributes. +// - Is equal to the sum of the given attributes retrieved via the given path expression(s). // // Null (unconfigured) and unknown (known after apply) values are skipped. -func EqualToSumOf(attributesToSum ...path.Path) tfsdk.AttributeValidator { - return equalToSumOfValidator{ - attributesToSumPaths: attributesToSum, - } +func EqualToSumOf(attributesToSumPathExpressions ...path.Expression) tfsdk.AttributeValidator { + return equalToSumOfValidator{attributesToSumPathExpressions} } diff --git a/int64validator/equal_to_sum_of_test.go b/int64validator/equal_to_sum_of_test.go index 15191fde..6a99cdf0 100644 --- a/int64validator/equal_to_sum_of_test.go +++ b/int64validator/equal_to_sum_of_test.go @@ -15,10 +15,10 @@ func TestEqualToSumOfValidator(t *testing.T) { t.Parallel() type testCase struct { - val attr.Value - attributesToSumPaths []path.Path - requestConfigRaw map[string]tftypes.Value - expectError bool + val attr.Value + attributesToSumPathExpressions path.Expressions + requestConfigRaw map[string]tftypes.Value + expectError bool } tests := map[string]testCase{ "not an Int64": { @@ -33,9 +33,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 more than sum of attributes": { val: types.Int64{Value: 11}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -45,9 +45,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 less than sum of attributes": { val: types.Int64{Value: 9}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -57,9 +57,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes": { val: types.Int64{Value: 10}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, 5), @@ -68,9 +68,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes, when one summed attribute is null": { val: types.Int64{Value: 8}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -79,9 +79,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are null": { val: types.Int64{Null: true}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -90,9 +90,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 returns error when all attributes to sum are null": { val: types.Int64{Value: 1}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, nil), @@ -102,9 +102,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 equal to sum of attributes, when one summed attribute is unknown": { val: types.Int64{Value: 8}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -113,9 +113,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes are unknown": { val: types.Int64{Unknown: true}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -124,9 +124,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "valid integer as Int64 does not return error when all attributes to sum are unknown": { val: types.Int64{Value: 1}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), @@ -135,9 +135,9 @@ func TestEqualToSumOfValidator(t *testing.T) { }, "error when attribute to sum is not Number": { val: types.Int64{Value: 9}, - attributesToSumPaths: []path.Path{ - path.Root("one"), - path.Root("two"), + attributesToSumPathExpressions: path.Expressions{ + path.MatchRoot("one"), + path.MatchRoot("two"), }, requestConfigRaw: map[string]tftypes.Value{ "one": tftypes.NewValue(tftypes.Bool, true), @@ -151,8 +151,9 @@ func TestEqualToSumOfValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), Schema: tfsdk.Schema{ @@ -167,7 +168,7 @@ func TestEqualToSumOfValidator(t *testing.T) { response := tfsdk.ValidateAttributeResponse{} - EqualToSumOf(test.attributesToSumPaths...).Validate(context.Background(), request, &response) + EqualToSumOf(test.attributesToSumPathExpressions...).Validate(context.Background(), request, &response) if !response.Diagnostics.HasError() && test.expectError { t.Fatal("expected error, got no error") From 1f5151c39fa5e91c89870a019c5514d72a23f16a Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 13 Jul 2022 13:01:22 +0100 Subject: [PATCH 05/11] Do not validate if any attributes are unknown (#20) --- int64validator/at_least_sum_of.go | 2 +- int64validator/at_most_sum_of.go | 2 +- int64validator/equal_to_sum_of.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go index c50d886d..5c597266 100644 --- a/int64validator/at_least_sum_of.go +++ b/int64validator/at_least_sum_of.go @@ -79,7 +79,7 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs } } - if numUnknownAttribsToSum == len(validator.attributesToSumPathExpressions) { + if numUnknownAttribsToSum > 0 { return } diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go index 1bf27585..84ba0ceb 100644 --- a/int64validator/at_most_sum_of.go +++ b/int64validator/at_most_sum_of.go @@ -79,7 +79,7 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd } } - if numUnknownAttribsToSum == len(validator.attributesToSumPathExpressions) { + if numUnknownAttribsToSum > 0 { return } diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go index e9c78748..d659cbe6 100644 --- a/int64validator/equal_to_sum_of.go +++ b/int64validator/equal_to_sum_of.go @@ -79,7 +79,7 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs } } - if numUnknownAttribsToSum == len(validator.attributesToSumPathExpressions) { + if numUnknownAttribsToSum > 0 { return } From aabdb17e0eee300b854b4d4c683c4a97baaa7b0b Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Tue, 19 Jul 2022 11:11:46 +0100 Subject: [PATCH 06/11] Updating dependencies, including terraform-plugin-framework@0.10.0 --- go.mod | 14 +++++++------- go.sum | 22 +++++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index dc60f2e9..ef69a74f 100644 --- a/go.mod +++ b/go.mod @@ -4,22 +4,22 @@ go 1.17 require ( github.com/google/go-cmp v0.5.8 - github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707 - github.com/hashicorp/terraform-plugin-go v0.11.0 + github.com/hashicorp/terraform-plugin-framework v0.10.0 + github.com/hashicorp/terraform-plugin-go v0.12.0 ) require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/terraform-plugin-log v0.4.1 // indirect + github.com/hashicorp/terraform-plugin-log v0.6.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect - github.com/vmihailenco/tagparser v0.1.1 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - google.golang.org/appengine v1.6.5 // indirect + github.com/vmihailenco/tagparser v0.1.2 // indirect + golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index 2265c03e..47d24461 100644 --- a/go.sum +++ b/go.sum @@ -62,12 +62,12 @@ github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707 h1:4wAdubsgZ/eAbCjW2At8w6UDeVCel76jWDHsWvbc8Pk= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707/go.mod h1:+H4ieVu7X4bfYlLB/zytek48e4CjcG+gjKdVOjVY1PU= -github.com/hashicorp/terraform-plugin-go v0.11.0 h1:YXsvSCx7GbQO5jIUQd77FesqmIBxgSvYAtAX1NqErTk= -github.com/hashicorp/terraform-plugin-go v0.11.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= -github.com/hashicorp/terraform-plugin-log v0.4.1 h1:xpbmVhvuU3mgHzLetOmx9pkOL2rmgpu302XxddON6eo= -github.com/hashicorp/terraform-plugin-log v0.4.1/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= +github.com/hashicorp/terraform-plugin-framework v0.10.0 h1:LGYcnvNdVaZA1ZHe53BHLVjaaGs7HTiq6+9Js29stL4= +github.com/hashicorp/terraform-plugin-framework v0.10.0/go.mod h1:CK7Opzukfu/2CPJs+HzUdfHrFlp+ZIQeSxjF0x8k464= +github.com/hashicorp/terraform-plugin-go v0.12.0 h1:6wW9mT1dSs0Xq4LR6HXj1heQ5ovr5GxXNJwkErZzpJw= +github.com/hashicorp/terraform-plugin-go v0.12.0/go.mod h1:kwhmaWHNDvT1B3QiSJdAtrB/D4RaKSY/v3r2BuoWK4M= +github.com/hashicorp/terraform-plugin-log v0.6.0 h1:/Vq78uSIdUSZ3iqDc9PESKtwt8YqNKN6u+khD+lLjuw= +github.com/hashicorp/terraform-plugin-log v0.6.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -110,6 +110,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvC github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -134,6 +136,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -157,6 +161,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -174,6 +180,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= @@ -187,7 +195,7 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From df358cd475c649a61245b2dc4cea5244c9bfe0db Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Tue, 19 Jul 2022 11:39:32 +0100 Subject: [PATCH 07/11] PR review: making use of the new `path.Expression` `.MergeExpressions` method --- int64validator/at_least_sum_of.go | 41 +++++++++++++++++-------------- int64validator/at_most_sum_of.go | 41 +++++++++++++++++-------------- int64validator/equal_to_sum_of.go | 41 +++++++++++++++++-------------- 3 files changed, 66 insertions(+), 57 deletions(-) diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go index 5c597266..f1075bf9 100644 --- a/int64validator/at_least_sum_of.go +++ b/int64validator/at_least_sum_of.go @@ -21,9 +21,9 @@ type atLeastSumOfValidator struct { } // Description describes the validation in plain text formatting. -func (validator atLeastSumOfValidator) Description(_ context.Context) string { +func (av atLeastSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, p := range validator.attributesToSumPathExpressions { + for _, p := range av.attributesToSumPathExpressions { attributePaths = append(attributePaths, p.String()) } @@ -31,22 +31,23 @@ func (validator atLeastSumOfValidator) Description(_ context.Context) string { } // MarkdownDescription describes the validation in Markdown formatting. -func (validator atLeastSumOfValidator) MarkdownDescription(ctx context.Context) string { - return validator.Description(ctx) +func (av atLeastSumOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) } // Validate performs the validation. -func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { +func (av atLeastSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { i, ok := validateInt(ctx, request, response) - if !ok { return } - var sumOfAttribs int64 - var numUnknownAttribsToSum int + // Ensure input path expressions resolution against the current attribute + expressions := request.AttributePathExpression.MergeExpressions(av.attributesToSumPathExpressions...) - for _, expression := range validator.attributesToSumPathExpressions { + // Sum the value of all the attributes involved, but only if they are all known. + var sumOfAttribs int64 + for _, expression := range expressions { matchedPaths, diags := request.Config.PathMatches(ctx, expression) response.Diagnostics.Append(diags...) @@ -56,8 +57,13 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs } for _, mp := range matchedPaths { - var attribToSum types.Int64 + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.AttributePath) { + continue + } + var attribToSum types.Int64 diags := request.Config.GetAttribute(ctx, mp, &attribToSum) response.Diagnostics.Append(diags...) @@ -66,12 +72,13 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs continue } - if attribToSum.IsNull() { - continue + // Delay validation until all involved attribute have a known value + if attribToSum.IsUnknown() { + return } - if attribToSum.IsUnknown() { - numUnknownAttribsToSum++ + // Attribute is null, so it doesn't contribute to the sum + if attribToSum.IsNull() { continue } @@ -79,14 +86,10 @@ func (validator atLeastSumOfValidator) Validate(ctx context.Context, request tfs } } - if numUnknownAttribsToSum > 0 { - return - } - if i < sumOfAttribs { response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, - validator.Description(ctx), + av.Description(ctx), fmt.Sprintf("%d", i), )) diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go index 84ba0ceb..87748fd9 100644 --- a/int64validator/at_most_sum_of.go +++ b/int64validator/at_most_sum_of.go @@ -21,9 +21,9 @@ type atMostSumOfValidator struct { } // Description describes the validation in plain text formatting. -func (validator atMostSumOfValidator) Description(_ context.Context) string { +func (av atMostSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, p := range validator.attributesToSumPathExpressions { + for _, p := range av.attributesToSumPathExpressions { attributePaths = append(attributePaths, p.String()) } @@ -31,22 +31,23 @@ func (validator atMostSumOfValidator) Description(_ context.Context) string { } // MarkdownDescription describes the validation in Markdown formatting. -func (validator atMostSumOfValidator) MarkdownDescription(ctx context.Context) string { - return validator.Description(ctx) +func (av atMostSumOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) } // Validate performs the validation. -func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { +func (av atMostSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { i, ok := validateInt(ctx, request, response) - if !ok { return } - var sumOfAttribs int64 - var numUnknownAttribsToSum int + // Ensure input path expressions resolution against the current attribute + expressions := request.AttributePathExpression.MergeExpressions(av.attributesToSumPathExpressions...) - for _, expression := range validator.attributesToSumPathExpressions { + // Sum the value of all the attributes involved, but only if they are all known. + var sumOfAttribs int64 + for _, expression := range expressions { matchedPaths, diags := request.Config.PathMatches(ctx, expression) response.Diagnostics.Append(diags...) @@ -56,8 +57,13 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd } for _, mp := range matchedPaths { - var attribToSum types.Int64 + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.AttributePath) { + continue + } + var attribToSum types.Int64 diags := request.Config.GetAttribute(ctx, mp, &attribToSum) response.Diagnostics.Append(diags...) @@ -66,12 +72,13 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd continue } - if attribToSum.IsNull() { - continue + // Delay validation until all involved attribute have a known value + if attribToSum.IsUnknown() { + return } - if attribToSum.IsUnknown() { - numUnknownAttribsToSum++ + // Attribute is null, so it doesn't contribute to the sum + if attribToSum.IsNull() { continue } @@ -79,14 +86,10 @@ func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsd } } - if numUnknownAttribsToSum > 0 { - return - } - if i > sumOfAttribs { response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, - validator.Description(ctx), + av.Description(ctx), fmt.Sprintf("%d", i), )) diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go index d659cbe6..ec106ac3 100644 --- a/int64validator/equal_to_sum_of.go +++ b/int64validator/equal_to_sum_of.go @@ -21,9 +21,9 @@ type equalToSumOfValidator struct { } // Description describes the validation in plain text formatting. -func (validator equalToSumOfValidator) Description(_ context.Context) string { +func (av equalToSumOfValidator) Description(_ context.Context) string { var attributePaths []string - for _, p := range validator.attributesToSumPathExpressions { + for _, p := range av.attributesToSumPathExpressions { attributePaths = append(attributePaths, p.String()) } @@ -31,22 +31,23 @@ func (validator equalToSumOfValidator) Description(_ context.Context) string { } // MarkdownDescription describes the validation in Markdown formatting. -func (validator equalToSumOfValidator) MarkdownDescription(ctx context.Context) string { - return validator.Description(ctx) +func (av equalToSumOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) } // Validate performs the validation. -func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { +func (av equalToSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { i, ok := validateInt(ctx, request, response) - if !ok { return } - var sumOfAttribs int64 - var numUnknownAttribsToSum int + // Ensure input path expressions resolution against the current attribute + expressions := request.AttributePathExpression.MergeExpressions(av.attributesToSumPathExpressions...) - for _, expression := range validator.attributesToSumPathExpressions { + // Sum the value of all the attributes involved, but only if they are all known. + var sumOfAttribs int64 + for _, expression := range expressions { matchedPaths, diags := request.Config.PathMatches(ctx, expression) response.Diagnostics.Append(diags...) @@ -56,8 +57,13 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs } for _, mp := range matchedPaths { - var attribToSum types.Int64 + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.AttributePath) { + continue + } + var attribToSum types.Int64 diags := request.Config.GetAttribute(ctx, mp, &attribToSum) response.Diagnostics.Append(diags...) @@ -66,12 +72,13 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs continue } - if attribToSum.IsNull() { - continue + // Delay validation until all involved attribute have a known value + if attribToSum.IsUnknown() { + return } - if attribToSum.IsUnknown() { - numUnknownAttribsToSum++ + // Attribute is null, so it doesn't contribute to the sum + if attribToSum.IsNull() { continue } @@ -79,14 +86,10 @@ func (validator equalToSumOfValidator) Validate(ctx context.Context, request tfs } } - if numUnknownAttribsToSum > 0 { - return - } - if i != sumOfAttribs { response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( request.AttributePath, - validator.Description(ctx), + av.Description(ctx), fmt.Sprintf("%d", i), )) From 66aea58a7206c2dfaa3fb7fe16a9b775a0f3890b Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Tue, 19 Jul 2022 11:39:41 +0100 Subject: [PATCH 08/11] Preparing CHANGELOG entry --- .changelog/29.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/29.txt diff --git a/.changelog/29.txt b/.changelog/29.txt new file mode 100644 index 00000000..5f46c890 --- /dev/null +++ b/.changelog/29.txt @@ -0,0 +1,3 @@ +```release-note:feature +int64validator: New validators `AtLeastSumOf`, `AtMostSumOf` and `EqualToSumOf`, that compare value of an attribute, against the sum of the value of other attributes +``` From 9c2061dfb74ee7bdee5d5bba6d314ea649a235e1 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Tue, 19 Jul 2022 15:27:23 +0100 Subject: [PATCH 09/11] Rely on 'tfsdk.ValueAs' to do type validation --- int64validator/type_validation.go | 16 +++------ int64validator/type_validation_test.go | 45 +++++++++++++++++++------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/int64validator/type_validation.go b/int64validator/type_validation.go index 383ac6e5..77a64026 100644 --- a/int64validator/type_validation.go +++ b/int64validator/type_validation.go @@ -3,26 +3,20 @@ package int64validator import ( "context" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) // validateInt ensures that the request contains an Int64 value. func validateInt(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) (int64, bool) { - t := request.AttributeConfig.Type(ctx) - if t != types.Int64Type { - response.Diagnostics.Append(validatordiag.InvalidAttributeTypeDiagnostic( - request.AttributePath, - "Expected value of type int64", - t.String(), - )) + var i types.Int64 + diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &i) + response.Diagnostics.Append(diags...) + if diags.HasError() { return 0, false } - i := request.AttributeConfig.(types.Int64) - - if i.Unknown || i.Null { + if i.IsUnknown() || i.IsNull() { return 0, false } diff --git a/int64validator/type_validation_test.go b/int64validator/type_validation_test.go index 596b51e7..c651b0f7 100644 --- a/int64validator/type_validation_test.go +++ b/int64validator/type_validation_test.go @@ -14,9 +14,11 @@ func TestValidateInt(t *testing.T) { t.Parallel() testCases := map[string]struct { - request tfsdk.ValidateAttributeRequest - expectedInt64 int64 - expectedOk bool + request tfsdk.ValidateAttributeRequest + expectedInt64 int64 + expectedOk bool + expectedDiagSummary string + expectedDiagDetail string }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ @@ -24,8 +26,10 @@ func TestValidateInt(t *testing.T) { AttributePath: path.Root("test"), AttributePathExpression: path.MatchRoot("test"), }, - expectedInt64: 0.0, - expectedOk: false, + expectedInt64: 0.0, + expectedOk: false, + expectedDiagSummary: "Value Conversion Error", + expectedDiagDetail: "An unexpected error was encountered trying to convert into a Terraform value. This is always an error in the provider. Please report the following to the provider developer:\n\nCannot use attr.Value types.Int64, only types.Bool is supported because types.primitive is the type in the schema", }, "int64-null": { request: tfsdk.ValidateAttributeRequest{ @@ -33,8 +37,10 @@ func TestValidateInt(t *testing.T) { AttributePath: path.Root("test"), AttributePathExpression: path.MatchRoot("test"), }, - expectedInt64: 0.0, - expectedOk: false, + expectedInt64: 0.0, + expectedOk: false, + expectedDiagSummary: "", + expectedDiagDetail: "", }, "int64-value": { request: tfsdk.ValidateAttributeRequest{ @@ -51,8 +57,10 @@ func TestValidateInt(t *testing.T) { AttributePath: path.Root("test"), AttributePathExpression: path.MatchRoot("test"), }, - expectedInt64: 0.0, - expectedOk: false, + expectedInt64: 0.0, + expectedOk: false, + expectedDiagSummary: "", + expectedDiagDetail: "", }, } @@ -62,10 +70,25 @@ func TestValidateInt(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - gotInt64, gotOk := validateInt(context.Background(), testCase.request, &tfsdk.ValidateAttributeResponse{}) + res := tfsdk.ValidateAttributeResponse{} + gotInt64, gotOk := validateInt(context.Background(), testCase.request, &res) + + if res.Diagnostics.HasError() { + if res.Diagnostics.ErrorsCount() != 1 { + t.Errorf("expected an error but found none") + } else { + if diff := cmp.Diff(res.Diagnostics[0].Summary(), testCase.expectedDiagSummary); diff != "" { + t.Errorf("unexpected diagnostic summary difference: %s", diff) + } + + if diff := cmp.Diff(res.Diagnostics[0].Detail(), testCase.expectedDiagDetail); diff != "" { + t.Errorf("unexpected diagnostic summary difference: %s", diff) + } + } + } if diff := cmp.Diff(gotInt64, testCase.expectedInt64); diff != "" { - t.Errorf("unexpected float64 difference: %s", diff) + t.Errorf("unexpected int64 difference: %s", diff) } if diff := cmp.Diff(gotOk, testCase.expectedOk); diff != "" { From 9fb4d535640c1667dc245b5112aa824b713fd69b Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 20 Jul 2022 09:31:26 +0100 Subject: [PATCH 10/11] PR review --- int64validator/at_least_sum_of.go | 22 ++++++++++++++-------- int64validator/at_most_sum_of.go | 22 ++++++++++++++-------- int64validator/equal_to_sum_of.go | 22 ++++++++++++++-------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/int64validator/at_least_sum_of.go b/int64validator/at_least_sum_of.go index f1075bf9..ee0fe56c 100644 --- a/int64validator/at_least_sum_of.go +++ b/int64validator/at_least_sum_of.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -63,22 +64,27 @@ func (av atLeastSumOfValidator) Validate(ctx context.Context, request tfsdk.Vali continue } - var attribToSum types.Int64 - diags := request.Config.GetAttribute(ctx, mp, &attribToSum) + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) response.Diagnostics.Append(diags...) - - // Collect all errors if diags.HasError() { continue } - // Delay validation until all involved attribute have a known value - if attribToSum.IsUnknown() { + if matchedValue.IsUnknown() { return } - // Attribute is null, so it doesn't contribute to the sum - if attribToSum.IsNull() { + if matchedValue.IsNull() { + continue + } + + // We know there is a value, convert it to the expected type + var attribToSum types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) + response.Diagnostics.Append(diags...) + if diags.HasError() { continue } diff --git a/int64validator/at_most_sum_of.go b/int64validator/at_most_sum_of.go index 87748fd9..1c1aae8e 100644 --- a/int64validator/at_most_sum_of.go +++ b/int64validator/at_most_sum_of.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -63,22 +64,27 @@ func (av atMostSumOfValidator) Validate(ctx context.Context, request tfsdk.Valid continue } - var attribToSum types.Int64 - diags := request.Config.GetAttribute(ctx, mp, &attribToSum) + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) response.Diagnostics.Append(diags...) - - // Collect all errors if diags.HasError() { continue } - // Delay validation until all involved attribute have a known value - if attribToSum.IsUnknown() { + if matchedValue.IsUnknown() { return } - // Attribute is null, so it doesn't contribute to the sum - if attribToSum.IsNull() { + if matchedValue.IsNull() { + continue + } + + // We know there is a value, convert it to the expected type + var attribToSum types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) + response.Diagnostics.Append(diags...) + if diags.HasError() { continue } diff --git a/int64validator/equal_to_sum_of.go b/int64validator/equal_to_sum_of.go index ec106ac3..bbb22892 100644 --- a/int64validator/equal_to_sum_of.go +++ b/int64validator/equal_to_sum_of.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -63,22 +64,27 @@ func (av equalToSumOfValidator) Validate(ctx context.Context, request tfsdk.Vali continue } - var attribToSum types.Int64 - diags := request.Config.GetAttribute(ctx, mp, &attribToSum) + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) response.Diagnostics.Append(diags...) - - // Collect all errors if diags.HasError() { continue } - // Delay validation until all involved attribute have a known value - if attribToSum.IsUnknown() { + if matchedValue.IsUnknown() { return } - // Attribute is null, so it doesn't contribute to the sum - if attribToSum.IsNull() { + if matchedValue.IsNull() { + continue + } + + // We know there is a value, convert it to the expected type + var attribToSum types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) + response.Diagnostics.Append(diags...) + if diags.HasError() { continue } From 5463f13dc86934e29bfd4ca650bb67a1b1a04125 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 20 Jul 2022 16:15:14 +0100 Subject: [PATCH 11/11] Updated changelog entry to match other entires --- .changelog/29.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changelog/29.txt b/.changelog/29.txt index 5f46c890..334b4f5b 100644 --- a/.changelog/29.txt +++ b/.changelog/29.txt @@ -1,3 +1,3 @@ -```release-note:feature -int64validator: New validators `AtLeastSumOf`, `AtMostSumOf` and `EqualToSumOf`, that compare value of an attribute, against the sum of the value of other attributes +```release-note:enhancement +int64validator: Added `AtLeastSumOf()`, `AtMostSumOf()` and `EqualToSumOf()` validation functions ```