Stop an array default's transform: suppression from leaking to sub-fields - #83
Merged
Merged
Conversation
…s sub-fields
AuthoredValues, the walker that validates an array field's default:/example:
at class load, overrode permittable_transform unconditionally to skip the
field's own transform: (its result is discarded anyway — see
validate_array_authored_value!). But the override ignored which field it
was being asked about, so it also suppressed transform: for every nested
sub-field the walk touched.
That meant a request omitting the array (falling back to the stored
default:) and an equivalent request explicitly sending the same shape
silently diverged whenever a sub-field declared transform::
array :line_items, default: [{"price"=>"10.00"}] do
optional :price, :decimal, transform: ->(v){ v * 100 }
end
Omitting line_items yielded price == 10; sending {"price"=>"10.00"}
explicitly yielded price == 1000. The exported OpenAPI default, which reads
the same field[:default], documented the untransformed value the server
never actually produced for equivalent input.
The override now compares the field it is asked about against the one
default:/example: validation is running for (by identity), so it still
suppresses that field's own transform: but runs every nested sub-field's
transform: normally, matching what an equivalent request would produce.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
The bug
AuthoredValuesis the request walkervalidate_array_authored_value!points at an authored arraydefault:/example:at class load — the samepermittable_check_arraya real request goes through, so the two paths cannot drift apart in how they cast, normalize, and validate. It overridespermittable_transformso that the array field's owntransform:never runs over its authored default (that result is discarded anyway — the value is stored exactly AS AUTHORED when the field declarestransform:, pervalidate_array_authored_value!).That override was unconditional — it ignored which field it was even being asked about, so it suppressed
transform:for every sub-field the walk touched too, not just the outer array field's own:line_itemsitself declares notransform:, sofield[:default]is supposed to be stored asAuthoredValues' read of it — "exactly what a request sending it gets", per the surrounding comment. But because the walk suppressed:price's transform too, the stored default became[{"price"=>10}]instead of[{"price"=>1000}]:line_items(falling back to the default) yieldedprice == 10.{"price"=>"10.00"}yieldedprice == 1000.Two requests that should be indistinguishable silently diverged 100x. The exported OpenAPI
default(which reads the samefield[:default]) also documented the untransformed value — a value the server never actually produces for equivalent input.The fix
AuthoredValues#permittable_transformnow compares the field it's asked about against@field— the one field whosedefault:/example:this particular walk is validating — by identity, instead of suppressing every field unconditionally:@field(the array field itself) still never runs its owntransform:during this walk — its result was always going to be discarded when it declares one, so nothing inside its subtree is worth reading transformed, keeping the walk free of app code exactly as a scalar default's cast-only check already is.transform:now runs normally, so the stored default matches what an equivalent request actually produces.Updated the two comments (in
authored_values.rbandvalidate_array_authored_value!) that documented the old, unconditional suppression.Verification
spec/permittable_spec.rb), confirmed to fail before the implementation change (price => 10instead of1000) and pass afterbundle exec rspec— 852 examples, 0 failuresbundle exec rubocop lib/permittable.rb lib/permittable/authored_values.rb spec/permittable_spec.rb— no offenses"never runs an app's transform: over a default:, at class load or on the way out"(where the array field itself DOES declaretransform:) still passes unchanged, since@field[:transform]truthy keeps suppressing the whole subtree in that case🤖 Generated with Claude Code