Skip to content

Commit

Permalink
Adding atMostSumOf int64 validator (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
bendbennett committed May 30, 2022
1 parent ebd2441 commit 5816bae
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 0 deletions.
82 changes: 82 additions & 0 deletions int64validator/at_most_sum_of.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package int64validator

import (
"context"
"fmt"
"strings"

"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/validatordiag"
)

var _ tfsdk.AttributeValidator = atMostSumOfValidator{}

// atMostSumOfValidator validates that an integer Attribute's value is at most the sum of one
// or more integer Attributes.
type atMostSumOfValidator struct {
attributesToSumPaths []*tftypes.AttributePath
}

// Description describes the validation in plain text formatting.
func (validator atMostSumOfValidator) Description(_ context.Context) string {
var attributePaths []string
for _, path := range validator.attributesToSumPaths {
attributePaths = append(attributePaths, path.String())
}

return fmt.Sprintf("value must be at most sum of %s", strings.Join(attributePaths, " + "))
}

// MarkdownDescription describes the validation in Markdown formatting.
func (validator atMostSumOfValidator) MarkdownDescription(ctx context.Context) string {
return validator.Description(ctx)
}

// Validate performs the validation.
func (validator atMostSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) {
i, ok := validateInt(ctx, request, response)

if !ok {
return
}

var sumOfAttribs int64

for _, path := range validator.attributesToSumPaths {
var attribToSum types.Int64

response.Diagnostics.Append(request.Config.GetAttribute(ctx, path, &attribToSum)...)
if response.Diagnostics.HasError() {
return
}

sumOfAttribs += attribToSum.Value
}

if i > sumOfAttribs {

response.Diagnostics.Append(validatordiag.AttributeValueDiagnostic(
request.AttributePath,
validator.Description(ctx),
fmt.Sprintf("%d", i),
))

return
}
}

// AtMostSumOf returns an AttributeValidator which ensures that any configured
// attribute value:
//
// - Is a number, which can be represented by a 64-bit integer.
// - Is exclusively at most the sum of the given attributes.
//
// Null (unconfigured) and unknown (known after apply) values are skipped.
func AtMostSumOf(attributesToSum []*tftypes.AttributePath) tfsdk.AttributeValidator {
return atMostSumOfValidator{
attributesToSumPaths: attributesToSum,
}
}
155 changes: 155 additions & 0 deletions int64validator/at_most_sum_of_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package int64validator

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

type testCase struct {
val attr.Value
attributesToSumPaths []*tftypes.AttributePath
requestConfigRaw map[string]attr.Value
expectError bool
}
tests := map[string]testCase{
"not an Int64": {
val: types.Bool{Value: true},
expectError: true,
},
"unknown Int64": {
val: types.Int64{Unknown: true},
},
"null Int64": {
val: types.Int64{Null: true},
},
"valid integer as Int64 more than sum of attributes": {
val: types.Int64{Value: 11},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Value: 5},
"two": types.Int64{Value: 5},
},
expectError: true,
},
"valid integer as Int64 equal to sum of attributes": {
val: types.Int64{Value: 10},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Value: 5},
"two": types.Int64{Value: 5},
},
},
"valid integer as Int64 less than sum of attributes": {
val: types.Int64{Value: 7},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Value: 4},
"two": types.Int64{Value: 4},
},
},
"valid integer as Int64 less than sum of attributes, when one summed attribute is null": {
val: types.Int64{Value: 8},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Null: true},
"two": types.Int64{Value: 9},
},
},
"valid integer as Int64 does not return error when all attributes are null": {
val: types.Int64{Null: true},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Null: true},
"two": types.Int64{Null: true},
},
},
"valid integer as Int64 less than sum of attributes, when one summed attribute is unknown": {
val: types.Int64{Value: 8},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Unknown: true},
"two": types.Int64{Value: 9},
},
},
"valid integer as Int64 does not return error when all attributes are unknown": {
val: types.Int64{Unknown: true},
attributesToSumPaths: []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("one"),
tftypes.NewAttributePath().WithAttributeName("two"),
},
requestConfigRaw: map[string]attr.Value{
"one": types.Int64{Unknown: true},
"two": types.Int64{Unknown: true},
},
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
reqConf := make(map[string]tftypes.Value, len(test.requestConfigRaw))

for k, v := range test.requestConfigRaw {
val, err := v.ToTerraformValue(context.Background())
if err != nil {
t.Fatalf("could not attr.Value at key:%s to tftypes.Value", k)
}

reqConf[k] = val
}

request := tfsdk.ValidateAttributeRequest{
AttributePath: tftypes.NewAttributePath().WithAttributeName("test"),
AttributeConfig: test.val,
Config: tfsdk.Config{
Raw: tftypes.NewValue(tftypes.Object{}, reqConf),
Schema: tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test": {Type: types.Int64Type},
"one": {Type: types.Int64Type},
"two": {Type: types.Int64Type},
},
},
},
}

response := tfsdk.ValidateAttributeResponse{}

AtMostSumOf(test.attributesToSumPaths).Validate(context.Background(), 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)
}
})
}
}

0 comments on commit 5816bae

Please sign in to comment.