Skip to content

Adding List element validation for ValuesAre #37

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 7 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/11.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: typically these files are associated with the PR number, rather than the issue, just so its easy to handle PRs that close multiple issues

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed 11.txt => 37.txt.

Introduced `listvalidator` package with `ValuesAre()` validation functions
```
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 0.3.0 (unreleased)
Copy link
Contributor

Choose a reason for hiding this comment

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

This file will be handled prior to release to prevent merge conflicts across PRs: https://github.com/hashicorp/terraform-plugin-framework-validators/blob/main/.github/CONTRIBUTING.md#releases

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed changes from CHANGELOG.md.


FEATURES:
* Introduced `listvalidator` package with `ValuesAre()` validation functions ([#11](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/11))

# 0.2.0 (June 7, 2022)

BREAKING CHANGES:
Expand Down
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)
}
})
}
}