-
Notifications
You must be signed in to change notification settings - Fork 13
Adding Set element validation for ValuesAre #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0635e3f
46824be
ec677d4
651cf20
eb25d3f
6759d60
d106fc2
4353987
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
Introduced `setvalidator` package with `ValuesAre()` validation function | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Package setvalidator provides validators for types.Set attributes. | ||
package setvalidator |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 s types.Set | ||
|
||
diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &s) | ||
|
||
if diags.HasError() { | ||
response.Diagnostics = append(response.Diagnostics, diags...) | ||
|
||
return nil, false | ||
} | ||
|
||
if s.Unknown || s.Null { | ||
return nil, false | ||
} | ||
|
||
return s.Elems, true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 set difference: %s", diff) | ||
} | ||
|
||
if diff := cmp.Diff(gotOk, testCase.expectedOk); diff != "" { | ||
t.Errorf("unexpected ok difference: %s", diff) | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package setvalidator | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/tfsdk" | ||
) | ||
|
||
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 _, elem := range elems { | ||
value, err := elem.ToTerraformValue(ctx) | ||
if err != nil { | ||
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 | ||
} | ||
|
||
request := tfsdk.ValidateAttributeRequest{ | ||
AttributePath: req.AttributePath.WithElementKeyValue(value), | ||
AttributeConfig: elem, | ||
Config: req.Config, | ||
} | ||
|
||
for _, validator := range v.valueValidators { | ||
validator.Validate(ctx, request, resp) | ||
} | ||
} | ||
} | ||
|
||
// 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, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
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 TestValuesAreValidator(t *testing.T) { | ||
t.Parallel() | ||
|
||
type testCase struct { | ||
val attr.Value | ||
valuesAreValidators []tfsdk.AttributeValidator | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use the actual attribute validator here, instead of hosting only it's input? Just curious. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thought I'd follow the same pattern we're using in other validator tests (e.g., |
||
expectError bool | ||
} | ||
tests := map[string]testCase{ | ||
"not Set": { | ||
val: types.Map{ | ||
ElemType: types.StringType, | ||
}, | ||
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 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) | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I'm gonna leave this as is if that's ok.