-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
equalToProductOfValidator
validator to the int64validator
pac…
…kage (#129) * Add an `equal_to_product_of` validator to the `int64validator` package * Add Changie entry
- Loading branch information
Showing
4 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |