Skip to content

Commit

Permalink
Add equalToProductOfValidator validator to the int64validator pac…
Browse files Browse the repository at this point in the history
…kage (#129)

* Add an `equal_to_product_of` validator to the `int64validator` package

* Add Changie entry
  • Loading branch information
SBGoods authored May 2, 2023
1 parent 0795efe commit 6d6816a
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20230502-112440.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: 'int64validator: Added `equalToProductOf` validator'
time: 2023-05-02T11:24:40.147812-04:00
custom:
Issue: "129"
113 changes: 113 additions & 0 deletions int64validator/equal_to_product_of.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package int64validator

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
)

var _ validator.Int64 = equalToProductOfValidator{}

// equalToProductOfValidator validates that an integer Attribute's value equals the product of one
// or more integer Attributes retrieved via the given path expressions.
type equalToProductOfValidator struct {
attributesToMultiplyPathExpressions path.Expressions
}

// Description describes the validation in plain text formatting.
func (av equalToProductOfValidator) Description(_ context.Context) string {
var attributePaths []string
for _, p := range av.attributesToMultiplyPathExpressions {
attributePaths = append(attributePaths, p.String())
}

return fmt.Sprintf("value must be equal to the product of %s", strings.Join(attributePaths, " + "))
}

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

// ValidateInt64 performs the validation.
func (av equalToProductOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

// Ensure input path expressions resolution against the current attribute
expressions := request.PathExpression.MergeExpressions(av.attributesToMultiplyPathExpressions...)

// Multiply the value of all the attributes involved, but only if they are all known.
productOfAttribs := int64(1)
for _, expression := range expressions {
matchedPaths, diags := request.Config.PathMatches(ctx, expression)
response.Diagnostics.Append(diags...)

// Collect all errors
if diags.HasError() {
continue
}

for _, mp := range matchedPaths {
// If the user specifies the same attribute this validator is applied to,
// also as part of the input, skip it
if mp.Equal(request.Path) {
continue
}

// Get the value
var matchedValue attr.Value
diags := request.Config.GetAttribute(ctx, mp, &matchedValue)
response.Diagnostics.Append(diags...)
if diags.HasError() {
continue
}

if matchedValue.IsUnknown() {
return
}

if matchedValue.IsNull() {
return
}

// We know there is a value, convert it to the expected type
var attribToMultiply types.Int64
diags = tfsdk.ValueAs(ctx, matchedValue, &attribToMultiply)
response.Diagnostics.Append(diags...)
if diags.HasError() {
continue
}

productOfAttribs *= attribToMultiply.ValueInt64()
}
}

if request.ConfigValue.ValueInt64() != productOfAttribs {
response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
request.Path,
av.Description(ctx),
fmt.Sprintf("%d", request.ConfigValue.ValueInt64()),
))
}
}

// EqualToProductOf returns an AttributeValidator which ensures that any configured
// attribute value:
//
// - Is a number, which can be represented by a 64-bit integer.
// - Is equal to the product of the given attributes retrieved via the given path expression(s).
//
// Validation is skipped if any null (unconfigured) and/or unknown (known after apply) values are present.
func EqualToProductOf(attributesToMultiplyPathExpressions ...path.Expression) validator.Int64 {
return equalToProductOfValidator{attributesToMultiplyPathExpressions}
}
34 changes: 34 additions & 0 deletions int64validator/equal_to_product_of_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package int64validator_test

import (
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"

"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
)

func ExampleEqualToProductOf() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attr": schema.Int64Attribute{
Required: true,
Validators: []validator.Int64{
// Validate this integer value must be equal to the
// product of integer values other_attr1 and other_attr2.
int64validator.EqualToProductOf(path.Expressions{
path.MatchRoot("other_attr1"),
path.MatchRoot("other_attr2"),
}...),
},
},
"other_attr1": schema.Int64Attribute{
Required: true,
},
"other_attr2": schema.Int64Attribute{
Required: true,
},
},
}
}
179 changes: 179 additions & 0 deletions int64validator/equal_to_product_of_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package int64validator

import (
"context"
"testing"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestEqualToProductOfValidator(t *testing.T) {
t.Parallel()

type testCase struct {
val types.Int64
attributesToMultiplyPathExpressions path.Expressions
requestConfigRaw map[string]tftypes.Value
expectError bool
}
tests := map[string]testCase{
"unknown Int64": {
val: types.Int64Unknown(),
},
"null Int64": {
val: types.Int64Null(),
},
"valid integer as Int64 more than product of attributes": {
val: types.Int64Value(26),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, 5),
"two": tftypes.NewValue(tftypes.Number, 5),
},
expectError: true,
},
"valid integer as Int64 less than product of attributes": {
val: types.Int64Value(24),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, 5),
"two": tftypes.NewValue(tftypes.Number, 5),
},
expectError: true,
},
"valid integer as Int64 equal to product of attributes": {
val: types.Int64Value(25),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, 5),
"two": tftypes.NewValue(tftypes.Number, 5),
},
},
"validation skipped when one attribute is null": {
val: types.Int64Value(10),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, nil),
"two": tftypes.NewValue(tftypes.Number, 8),
},
},
"validation skipped when all attributes are null": {
val: types.Int64Null(),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, nil),
"two": tftypes.NewValue(tftypes.Number, nil),
},
},
"validation skipped when all attributes to multiply are null": {
val: types.Int64Value(1),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, nil),
"two": tftypes.NewValue(tftypes.Number, nil),
},
},
"validation skipped when one attribute is unknown": {
val: types.Int64Value(10),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
"two": tftypes.NewValue(tftypes.Number, 8),
},
},
"validation skipped when all attributes are unknown": {
val: types.Int64Unknown(),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
"two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
},
},
"validation skipped when all attributes to multiply are unknown": {
val: types.Int64Value(1),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
"two": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
},
},
"error when attribute to multiply is not Number": {
val: types.Int64Value(9),
attributesToMultiplyPathExpressions: path.Expressions{
path.MatchRoot("one"),
path.MatchRoot("two"),
},
requestConfigRaw: map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Bool, true),
"two": tftypes.NewValue(tftypes.Number, 9),
},
expectError: true,
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()
request := validator.Int64Request{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: test.val,
Config: tfsdk.Config{
Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw),
Schema: schema.Schema{
Attributes: map[string]schema.Attribute{
"test": schema.Int64Attribute{},
"one": schema.Int64Attribute{},
"two": schema.Int64Attribute{},
},
},
},
}

response := validator.Int64Response{}

EqualToProductOf(test.attributesToMultiplyPathExpressions...).ValidateInt64(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 6d6816a

Please sign in to comment.