Conversation
Danger ReportNo issues found. |
6883730 to
7ee184f
Compare
|
Thanks for this — the premise is right and worth landing. But 1. The lookup is now key-type sensitive
2.
|
| ms/validate | |
|---|---|
keys_in_common as written here |
2.33 |
attrs.select { |a| resource_params.key?(a) }, .map { scope.full_name(_1) } moved into validation_error! |
0.87 |
62% of what's left is full_name calls whose result is discarded — and it isn't cheap: a fiber-local ParamScopeTracker.current, a recursive walk up the parent scopes, and a string interpolation per call.
That restructure also makes the two bugs above one-line fixes instead of two, because the key lookup and the name formatting stop being the same expression. Right now AtLeastOneOfValidator re-implements the lookup inline (attrs.any? { |attr| params.key?(attr) }) rather than calling the shared helper, so the key-matching rule lives in two files and fix #1 has to be written twice. A shared present_attrs / any_attr_present? pair would keep one definition.
Smaller things
exactly_one_of_validator.rb:17andall_or_none_of_validator.rb:13now recomputeall_keyson the error path for attrskeys_in_commonjust built names for.known_keysused to be computed once and reused. This disappears for free under the restructure above.keys_in_common's arity changed from(resource_params, known_keys = all_keys)to(resource_params). It'sprivate, so no UPGRADING entry is needed, but subclassing a validator base is the documented way to add a group validator, and an out-of-tree one passing the second argument now raisesArgumentErrorat request time.known_keys = nilfalling through to the new path would cost nothing.- The CHANGELOG entry goes at the bottom of
#### Fixes, directly above* Your contribution here.— every other 4.1.0 entry is appended, and putting it at the top will conflict with the other open PRs. - The whitespace fix at
CHANGELOG.md:350(the 3.x section) is unrelated to group validators — worth dropping from this PR. - The doc comment on
keys_in_commonrestates the method name and then describes the implementation it replaced. PerAGENTS.md, that's PR-description material; comments are for why something non-obvious is happening. Same for the comment atmutually_exclusive_spec.rb:264.
On the added specs
Worth knowing: copying this PR's mutually_exclusive_spec.rb onto master gives 12 examples, 0 failures — both new examples pass without the change, so they don't currently guard it. The first one asserts 201, which a completely broken mutually_exclusive also satisfies. And at_least_one_of, exactly_one_of and all_or_none_of are all rewritten here with no new coverage; exactly_one_of in particular moved from known_keys.intersect? semantics to a raw count.
🤖 Generated with Claude Code
|
Thanks for the review, all points landed. Addressed in the follow-up commit:
Benchmark impact (isolated processes, Ruby 4.0.5): Synthetic (
Deferring Happy to tweak further if anything still looks off. |
Keep Symbol/String lookup and Array#&-style dedupe; move full_name to the error path; add regressions and large_model old/new bench.
b91aa21 to
729bc71
Compare
mutually_exclusive,exactly_one_of,at_least_one_ofandall_or_none_ofall go throughMultipleParamsBase#keys_in_common(or the samefull_name-over-every-key pattern inAtLeastOneOfValidator).Previously, for every element of an
Arrayscope, the validator:scope.full_nameon every key of the element hashThat is
O(request_keys × nesting)per element, even when the group only declares 2–3 attrs. On large payloads this dominates validation time; the cost grows with unrelated keys on each element, not with the exclusivity check itself.Change
Only inspect the declared group attrs, and call
full_namefor those that are present (still needed for error path names):Same idea for
AtLeastOneOfValidator(attrs.any? { params.key?(attr) }).Behavior and error messages are unchanged.
Motivation
Hit in production on the API on which is based the large_model benchmark: nested
mutually_exclusiveunderservices/quantities/timewindows(hundreds–thousands of elements). Profiling showedMutuallyExclusiveValidatoras the top Grape validator cost (~0.19s of ~0.34s validators on a ~429-service payload), largely fromfull_namework insidekeys_in_common.Benchmark
Fixture:
POSTJSON withrequires :items, type: Array, each element ~15 optional keys, and 3mutually_exclusiverules (beer/wine,pickup/value,delivery/value). Happy-path body (no validation error). Warmup 3 calls, then average of 5.full_nameon all keys)Repro sketch:
Gains scale with (elements × keys_per_element × group_rules); small flat params blocks see little difference.