Conversation
detro
left a comment
There was a problem hiding this comment.
I think I spotted a small issue with the handling of Any() and Warnings.
I have also left some suggestions around godoc for this package.
| for k, validator := range v.valueValidators { | ||
| validator.Validate(ctx, req, resp) | ||
| if k+1 > len(resp.Diagnostics) { | ||
| resp.Diagnostics = []diag.Diagnostic{} | ||
| return | ||
| } | ||
| } |
There was a problem hiding this comment.
Mmm, I think this might be a bit "brutal", in the sense that we swallow any kind of Diagnostic, even the ones that are Warning and not errors.
Maybe you can make use of helpers/validatordiag.ErrorsCount and helpers/validatordiag.WarningCount here?
There was a problem hiding this comment.
Maybe a different way to go about this would be to feed a tailor made, empty Response in each validator, and return the first time response.Diagnostics.HasError() == false.
There was a problem hiding this comment.
Ah, and of course the Diagnostics would need to be accumulated so that, in case we reach the bottom of the loop, we then want to attach the sum of all those Diagnostics to the original Response.
There was a problem hiding this comment.
Have refactored to use your suggestion of response.Diagnostics.HasError() == false.
Diagnostics are accumulated and returned if none of the validators return without an error.
In terms of avoiding swallowing warnings, currently, it doesn't look like it's possible to selectively pull warnings out of diagnostics with something like response.Diagnostics.GetWarnings(). We could decorate diagnostics to make this possible or, modify diag.Diagnostics to add that function, for instance:
func (diags Diagnostics) GetWarnings() Diagnostics {
var d Diagnostics
for _, diag := range diags {
if diag.Severity() == SeverityWarning {
d = append(d, diag)
}
}
return d
}Thoughts?
There was a problem hiding this comment.
Yeah, that's why I added this to validatordiag:
// ErrorsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityError.
func ErrorsCount(diags diag.Diagnostics) int {
count := 0
for _, d := range diags {
if diag.SeverityError == d.Severity() {
count++
}
}
return count
}
// WarningsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityWarning.
func WarningsCount(diags diag.Diagnostics) int {
count := 0
for _, d := range diags {
if diag.SeverityWarning == d.Severity() {
count++
}
}
return count
}
But I'd be keen to have it added to the Diagnostics type directly in the FW. Would be even better and more aptly located.
There was a problem hiding this comment.
Mmm, but I think I see what you are trying to say. Counting != getting them extracted. Sorry, I was being dumb.
Co-authored-by: Ivan De Marino <ivan.demarino@hashicorp.com>
609dcac to
a4a0b46
Compare
detro
left a comment
There was a problem hiding this comment.
LGTM, and if you would like to throw together a little PR to add those Warnings and Errors extractor from Diagnostics, I'd be happy to review that. Feels like a useful addition.
| // If the number of iterations (i.e., k + 1) is greater than the number of diagnostics in the response then | ||
| // at least one of the validations has passed and, we return without any diagnostics. |
There was a problem hiding this comment.
Does this apply for the allValidator?
There was a problem hiding this comment.
Docs needed updating following code changes. Fixed.
| @@ -0,0 +1,69 @@ | |||
| package metavalidator | |||
There was a problem hiding this comment.
It looks like a few of the other packages were recently switched to the _test testing package convention so they can verify things via only exported functionality. Not sure if this should be updated as well.
There was a problem hiding this comment.
Have moved the tests to their own metavalidator_test pkg.
|
|
||
| validator.Validate(ctx, req, validatorResp) | ||
| if !validatorResp.Diagnostics.HasError() { | ||
| resp.Diagnostics = []diag.Diagnostic{} |
There was a problem hiding this comment.
Rather than returning empty diagnostics, it feels appropriate to return the validatorResp.Diagnostics here in case there are warning diagnostics with the passing validator.
| resp.Diagnostics = []diag.Diagnostic{} | |
| // Ensure any warning diagnostics from the passing validator are returned | |
| resp.Diagnostics = validatorResp.Diagnostics |
There was a problem hiding this comment.
Believe we need hashicorp/terraform-plugin-framework#392 so that we can extract the warnings from diagnostics and return them. Otherwise if we're using Any() with All() it's possible that we'd have errors from a previous iteration that would be returned, such as the following:
Any(
All(validator1, validator2, validator3),
All(validator4, validator5,validator6),
)Have just range(d) over the diagnostics and inspected the Severity in order to extract warning diags.
There was a problem hiding this comment.
My personal semantics opinion here is that given the above A || B, where A may contain conflicting logic than B and where B passes, that returning any warnings from A could potentially be confusing for practitioners. If provider developers want the ability to also return all warnings from other logical branches, then we could either introduce an "options" type parameter to allow triggering the variant behavior, or create a separate variant validator.
If we did want to return warnings from other logical branches, then the logic would need to fully remove any early return behavior and use a variable for tracking whether any given validator was "passing", performing the warnings extraction at the end of function based on that tracking variable to remove error diagnostics to prevent any sort of validator ordering semantics within Any().
There was a problem hiding this comment.
Have created a separate AnyWithAllWarningsValidator but more than happy to change the name if something else would suit better.
| "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" | ||
| ) | ||
|
|
||
| func TestAnyValidator(t *testing.T) { |
There was a problem hiding this comment.
To ensure behaviors with warning diagnostics, we might need to create a testing validator that always returns a warning (but no error) diagnostic. Then the unit testing can verify (e.g. via cmp.Diff the returned diagnostics) that a passing validator with a warning diagnostic has its warning still returned.
There was a problem hiding this comment.
Believe we need hashicorp/terraform-plugin-framework#392 so that we can extract the warnings from diagnostics and return them. Have just range(d) over the diagnostics and inspected the Severity in order to extract warning diags.
Have created a testing validator to exercise the case you describe @bflad.
| if !validatorResp.Diagnostics.HasError() { | ||
| diagWarnings := diag.Diagnostics{} | ||
|
|
||
| for _, d := range resp.Diagnostics { |
There was a problem hiding this comment.
This change will return all of the accumulated warning diagnostics from previously failed validations along with any warning from the validator that passes as discussed - "For the Warning for Any, I think it’s non desirable to swallow those".
There was a problem hiding this comment.
Have created a separate AnyWithAllWarningsValidator but more than happy to change the name if something else would suit better.
detro
left a comment
There was a problem hiding this comment.
I had written my comments 3h ago and forgot to push submit 🤦
| if !validatorResp.Diagnostics.HasError() { | ||
| resp.Diagnostics = validatorResp.Diagnostics | ||
| return | ||
| } |
There was a problem hiding this comment.
Doesn't this also means that, any previous validator warnings are swollen?
There was a problem hiding this comment.
Yes. There's a newly added AnyWithAllWarningsValidator that will accumulate all errors and warnings from all validators and return all warnings from all validators if any of the validators pass.
| diagWarnings := diag.Diagnostics{} | ||
|
|
||
| for _, d := range resp.Diagnostics { | ||
| if d.Severity() == diag.SeverityWarning { | ||
| diagWarnings.Append(d) | ||
| } | ||
| } | ||
|
|
||
| resp.Diagnostics = diagWarnings | ||
| return |
There was a problem hiding this comment.
| diagWarnings := diag.Diagnostics{} | |
| for _, d := range resp.Diagnostics { | |
| if d.Severity() == diag.SeverityWarning { | |
| diagWarnings.Append(d) | |
| } | |
| } | |
| resp.Diagnostics = diagWarnings | |
| return | |
| resp.Diagnostics = resp.Diagnostics.Warnings() |
😉
There was a problem hiding this comment.
I have also noticed that the return wasn't really necessary there, given it's the end of the function.
There was a problem hiding this comment.
I left that in, in case @bflad had any further comments. Will refactor once Have gone ahead and pulled the sha containing the convenience functions and refactored.terraform-plugin-framework containing the convenience functions has been tagged.
Also refactored all of the tests to use path.Root() rather than tftypes.NewAttributePath().WithAttributeName().
| // If the diagnostics returned from the validator do not contain an error we return any warning diagnostics from the | ||
| // passing validator. |
There was a problem hiding this comment.
The mention of "from the passing validator", kinda hints at how it works internally without being super explicit.
What if it explicitly mentions something like "it passes once it reaches a validator that returns no errors"? This way the sentence "we return any warning diagnostics from the passing validator" has more context.
There was a problem hiding this comment.
I've expanded the docs a bit. See what you think.
| return v.Description(ctx) | ||
| } | ||
|
|
||
| // Validate performs the validation. |
There was a problem hiding this comment.
This is sort of complementary to the doc of Any(), and I think it would be useful if one mentions the other, and it explains this whole "sequencing" that both validators do.
There was a problem hiding this comment.
I have just read also the doc you added to each of the wrapper functions. I think my argument is less strong, as in there (that is ultimately the public entry point), the docs is pretty clear about what those validators do internally.
👍
There was a problem hiding this comment.
Again, have expanded the docs.
…e path.Root() to replace tftypes.NewAttributePath().WithAttrrtibuteName() (#28)
bflad
left a comment
There was a problem hiding this comment.
A few minor things and pending terraform-plugin-framework v0.10.0 release, but otherwise looks good to me 🚀 Excellent work.
| require ( | ||
| github.com/google/go-cmp v0.5.8 | ||
| github.com/hashicorp/terraform-plugin-framework v0.9.0 | ||
| github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906 |
There was a problem hiding this comment.
Just putting a note here so we either rebase this after the dependabot PR for v0.10.0 or update this PR for v0.10.0 when its released.
| stringvalidator.LengthAtLeast(5), | ||
| }, | ||
| expectError: true, | ||
| // We can't test the diags returned as they are in the /internal/reflect pkg. |
There was a problem hiding this comment.
Aye this has bitten me in the past as well -- I think we should introduce an Equal() method on diag.Diagnostics so go-cmp can use the underlying (diag.Diagnostic).Equal() equality checking, rather than it needing to be Go type specific. I'll raise an issue upstream.
| @@ -0,0 +1,47 @@ | |||
| package validatordiag | |||
There was a problem hiding this comment.
I believe this file shouldn't be recreated in preference of the new helper/validatordiag/diag.go 👍
Co-authored-by: Brian Flad <bflad417@gmail.com>
|
I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions. |
Closes: #28
As mentioned in #28 the implementation in this PR results in the following: "This has at least one particular quirk which may be problematic in that it could swallow unrecoverable errors, such as implementing an incorrect type validator intermixed with any valid one.". This seems acceptable given that the purpose of the
Anyvalidator is to determine whether at least one of the validators validates against the attribute but happy to discuss.