Skip to content
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

Adding List element validation for ValuesAre #37

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/37.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Introduced `listvalidator` package with `ValuesAre()` validation functions
```
2 changes: 2 additions & 0 deletions listvalidator/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package listvalidator provides validators for types.List attributes.
package listvalidator
28 changes: 28 additions & 0 deletions listvalidator/type_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package listvalidator

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// validateList ensures that the request contains a List value.
func validateList(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) ([]attr.Value, bool) {
var l types.List

diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &l)

if diags.HasError() {
response.Diagnostics = append(response.Diagnostics, diags...)

return nil, false
}

if l.Unknown || l.Null {
return nil, false
}

return l.Elems, true
}
82 changes: 82 additions & 0 deletions listvalidator/type_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package listvalidator

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 TestValidateList(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
request tfsdk.ValidateAttributeRequest
expectedListElems []attr.Value
expectedOk bool
}{
"invalid-type": {
request: tfsdk.ValidateAttributeRequest{
AttributeConfig: types.Bool{Value: true},
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
},
expectedListElems: nil,
expectedOk: false,
},
"list-null": {
request: tfsdk.ValidateAttributeRequest{
AttributeConfig: types.List{Null: true},
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
},
expectedListElems: nil,
expectedOk: false,
},
"list-unknown": {
request: tfsdk.ValidateAttributeRequest{
AttributeConfig: types.List{Unknown: true},
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
},
expectedListElems: nil,
expectedOk: false,
},
"list-value": {
request: tfsdk.ValidateAttributeRequest{
AttributeConfig: types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "first"},
types.String{Value: "second"},
},
},
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
},
expectedListElems: []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()

gotListElems, gotOk := validateList(context.Background(), testCase.request, &tfsdk.ValidateAttributeResponse{})

if diff := cmp.Diff(gotListElems, testCase.expectedListElems); diff != "" {
t.Errorf("unexpected float64 difference: %s", diff)
}

if diff := cmp.Diff(gotOk, testCase.expectedOk); diff != "" {
t.Errorf("unexpected ok difference: %s", diff)
}
})
}
}
64 changes: 64 additions & 0 deletions listvalidator/values_are.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package listvalidator

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)

var _ tfsdk.AttributeValidator = valuesAreValidator{}

// valuesAreValidator validates that each list 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 := validateList(ctx, req, resp)
if !ok {
return
}

for k, elem := range elems {
request := tfsdk.ValidateAttributeRequest{
AttributePath: req.AttributePath.WithElementKeyInt(k),
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 List.
// - That contains list 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 listvalidator/values_are_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package listvalidator

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
expectError bool
}
tests := map[string]testCase{
"not List": {
val: types.Set{
ElemType: types.StringType,
},
expectError: true,
},
"List unknown": {
val: types.List{
Unknown: true,
ElemType: types.StringType,
},
expectError: false,
},
"List null": {
val: types.List{
Null: true,
ElemType: types.StringType,
},
expectError: false,
},
"List elems invalid": {
val: types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "first"},
types.String{Value: "second"},
},
},
valuesAreValidators: []tfsdk.AttributeValidator{
stringvalidator.LengthAtLeast(6),
},
expectError: true,
},
"List elems invalid for second validator": {
val: types.List{
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,
},
"List elems wrong type for validator": {
val: types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "first"},
types.String{Value: "second"},
},
},
valuesAreValidators: []tfsdk.AttributeValidator{
int64validator.AtLeast(6),
},
expectError: true,
},
"List elems valid": {
val: types.List{
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)
}
})
}
}