filters: Reduce duplication of bool filter parsing and invalidFilter error#44003
filters: Reduce duplication of bool filter parsing and invalidFilter error#44003cpuguy83 merged 2 commits intomoby:masterfrom
Conversation
0d18ae0 to
39469a0
Compare
daemon/images/image_prune.go
Outdated
| err = pruneFilters.ParseBoolIfPresent(&danglingOnly, "dangling") | ||
| if err != nil { |
There was a problem hiding this comment.
Wondering if it would make for an easier "api" to make this something like filters.GetBool("name", <default>)
Thinking of which, we have a httputils.BoolValueOrDefault (although that's in the api/server package, so I haven't fully checked what the consequences for that are). I recall I was looking at some of the httputils package to make (some of) them return a proper error (which could be "ignored" by the caller where suitable).
I'm also not sure if that function follows the same semantics for "what's valid" (looks like we may be a bit inconsistent there)
daemon/list.go
Outdated
| code, err := strconv.Atoi(value) | ||
| if err != nil { | ||
| return err | ||
| return filters.InvalidFilter{Filter: "exited", Value: value} |
There was a problem hiding this comment.
If possible, I think we should avoid exporting InvalidFilter, as it's mostly a "convenience" function to format an errdefs.InvalidParam(). Basically the "contact" is to return an errdef type, and that it happens to be "because of an invalid filter" is mostly an internal things for the filters package.
We could either decide to have a local helper, or inline for these two occurrences (return errdefs.InvalidParam(errors.Errorf("invalid filter ..."))
Or we could consider having a method to get Integer values (filters.GetInt(name, <default>)), like we added for booleans. Of course the ideal would've been for the filter values to be typed, but I guess that requires more work.
There was a problem hiding this comment.
I like the filters.GetInt approach, but didn't want to bloat the filters API too much. This would allow us to completely unexport invalidFilter though. I'll give it a shot.
There was a problem hiding this comment.
I decided not to implement the function for int parsing - there's only one usage for it (parsing stars filter in image search) and it handles multiple values by choosing the biggest. I'm not sure if this behaviour would be expected by future usages of GetIntOrDefault function, so I think it's better not to "abstract too early".
| return nil | ||
| } | ||
|
|
||
| if len(fieldValues) == 0 { |
There was a problem hiding this comment.
Maybe instead we ca error if len(fieldValues) != 1 which would also simplifly the conflict logic.
There was a problem hiding this comment.
Oh, I forgot about this PR, thanks for reminding me. I'll try to update it once I have some spare time!
IIRC I did this this way specifically to not change the old behaviour of filter.ExactMatch("dangling", "true") || filter.ExactMatch("dangling", "1") , which doesn't error when you specify the filter twice with different syntax (true/1 or false/0) but with the same value. This is only the case for image prune and volume service list/prune though.
If we don't need to keep this behaviour then we can go with len != 1.
api/types/filters/parse.go
Outdated
|
|
||
| // ParseBoolIfPresent overwrites the target with a boolean value of the key if it's present. | ||
| // If the value cannot be interpreted as a boolean an error is returned. | ||
| func (args Args) ParseBoolIfPresent(target *bool, key string) error { |
There was a problem hiding this comment.
Have you thought about returning a bool instead of dealing with the bool pointer?
There was a problem hiding this comment.
Yes - returning bool doesn't distinguish between value not being present and being false. In this case if value is missing, the target is not overwritten (leaves the previously assigned value) and error is nil.
I think we should go ...OrDefault approach suggested by Sebastiaan though.
|
|
||
| func TestParseBoolIfPresent(t *testing.T) { | ||
|
|
||
| for _, tC := range []struct { |
There was a problem hiding this comment.
Can you give all the test cases a name and run them in a subtest? It helps when something is failing what test case it's failing on.
891cd6d to
abc9923
Compare
a26705c to
18e30a0
Compare
|
@cpuguy83 @thaJeztah PTAL |
| conflicting := isFalse && isTrue | ||
| invalid := !isFalse && !isTrue | ||
|
|
||
| if conflicting || invalid { |
There was a problem hiding this comment.
This could all just be if isFalse == isTrue {, right?
There was a problem hiding this comment.
Hmm, don't we also want to error out if we have any values that aren't one of the four supported strings? (or is that a breaking change from the previous implementation?)
There was a problem hiding this comment.
Yes, that's the invalid check (true if value is none of [0,1,true,false]) and it ends with the error.
There are tests to prove that 😄 :
moby/api/types/filters/parse_test.go
Lines 451 to 468 in 18e30a0
There was a problem hiding this comment.
Yes, this is logically the same as if isFalse == isTrue, but it looked confusing (looks like false == true).
I went with if conflicting || invalid instead as it felt clearer and explicit about when this branch is taken (when the filter values are conflicting, or are invalid).
But maybe that's just my perception 😅
api/types/filters/parse.go
Outdated
| return true, nil | ||
| } | ||
|
|
||
| return defaultValue, nil |
There was a problem hiding this comment.
This should never be reached, right? Should this return some kind of error that it's an unexpected code path so we can chase it down easier if it ever somehow happens? 😅
There was a problem hiding this comment.
Changed to an error. I was tempted to just panic since it's not possible to reach this, but.. better safe than sorry I guess 😄
18e30a0 to
e040a9d
Compare
api/types/filters/parse.go
Outdated
| return true, nil | ||
| } | ||
|
|
||
| return defaultValue, fmt.Errorf("unexpected code path reached for values=%s", args.Get(key)) |
There was a problem hiding this comment.
As we used an errdef for the other error, ideally this one would be an errdefs.ErrSystem. It's the default (if no error-type is defined), but will also be logged otherwise as "FIXME: didn't have a error-type for error.."
There was a problem hiding this comment.
I wanted to avoid importing errdefs (it's not imported in this file, the invalidFilter just implements the InvalidParameter function, and thought that because this shouldn't happen anyway, then the "FIXME" message would at least be visible in the logs if it somehow ended up taking this code path 😄
I can add a helper struct that implements the errdefs.ErrSystem if you feel it would be better though.
There was a problem hiding this comment.
Ah, right, yes. So ideally implementing a type for it, although it looks like errdefs is already imported for other parts, so probably not a huge deal to use it.
I'm fine with looking at that separately though; not a blocker.
There was a problem hiding this comment.
Ah, it seems that errdefs somehow made it's way to the parse_test.go. Looking at the change that caused is makes me think this was accidental.
I dropped the errdefs dependency completely and changed the error to error-type.
| // GetBoolOrDefault returns a boolean value of the key if the key is present | ||
| // and is intepretable as a boolean value. Otherwise the default value is returned. | ||
| // Error is not nil only if the filter values are not valid boolean or are conflicting. | ||
| func (args Args) GetBoolOrDefault(key string, defaultValue bool) (bool, error) { |
There was a problem hiding this comment.
I wish Golang had a better option for variadic arguments, as then we could've made the defaultValue optional (I guess we could do bool... and check "maximum 1", but that'd be a bit of an ugly hack).
Bit of a bike-shed/nit, but perhaps GetBool(key, defaultValue) would work? It's shorter, and from the list of arguments I don't think it's really impacting readability / intent; WDYT?
There was a problem hiding this comment.
Actually I think the OrDefault suffix makes it a bit more clearer what the second boolean argument is.
Generally boolean arguments make the code harder to understand from the invocation alone.
Consider:
isTask, err := psFilters.GetBoolOrDefault("is-task", false)vs
isTask, err := psFilters.GetBool("is-task", false)The first one makes it a lot more apparent that the false is the default. In the second case reader might not be sure what the false is - is it default, is it some setting that influences how it parsed?
Writing the extra OrDefault is mostly mitigated by the autocompletion, but when reading you don't necessarily see the argument names.
Of course it's highly subjective and maybe others view this differently, so I'm happy to hear other opinions on this one 😃
api/types/filters/errors.go
Outdated
| if e.Filter != "" { | ||
| msg += " '" + e.Filter | ||
| if e.Value != nil { | ||
| msg += fmt.Sprintf("=%s", e.Value) |
There was a problem hiding this comment.
We're already concatenating, and Sprintf() doesn't add much here, so perhaps just;
| msg += fmt.Sprintf("=%s", e.Value) | |
| msg += "=" + e.Value |
There was a problem hiding this comment.
Ah, good one, thanks!
Ugh, not this time! 😂 Value is []string so it can't be concatenated with string:
invalid operation: "=" + e.Value (mismatched types string and []string)
There was a problem hiding this comment.
🤦 doh! I hate the filter implementation.
There was a problem hiding this comment.
Hmm, I'm thinking that this could be changed to msg = fmt.Sprintf("%s=%s", msg, e.Value) - if we already do the Sprintf, let it handle the concatenation too.
There was a problem hiding this comment.
I think it was done because the value is "optionally there". Current code seems fine from that perspective.
| code, err := strconv.Atoi(value) | ||
| if err != nil { | ||
| return err | ||
| return errdefs.InvalidParameter(errors.Wrapf(err, "invalid filter 'exited=%s'", value)) |
There was a problem hiding this comment.
Looks like a potential opportunity to also have a filters.GetInt(key, defaultValue)
There was a problem hiding this comment.
I considered GetInt but decided not to implement it because at the time I thought it's only used for stars (didn't notice the exited 😄) which made it a bit awkward to consider how multiple integer values should be handled and generalize solution from one usage.
So, to reevaluate this one:
Currently we seem to have two usages for parsing filter as integer and both usages have different approach in how they handle multiple values.
exited- uses all valuesstars- chooses the largest value ()moby/daemon/images/image_search.go
Line 49 in e040a9d
To handle both those cases, we would only be able to have something like GetInts(key string, default []int) ([]int, error) which would just make sure that all values for the given filter are valid integers and would return a parsed []int, or a default if the filter is missing.
This would simplify the exited case, because it would reduce this code to
filtExited, err := psFilter.GetInts("exited", nil)
if err != nil {
...And for stars this would still require to loop through the result, but now the int parsing would be gone, and it would focus only on the if iHasStar > hasStarFilter { hasStarFilter = iHasStar } part.
It's still a win I think. WDYT?
There was a problem hiding this comment.
Perhaps that may work (definitely not for this PR though!). Will have to give it another (closer) look to see what everything would look like. The "multiple values" (and in many cases rather ambiguous) is rather awkward with the filters.
thaJeztah
left a comment
There was a problem hiding this comment.
LGTM
unless anyone else has thoughts on
Of course it's highly subjective and maybe others view this differently, so I'm happy to hear other opinions on this one 😃
(GetBoolOrDefault(key, defaultValue) vs GetBool(key, defaultValue))
|
@cpuguy83 ptal |
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
The pattern of parsing bool was repeated across multiple files and caused the duplication of the invalidFilter error helper. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
e040a9d to
a654cbf
Compare
When working on images prune for containerd branch I noticed that the logic for parsing the filter value as boolean is duplicated in multiple places and each package has its own invalidFilter type which basically does the same thing.
- What I did
GetBoolOrDefault- How to verify it
CI, lint, tests
- Description for the changelog
- A picture of a cute animal (not mandatory but encouraged)