-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Adding atLeastSumOf, atMostSumOf and equalToSumOf int64 validators (#20) * Switching to using path expressions * Do not validate if any attributes are unknown (#20) * Updating dependencies, including terraform-plugin-framework@0.10.0 * PR review: making use of the new `path.Expression` `.MergeExpressions` method * Preparing CHANGELOG entry * Rely on 'tfsdk.ValueAs' to do type validation Co-authored-by: Ivan De Marino <ivan.demarino@hashicorp.com> Co-authored-by: Brian Flad <bflad417@gmail.com>
- Loading branch information
1 parent
81783a7
commit 3ef7858
Showing
11 changed files
with
953 additions
and
36 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,3 @@ | ||
```release-note:enhancement | ||
int64validator: Added `AtLeastSumOf()`, `AtMostSumOf()` and `EqualToSumOf()` validation functions | ||
``` |
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
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
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,115 @@ | ||
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/tfsdk" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" | ||
) | ||
|
||
var _ tfsdk.AttributeValidator = atLeastSumOfValidator{} | ||
|
||
// atLeastSumOfValidator validates that an integer Attribute's value is at least the sum of one | ||
// or more integer Attributes retrieved via the given path expressions. | ||
type atLeastSumOfValidator struct { | ||
attributesToSumPathExpressions path.Expressions | ||
} | ||
|
||
// Description describes the validation in plain text formatting. | ||
func (av atLeastSumOfValidator) Description(_ context.Context) string { | ||
var attributePaths []string | ||
for _, p := range av.attributesToSumPathExpressions { | ||
attributePaths = append(attributePaths, p.String()) | ||
} | ||
|
||
return fmt.Sprintf("value must be at least sum of %s", strings.Join(attributePaths, " + ")) | ||
} | ||
|
||
// MarkdownDescription describes the validation in Markdown formatting. | ||
func (av atLeastSumOfValidator) MarkdownDescription(ctx context.Context) string { | ||
return av.Description(ctx) | ||
} | ||
|
||
// Validate performs the validation. | ||
func (av atLeastSumOfValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { | ||
i, ok := validateInt(ctx, request, response) | ||
if !ok { | ||
return | ||
} | ||
|
||
// Ensure input path expressions resolution against the current attribute | ||
expressions := request.AttributePathExpression.MergeExpressions(av.attributesToSumPathExpressions...) | ||
|
||
// Sum the value of all the attributes involved, but only if they are all known. | ||
var sumOfAttribs int64 | ||
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.AttributePath) { | ||
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() { | ||
continue | ||
} | ||
|
||
// We know there is a value, convert it to the expected type | ||
var attribToSum types.Int64 | ||
diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) | ||
response.Diagnostics.Append(diags...) | ||
if diags.HasError() { | ||
continue | ||
} | ||
|
||
sumOfAttribs += attribToSum.Value | ||
} | ||
} | ||
|
||
if i < sumOfAttribs { | ||
response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( | ||
request.AttributePath, | ||
av.Description(ctx), | ||
fmt.Sprintf("%d", i), | ||
)) | ||
|
||
return | ||
} | ||
} | ||
|
||
// AtLeastSumOf returns an AttributeValidator which ensures that any configured | ||
// attribute value: | ||
// | ||
// - Is a number, which can be represented by a 64-bit integer. | ||
// - Is at least the sum of the attributes retrieved via the given path expression(s). | ||
// | ||
// Null (unconfigured) and unknown (known after apply) values are skipped. | ||
func AtLeastSumOf(attributesToSumPathExpressions ...path.Expression) tfsdk.AttributeValidator { | ||
return atLeastSumOfValidator{attributesToSumPathExpressions} | ||
} |
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,181 @@ | ||
package int64validator | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/tfsdk" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
func TestAtLeastSumOfValidator(t *testing.T) { | ||
t.Parallel() | ||
|
||
type testCase struct { | ||
val attr.Value | ||
attributesToSumExpressions path.Expressions | ||
requestConfigRaw map[string]tftypes.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 less than sum of attributes": { | ||
val: types.Int64{Value: 10}, | ||
attributesToSumExpressions: path.Expressions{ | ||
path.MatchRoot("one"), | ||
path.MatchRoot("two"), | ||
}, | ||
requestConfigRaw: map[string]tftypes.Value{ | ||
"one": tftypes.NewValue(tftypes.Number, 15), | ||
"two": tftypes.NewValue(tftypes.Number, 15), | ||
}, | ||
expectError: true, | ||
}, | ||
"valid integer as Int64 equal to sum of attributes": { | ||
val: types.Int64{Value: 10}, | ||
attributesToSumExpressions: 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), | ||
}, | ||
}, | ||
"valid integer as Int64 greater than sum of attributes": { | ||
val: types.Int64{Value: 10}, | ||
attributesToSumExpressions: path.Expressions{ | ||
path.MatchRoot("one"), | ||
path.MatchRoot("two"), | ||
}, | ||
requestConfigRaw: map[string]tftypes.Value{ | ||
"one": tftypes.NewValue(tftypes.Number, 4), | ||
"two": tftypes.NewValue(tftypes.Number, 4), | ||
}, | ||
}, | ||
"valid integer as Int64 greater than sum of attributes, when one summed attribute is null": { | ||
val: types.Int64{Value: 10}, | ||
attributesToSumExpressions: path.Expressions{ | ||
path.MatchRoot("one"), | ||
path.MatchRoot("two"), | ||
}, | ||
requestConfigRaw: map[string]tftypes.Value{ | ||
"one": tftypes.NewValue(tftypes.Number, nil), | ||
"two": tftypes.NewValue(tftypes.Number, 9), | ||
}, | ||
}, | ||
"valid integer as Int64 does not return error when all attributes are null": { | ||
val: types.Int64{Null: true}, | ||
attributesToSumExpressions: 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), | ||
}, | ||
}, | ||
"valid integer as Int64 returns error when all attributes to sum are null": { | ||
val: types.Int64{Value: -1}, | ||
attributesToSumExpressions: 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), | ||
}, | ||
expectError: true, | ||
}, | ||
"valid integer as Int64 greater than sum of attributes, when one summed attribute is unknown": { | ||
val: types.Int64{Value: 10}, | ||
attributesToSumExpressions: 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, 9), | ||
}, | ||
}, | ||
"valid integer as Int64 does not return error when all attributes are unknown": { | ||
val: types.Int64{Unknown: true}, | ||
attributesToSumExpressions: 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), | ||
}, | ||
}, | ||
"valid integer as Int64 does not return error when all attributes to sum are unknown": { | ||
val: types.Int64{Value: -1}, | ||
attributesToSumExpressions: 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 sum is not Number": { | ||
val: types.Int64{Value: 9}, | ||
attributesToSumExpressions: 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) { | ||
request := tfsdk.ValidateAttributeRequest{ | ||
AttributePath: path.Root("test"), | ||
AttributePathExpression: path.MatchRoot("test"), | ||
AttributeConfig: test.val, | ||
Config: tfsdk.Config{ | ||
Raw: tftypes.NewValue(tftypes.Object{}, test.requestConfigRaw), | ||
Schema: tfsdk.Schema{ | ||
Attributes: map[string]tfsdk.Attribute{ | ||
"test": {Type: types.Int64Type}, | ||
"one": {Type: types.Int64Type}, | ||
"two": {Type: types.Int64Type}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
response := tfsdk.ValidateAttributeResponse{} | ||
|
||
AtLeastSumOf(test.attributesToSumExpressions...).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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.