From 0635e3f9f1b34efb80a8ea09b936f89de8f016e2 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 21 Jun 2022 10:35:45 +0100 Subject: [PATCH 01/11] Adding Set element validation for ValuesAre (#12) --- setvalidator/type_validation.go | 28 +++++++ setvalidator/type_validation_test.go | 82 ++++++++++++++++++++ setvalidator/values_are.go | 72 +++++++++++++++++ setvalidator/values_are_test.go | 111 +++++++++++++++++++++++++++ validatordiag/diag.go | 10 +++ 5 files changed, 303 insertions(+) create mode 100644 setvalidator/type_validation.go create mode 100644 setvalidator/type_validation_test.go create mode 100644 setvalidator/values_are.go create mode 100644 setvalidator/values_are_test.go diff --git a/setvalidator/type_validation.go b/setvalidator/type_validation.go new file mode 100644 index 00000000..d77ff7e3 --- /dev/null +++ b/setvalidator/type_validation.go @@ -0,0 +1,28 @@ +package setvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// validateSet ensures that the request contains a Set value. +func validateSet(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) ([]attr.Value, bool) { + var n types.Set + + diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &n) + + if diags.HasError() { + response.Diagnostics = append(response.Diagnostics, diags...) + + return nil, false + } + + if n.Unknown || n.Null { + return nil, false + } + + return n.Elems, true +} diff --git a/setvalidator/type_validation_test.go b/setvalidator/type_validation_test.go new file mode 100644 index 00000000..2baa67c1 --- /dev/null +++ b/setvalidator/type_validation_test.go @@ -0,0 +1,82 @@ +package setvalidator + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "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 TestValidateSet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + request tfsdk.ValidateAttributeRequest + expectedSetElems []attr.Value + expectedOk bool + }{ + "invalid-type": { + request: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.Bool{Value: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + }, + expectedSetElems: nil, + expectedOk: false, + }, + "set-null": { + request: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.Set{Null: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + }, + expectedSetElems: nil, + expectedOk: false, + }, + "set-unknown": { + request: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.Set{Unknown: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + }, + expectedSetElems: nil, + expectedOk: false, + }, + "set-value": { + request: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + }, + expectedSetElems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + expectedOk: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotSetElems, gotOk := validateSet(context.Background(), testCase.request, &tfsdk.ValidateAttributeResponse{}) + + if diff := cmp.Diff(gotSetElems, testCase.expectedSetElems); diff != "" { + t.Errorf("unexpected float64 difference: %s", diff) + } + + if diff := cmp.Diff(gotOk, testCase.expectedOk); diff != "" { + t.Errorf("unexpected ok difference: %s", diff) + } + }) + } +} diff --git a/setvalidator/values_are.go b/setvalidator/values_are.go new file mode 100644 index 00000000..31f130be --- /dev/null +++ b/setvalidator/values_are.go @@ -0,0 +1,72 @@ +package setvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = valuesAreValidator{} + +// valuesAreValidator validates that each set member validates against each of the value validators. +type valuesAreValidator struct { + valueValidators []tfsdk.AttributeValidator +} + +// Description describes the validation in plain text formatting. +func (v valuesAreValidator) Description(ctx context.Context) string { + var descriptions []string + for _, validator := range v.valueValidators { + descriptions = append(descriptions, validator.Description(ctx)) + } + + return fmt.Sprintf("value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valuesAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + elems, ok := validateSet(ctx, req, resp) + if !ok { + return + } + + for k, elem := range elems { + value, err := elem.ToTerraformValue(ctx) + if err != nil { + resp.Diagnostics.Append(validatordiag.AttributeValueTerraformValueDiagnostic( + req.AttributePath, + fmt.Sprintf("element at index: %d cannot be converted to Terraform value", k), + err.Error(), + )) + return + } + + request := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyValue(value), + AttributeConfig: elem, + Config: req.Config, + } + + for _, validator := range v.valueValidators { + validator.Validate(ctx, request, resp) + if resp.Diagnostics.HasError() { + return + } + } + } +} + +func ValuesAre(valueValidators ...tfsdk.AttributeValidator) tfsdk.AttributeValidator { + return valuesAreValidator{ + valueValidators: valueValidators, + } +} diff --git a/setvalidator/values_are_test.go b/setvalidator/values_are_test.go new file mode 100644 index 00000000..ef1a17e0 --- /dev/null +++ b/setvalidator/values_are_test.go @@ -0,0 +1,111 @@ +package setvalidator + +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" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" +) + +func TestAtLeastValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + valuesAreValidators []tfsdk.AttributeValidator + expectError bool + } + tests := map[string]testCase{ + "Set unknown": { + val: types.Set{ + Unknown: true, + }, + expectError: true, + }, + "Set null": { + val: types.Set{ + Null: true, + }, + expectError: true, + }, + "Set elems invalid": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + valuesAreValidators: []tfsdk.AttributeValidator{ + stringvalidator.LengthAtLeast(6), + }, + expectError: true, + }, + "Set elems invalid for second validator": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + valuesAreValidators: []tfsdk.AttributeValidator{ + stringvalidator.LengthAtLeast(2), + stringvalidator.LengthAtLeast(6), + }, + expectError: true, + }, + "Set elems wrong type for validator": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + valuesAreValidators: []tfsdk.AttributeValidator{ + int64validator.AtLeast(6), + }, + expectError: true, + }, + "Set elems valid": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + valuesAreValidators: []tfsdk.AttributeValidator{ + stringvalidator.LengthAtLeast(5), + }, + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/validatordiag/diag.go b/validatordiag/diag.go index c43567f2..c9f7b2ab 100644 --- a/validatordiag/diag.go +++ b/validatordiag/diag.go @@ -35,6 +35,16 @@ func AttributeValueMatchesDiagnostic(path *tftypes.AttributePath, description st ) } +// AttributeValueTerraformValueDiagnostic returns an error Diagnostic to be used when an attribute's value cannot be +// converted to terraform value. +func AttributeValueTerraformValueDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Attribute Value Terraform Value", + capitalize(description)+", err: "+value, + ) +} + // capitalize will uppercase the first letter in a UTF-8 string. func capitalize(str string) string { if str == "" { From 46824be5f807eef214cd7666094ed48e85a7d79f Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 21 Jun 2022 10:51:11 +0100 Subject: [PATCH 02/11] Renaming var (#12) --- setvalidator/type_validation.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setvalidator/type_validation.go b/setvalidator/type_validation.go index d77ff7e3..60ba6613 100644 --- a/setvalidator/type_validation.go +++ b/setvalidator/type_validation.go @@ -10,9 +10,9 @@ import ( // validateSet ensures that the request contains a Set value. func validateSet(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) ([]attr.Value, bool) { - var n types.Set + var s types.Set - diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &n) + diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &s) if diags.HasError() { response.Diagnostics = append(response.Diagnostics, diags...) @@ -20,9 +20,9 @@ func validateSet(ctx context.Context, request tfsdk.ValidateAttributeRequest, re return nil, false } - if n.Unknown || n.Null { + if s.Unknown || s.Null { return nil, false } - return n.Elems, true + return s.Elems, true } From ec677d45e72ae771d4f91e208b034494f7d38854 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 21 Jun 2022 11:53:03 +0100 Subject: [PATCH 03/11] Rename test (#12) --- setvalidator/values_are_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setvalidator/values_are_test.go b/setvalidator/values_are_test.go index ef1a17e0..ca9d785f 100644 --- a/setvalidator/values_are_test.go +++ b/setvalidator/values_are_test.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" ) -func TestAtLeastValidator(t *testing.T) { +func TestValuesAreValidator(t *testing.T) { t.Parallel() type testCase struct { From 651cf207860be9b3b63c527536529da2c46448c2 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 21 Jun 2022 14:00:10 +0100 Subject: [PATCH 04/11] Fix tests (#12) --- setvalidator/values_are_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/setvalidator/values_are_test.go b/setvalidator/values_are_test.go index ca9d785f..ce74b772 100644 --- a/setvalidator/values_are_test.go +++ b/setvalidator/values_are_test.go @@ -22,17 +22,25 @@ func TestValuesAreValidator(t *testing.T) { expectError bool } tests := map[string]testCase{ + "not Set": { + val: types.Map{ + ElemType: types.StringType, + }, + expectError: true, + }, "Set unknown": { val: types.Set{ - Unknown: true, + Unknown: true, + ElemType: types.StringType, }, - expectError: true, + expectError: false, }, "Set null": { val: types.Set{ - Null: true, + Null: true, + ElemType: types.StringType, }, - expectError: true, + expectError: false, }, "Set elems invalid": { val: types.Set{ From eb25d3f9839792f583d9fc7a290be63f2f33a917 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 22 Jun 2022 14:59:27 +0100 Subject: [PATCH 05/11] Updates following code review (#12) --- setvalidator/type_validation_test.go | 2 +- setvalidator/values_are.go | 26 +++++++++++++++----------- validatordiag/diag.go | 10 ---------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/setvalidator/type_validation_test.go b/setvalidator/type_validation_test.go index 2baa67c1..98c83cc0 100644 --- a/setvalidator/type_validation_test.go +++ b/setvalidator/type_validation_test.go @@ -71,7 +71,7 @@ func TestValidateSet(t *testing.T) { gotSetElems, gotOk := validateSet(context.Background(), testCase.request, &tfsdk.ValidateAttributeResponse{}) if diff := cmp.Diff(gotSetElems, testCase.expectedSetElems); diff != "" { - t.Errorf("unexpected float64 difference: %s", diff) + t.Errorf("unexpected set difference: %s", diff) } if diff := cmp.Diff(gotOk, testCase.expectedOk); diff != "" { diff --git a/setvalidator/values_are.go b/setvalidator/values_are.go index 31f130be..89c13b6c 100644 --- a/setvalidator/values_are.go +++ b/setvalidator/values_are.go @@ -6,8 +6,6 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - - "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" ) var _ tfsdk.AttributeValidator = valuesAreValidator{} @@ -39,14 +37,16 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr return } - for k, elem := range elems { + for _, elem := range elems { value, err := elem.ToTerraformValue(ctx) if err != nil { - resp.Diagnostics.Append(validatordiag.AttributeValueTerraformValueDiagnostic( - req.AttributePath, - fmt.Sprintf("element at index: %d cannot be converted to Terraform value", k), - err.Error(), - )) + resp.Diagnostics.AddError( + "Attribute Conversion Error During Set Element Validation", + "An unexpected error was encountered when handling the a Set element. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Attribute Conversion Error During Set Element Validation.", + ) return } @@ -58,13 +58,17 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr for _, validator := range v.valueValidators { validator.Validate(ctx, request, resp) - if resp.Diagnostics.HasError() { - return - } } } } +// ValuesAre returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a Set. +// - Contains Set elements, each of which validate against each value validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. func ValuesAre(valueValidators ...tfsdk.AttributeValidator) tfsdk.AttributeValidator { return valuesAreValidator{ valueValidators: valueValidators, diff --git a/validatordiag/diag.go b/validatordiag/diag.go index c9f7b2ab..c43567f2 100644 --- a/validatordiag/diag.go +++ b/validatordiag/diag.go @@ -35,16 +35,6 @@ func AttributeValueMatchesDiagnostic(path *tftypes.AttributePath, description st ) } -// AttributeValueTerraformValueDiagnostic returns an error Diagnostic to be used when an attribute's value cannot be -// converted to terraform value. -func AttributeValueTerraformValueDiagnostic(path *tftypes.AttributePath, description string, value string) diag.Diagnostic { - return diag.NewAttributeErrorDiagnostic( - path, - "Invalid Attribute Value Terraform Value", - capitalize(description)+", err: "+value, - ) -} - // capitalize will uppercase the first letter in a UTF-8 string. func capitalize(str string) string { if str == "" { From 6759d60fd21e3f4bd861ce1f6b09098b3368b738 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 22 Jun 2022 15:00:56 +0100 Subject: [PATCH 06/11] Adding doc.go and changelog entry (#12) --- .changelog/12.txt | 3 +++ setvalidator/doc.go | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 .changelog/12.txt create mode 100644 setvalidator/doc.go diff --git a/.changelog/12.txt b/.changelog/12.txt new file mode 100644 index 00000000..3be4d3b0 --- /dev/null +++ b/.changelog/12.txt @@ -0,0 +1,3 @@ +```release-note:feature +Introduced `setvalidator` package with `ValuesAre()` validation function +``` \ No newline at end of file diff --git a/setvalidator/doc.go b/setvalidator/doc.go new file mode 100644 index 00000000..31e34c2d --- /dev/null +++ b/setvalidator/doc.go @@ -0,0 +1,2 @@ +// Package setvalidator provides validators for types.Set attributes. +package setvalidator From d106fc2490497d5e424b3f361929f5cd9b30d2ed Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 22 Jun 2022 15:03:53 +0100 Subject: [PATCH 07/11] Updating CHANGELOG.md (#12) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4386bf05..38f70cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.3.0 (unreleased) + +FEATURES: +* Introduced `setvalidator` package with `ValuesAre()` validation function ([#12](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/12)) + # 0.2.0 (June 7, 2022) BREAKING CHANGES: From 4353987fccfa87cddddba79548a38fe0238766a0 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 23 Jun 2022 08:49:59 +0100 Subject: [PATCH 08/11] Rename .changelog file to match PR number and remove updates to CHANGELOG.md (#12) --- .changelog/{12.txt => 36.txt} | 0 CHANGELOG.md | 5 ----- 2 files changed, 5 deletions(-) rename .changelog/{12.txt => 36.txt} (100%) diff --git a/.changelog/12.txt b/.changelog/36.txt similarity index 100% rename from .changelog/12.txt rename to .changelog/36.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f70cc6..4386bf05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,3 @@ -# 0.3.0 (unreleased) - -FEATURES: -* Introduced `setvalidator` package with `ValuesAre()` validation function ([#12](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/12)) - # 0.2.0 (June 7, 2022) BREAKING CHANGES: From afe55e6f9be68b5beb007a60fda5640ffa725dea Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 21 Jun 2022 16:34:50 +0100 Subject: [PATCH 09/11] Adding Set validation for SizeAtLeast, SizeAtMost and SizeBetween (#5) --- setvalidator/size_at_least.go | 51 +++++++++++ setvalidator/size_at_least_test.go | 90 +++++++++++++++++++ setvalidator/size_at_most.go | 51 +++++++++++ setvalidator/size_at_most_test.go | 93 ++++++++++++++++++++ setvalidator/size_between.go | 54 ++++++++++++ setvalidator/size_between_test.go | 133 +++++++++++++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 setvalidator/size_at_least.go create mode 100644 setvalidator/size_at_least_test.go create mode 100644 setvalidator/size_at_most.go create mode 100644 setvalidator/size_at_most_test.go create mode 100644 setvalidator/size_between.go create mode 100644 setvalidator/size_between_test.go diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go new file mode 100644 index 00000000..e9a12cd1 --- /dev/null +++ b/setvalidator/size_at_least.go @@ -0,0 +1,51 @@ +package setvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = sizeAtLeastValidator{} + +// sizeAtLeastValidator validates that set contains at least min elements. +type sizeAtLeastValidator struct { + min int +} + +// Description describes the validation in plain text formatting. +func (v sizeAtLeastValidator) Description(ctx context.Context) string { + return fmt.Sprintf("set must contain at least %d elements", v.min) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v sizeAtLeastValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + elems, ok := validateSet(ctx, req, resp) + if !ok { + return + } + + if len(elems) < v.min { + resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + req.AttributePath, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + )) + + return + } +} + +func SizeAtLeast(min int) tfsdk.AttributeValidator { + return sizeAtLeastValidator{ + min: min, + } +} diff --git a/setvalidator/size_at_least_test.go b/setvalidator/size_at_least_test.go new file mode 100644 index 00000000..3d910f5e --- /dev/null +++ b/setvalidator/size_at_least_test.go @@ -0,0 +1,90 @@ +package setvalidator + +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 TestSizeAtLeastValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + min int + expectError bool + } + tests := map[string]testCase{ + "not a Set": { + val: types.Bool{Value: true}, + expectError: true, + }, + "Set unknown": { + val: types.Set{ + Unknown: true, + ElemType: types.StringType, + }, + expectError: false, + }, + "Set null": { + val: types.Set{ + Null: true, + ElemType: types.StringType, + }, + expectError: false, + }, + "Set size greater than min": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + min: 1, + expectError: false, + }, + "Set size equal to min": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + }, + }, + min: 1, + expectError: false, + }, + "Set size less than min": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{}, + }, + min: 1, + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + SizeAtLeast(test.min).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go new file mode 100644 index 00000000..41d6321c --- /dev/null +++ b/setvalidator/size_at_most.go @@ -0,0 +1,51 @@ +package setvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = sizeAtMostValidator{} + +// sizeAtMostValidator validates that set contains at most max elements. +type sizeAtMostValidator struct { + max int +} + +// Description describes the validation in plain text formatting. +func (v sizeAtMostValidator) Description(ctx context.Context) string { + return fmt.Sprintf("set must contain at most %d elements", v.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v sizeAtMostValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + elems, ok := validateSet(ctx, req, resp) + if !ok { + return + } + + if len(elems) > v.max { + resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + req.AttributePath, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + )) + + return + } +} + +func SizeAtMost(max int) tfsdk.AttributeValidator { + return sizeAtMostValidator{ + max: max, + } +} diff --git a/setvalidator/size_at_most_test.go b/setvalidator/size_at_most_test.go new file mode 100644 index 00000000..781d92cb --- /dev/null +++ b/setvalidator/size_at_most_test.go @@ -0,0 +1,93 @@ +package setvalidator + +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 TestSizeAtMostValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + max int + expectError bool + } + tests := map[string]testCase{ + "not a Set": { + val: types.Bool{Value: true}, + expectError: true, + }, + "Set unknown": { + val: types.Set{ + Unknown: true, + ElemType: types.StringType, + }, + expectError: false, + }, + "Set null": { + val: types.Set{ + Null: true, + ElemType: types.StringType, + }, + expectError: false, + }, + "Set size less than max": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + }, + }, + max: 2, + expectError: false, + }, + "Set size equal to max": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + max: 2, + expectError: false, + }, + "Set size greater than max": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + types.String{Value: "third"}, + }}, + max: 2, + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + SizeAtMost(test.max).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go new file mode 100644 index 00000000..fc726ac9 --- /dev/null +++ b/setvalidator/size_between.go @@ -0,0 +1,54 @@ +package setvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + + "github.com/hashicorp/terraform-plugin-framework-validators/validatordiag" +) + +var _ tfsdk.AttributeValidator = sizeBetweenValidator{} + +// sizeBetweenValidator validates that set contains at least min elements +// and at most max elements. +type sizeBetweenValidator struct { + min int + max int +} + +// Description describes the validation in plain text formatting. +func (v sizeBetweenValidator) Description(ctx context.Context) string { + return fmt.Sprintf("set must contain at least %d elements and at most %d elements", v.min, v.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v sizeBetweenValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + elems, ok := validateSet(ctx, req, resp) + if !ok { + return + } + + if len(elems) < v.min || len(elems) > v.max { + resp.Diagnostics.Append(validatordiag.AttributeValueDiagnostic( + req.AttributePath, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + )) + + return + } +} + +func SizeBetween(min, max int) tfsdk.AttributeValidator { + return sizeBetweenValidator{ + min: min, + max: max, + } +} diff --git a/setvalidator/size_between_test.go b/setvalidator/size_between_test.go new file mode 100644 index 00000000..5c5e7997 --- /dev/null +++ b/setvalidator/size_between_test.go @@ -0,0 +1,133 @@ +package setvalidator + +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 TestSizeBetweenValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val attr.Value + min int + max int + expectError bool + } + tests := map[string]testCase{ + "not a Set": { + val: types.Bool{Value: true}, + expectError: true, + }, + "Set unknown": { + val: types.Set{ + Unknown: true, + ElemType: types.StringType, + }, + expectError: false, + }, + "Set null": { + val: types.Set{ + Null: true, + ElemType: types.StringType, + }, + expectError: false, + }, + "Set size greater than min": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + min: 1, + max: 3, + expectError: false, + }, + "Set size equal to min": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + }, + }, + min: 1, + max: 3, + expectError: false, + }, + "Set size less than max": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + }, + }, + min: 1, + max: 3, + expectError: false, + }, + "Set size equal to max": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + types.String{Value: "third"}, + }, + }, + min: 1, + max: 3, + expectError: false, + }, + "Set size less than min": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{}, + }, + min: 1, + max: 3, + expectError: true, + }, + "Set size greater than max": { + val: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "first"}, + types.String{Value: "second"}, + types.String{Value: "third"}, + types.String{Value: "fourth"}, + }, + }, + min: 1, + max: 3, + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + AttributeConfig: test.val, + } + response := tfsdk.ValidateAttributeResponse{} + SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} From d6a993cf5e9e3ae7248c552bb9f1deba32b006f1 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 22 Jun 2022 15:31:29 +0100 Subject: [PATCH 10/11] Update following code review (#5) --- .changelog/5.txt | 3 +++ setvalidator/size_at_least.go | 7 +++++++ setvalidator/size_at_most.go | 7 +++++++ setvalidator/size_between.go | 7 +++++++ 4 files changed, 24 insertions(+) create mode 100644 .changelog/5.txt diff --git a/.changelog/5.txt b/.changelog/5.txt new file mode 100644 index 00000000..b966ea7f --- /dev/null +++ b/.changelog/5.txt @@ -0,0 +1,3 @@ +```release-note:feature +Added `SizeAtLeast()`, `SizeAtMost()` and `SizeBetween` validation functions to `setvalidator` package +``` \ No newline at end of file diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go index e9a12cd1..c3d99e3b 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -44,6 +44,13 @@ func (v sizeAtLeastValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } } +// SizeAtLeast returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a Set. +// - Contains at least min elements. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. func SizeAtLeast(min int) tfsdk.AttributeValidator { return sizeAtLeastValidator{ min: min, diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go index 41d6321c..8fe1cc91 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -44,6 +44,13 @@ func (v sizeAtMostValidator) Validate(ctx context.Context, req tfsdk.ValidateAtt } } +// SizeAtMost returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a Set. +// - Contains at most max elements. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. func SizeAtMost(max int) tfsdk.AttributeValidator { return sizeAtMostValidator{ max: max, diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go index fc726ac9..894d327a 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -46,6 +46,13 @@ func (v sizeBetweenValidator) Validate(ctx context.Context, req tfsdk.ValidateAt } } +// SizeBetween returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a Set. +// - Contains at least min elements and at most max elements. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. func SizeBetween(min, max int) tfsdk.AttributeValidator { return sizeBetweenValidator{ min: min, From e71aeeffda81cf950422910ab9e9c42bfd673baf Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 23 Jun 2022 08:56:50 +0100 Subject: [PATCH 11/11] Rename .changelog file to match PR number (#5) --- .changelog/{5.txt => 40.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{5.txt => 40.txt} (100%) diff --git a/.changelog/5.txt b/.changelog/40.txt similarity index 100% rename from .changelog/5.txt rename to .changelog/40.txt