-
Notifications
You must be signed in to change notification settings - Fork 13
Adding AtLeastSumOf
, AtMostSumOf
, EqualToSumOf
Validators
#29
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
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9b4e4c0
Adding atLeastSumOf, atMostSumOf and equalToSumOf int64 validators (#20)
bendbennett 4e3474b
Checking for summing of unknown attributes and using variadic args (#20)
bendbennett 533f2df
Switching to using native framework path for AtLeastSumOf, AtMostSumO…
bendbennett f37d1cb
Switching to using path expressions for AtLeastSumOf, AtMostSumOf and…
bendbennett 1f5151c
Do not validate if any attributes are unknown (#20)
bendbennett aabdb17
Updating dependencies, including [email protected]
df358cd
PR review: making use of the new `path.Expression` `.MergeExpressions…
66aea58
Preparing CHANGELOG entry
9c2061d
Rely on 'tfsdk.ValueAs' to do type validation
9fb4d53
PR review
5463f13
Updated changelog entry to match other entires
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive-by note: It'd be great to ensure there are unit tests covering when the one/two attributes are null and/or unknown as well. Or all three are null/unknown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have added some additional tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These look great! Another good test case might be verifying that giving a path to an unexpected type (e.g. boolean attribute) always returns an error diagnostic. It appears the code should already be doing this, but it might be good to cover this for future code changes. Seems like it should be popping an additional attribute into the test schema, then referencing it in a new test case. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have added a test case for attributePath(s) referencing unexpected types in
at_least|at_most|equal_to_sum_of_test
.