Skip to content

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

Merged
merged 8 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/36.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Introduced `setvalidator` package with `ValuesAre()` validation function
```
2 changes: 2 additions & 0 deletions setvalidator/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package setvalidator provides validators for types.Set attributes.
package setvalidator
28 changes: 28 additions & 0 deletions setvalidator/type_validation.go
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
}
82 changes: 82 additions & 0 deletions setvalidator/type_validation_test.go
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)
}
})
}
}
76 changes: 76 additions & 0 deletions setvalidator/values_are.go
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, " + "))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Sprintf("value must satisfy all validations: %s", strings.Join(descriptions, " + "))
return fmt.Sprintf("Attribute value must satisfy the following: %s", strings.Join(descriptions, " + "))

Just a suggestion.

Copy link
Contributor Author

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.

}

// 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,
}
}
119 changes: 119 additions & 0 deletions setvalidator/values_are_test.go
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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., int64validator.at_least_test.go).

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)
}
})
}
}