Skip to content

Adding Any/Or Generic Validator#43

Merged
bendbennett merged 18 commits intomainfrom
bendbennett/issues-28
Jul 5, 2022
Merged

Adding Any/Or Generic Validator#43
bendbennett merged 18 commits intomainfrom
bendbennett/issues-28

Conversation

@bendbennett
Copy link
Copy Markdown
Contributor

@bendbennett bendbennett commented Jun 23, 2022

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 Any validator is to determine whether at least one of the validators validates against the attribute but happy to discuss.

@bendbennett bendbennett added this to the v0.3.0 milestone Jun 23, 2022
@bendbennett bendbennett marked this pull request as ready for review June 23, 2022 10:36
@bendbennett bendbennett requested a review from a team as a code owner June 23, 2022 10:36
@bendbennett bendbennett added the enhancement New feature or request label Jun 23, 2022
Copy link
Copy Markdown
Contributor

@detro detro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread metavalidator/any.go Outdated
Comment on lines +38 to +44
for k, validator := range v.valueValidators {
validator.Validate(ctx, req, resp)
if k+1 > len(resp.Diagnostics) {
resp.Diagnostics = []diag.Diagnostic{}
return
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@detro detro Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, but I think I see what you are trying to say. Counting != getting them extracted. Sorry, I was being dumb.

Comment thread metavalidator/doc.go Outdated
Comment thread metavalidator/any.go Outdated
Comment thread metavalidator/all.go Outdated
bendbennett and others added 2 commits June 24, 2022 09:18
@bendbennett bendbennett force-pushed the bendbennett/issues-28 branch from 609dcac to a4a0b46 Compare June 24, 2022 08:25
Copy link
Copy Markdown
Contributor

@detro detro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread metavalidator/all.go
Comment thread metavalidator/all.go Outdated
Comment on lines +35 to +36
// 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this apply for the allValidator?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs needed updating following code changes. Fixed.

Comment thread metavalidator/all_test.go Outdated
@@ -0,0 +1,69 @@
package metavalidator
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have moved the tests to their own metavalidator_test pkg.

Comment thread metavalidator/any.go Outdated

validator.Validate(ctx, req, validatorResp)
if !validatorResp.Diagnostics.HasError() {
resp.Diagnostics = []diag.Diagnostic{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than returning empty diagnostics, it feels appropriate to return the validatorResp.Diagnostics here in case there are warning diagnostics with the passing validator.

Suggested change
resp.Diagnostics = []diag.Diagnostic{}
// Ensure any warning diagnostics from the passing validator are returned
resp.Diagnostics = validatorResp.Diagnostics

Copy link
Copy Markdown
Contributor Author

@bendbennett bendbennett Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have created a separate AnyWithAllWarningsValidator but more than happy to change the name if something else would suit better.

Comment thread metavalidator/any_test.go
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
)

func TestAnyValidator(t *testing.T) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

@bendbennett bendbennett Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread metavalidator/any.go Outdated
if !validatorResp.Diagnostics.HasError() {
diagWarnings := diag.Diagnostics{}

for _, d := range resp.Diagnostics {
Copy link
Copy Markdown
Contributor Author

@bendbennett bendbennett Jun 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have created a separate AnyWithAllWarningsValidator but more than happy to change the name if something else would suit better.

@bendbennett bendbennett requested review from bflad and detro June 28, 2022 09:00
Copy link
Copy Markdown
Contributor

@detro detro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had written my comments 3h ago and forgot to push submit 🤦

Comment thread metavalidator/any.go
Comment on lines +45 to +48
if !validatorResp.Diagnostics.HasError() {
resp.Diagnostics = validatorResp.Diagnostics
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this also means that, any previous validator warnings are swollen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread metavalidator/any_with_all_warnings.go Outdated
Comment on lines +55 to +64
diagWarnings := diag.Diagnostics{}

for _, d := range resp.Diagnostics {
if d.Severity() == diag.SeverityWarning {
diagWarnings.Append(d)
}
}

resp.Diagnostics = diagWarnings
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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()

😉

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have also noticed that the return wasn't really necessary there, given it's the end of the function.

Copy link
Copy Markdown
Contributor Author

@bendbennett bendbennett Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left that in, in case @bflad had any further comments. Will refactor once terraform-plugin-framework containing the convenience functions has been tagged. Have gone ahead and pulled the sha containing the convenience functions and refactored.

Also refactored all of the tests to use path.Root() rather than tftypes.NewAttributePath().WithAttributeName().

Comment thread metavalidator/any.go Outdated
Comment on lines +35 to +36
// If the diagnostics returned from the validator do not contain an error we return any warning diagnostics from the
// passing validator.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've expanded the docs a bit. See what you think.

return v.Description(ctx)
}

// Validate performs the validation.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, have expanded the docs.

…e path.Root() to replace tftypes.NewAttributePath().WithAttrrtibuteName() (#28)
@bendbennett bendbennett requested a review from detro June 29, 2022 08:15
Copy link
Copy Markdown
Contributor

@detro detro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@bflad bflad removed this from the v0.3.0 milestone Jun 29, 2022
@bflad bflad added this to the v0.4.0 milestone Jun 29, 2022
Copy link
Copy Markdown
Contributor

@bflad bflad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor things and pending terraform-plugin-framework v0.10.0 release, but otherwise looks good to me 🚀 Excellent work.

Comment thread go.mod
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread metavalidator/all.go Outdated
Comment thread metavalidator/all_test.go Outdated
stringvalidator.LengthAtLeast(5),
},
expectError: true,
// We can't test the diags returned as they are in the /internal/reflect pkg.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread metavalidator/any.go
Comment thread metavalidator/any_with_all_warnings.go
Comment thread validatordiag/diag.go Outdated
@@ -0,0 +1,47 @@
package validatordiag
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this file shouldn't be recreated in preference of the new helper/validatordiag/diag.go 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

@bendbennett bendbennett merged commit c9b8b39 into main Jul 5, 2022
@bendbennett bendbennett deleted the bendbennett/issues-28 branch July 5, 2022 15:31
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 5, 2022

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.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Aug 5, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Any/Or Attribute Validators (Pass If At Least One Attribute Validator Passes)

3 participants