fix(element-builder): dispatch on value.class for unrelated Serializables - #741
Open
ronaldtse wants to merge 1 commit into
Open
fix(element-builder): dispatch on value.class for unrelated Serializables#741ronaldtse wants to merge 1 commit into
ronaldtse wants to merge 1 commit into
Conversation
…bles `ElementBuilder#create_nested_model_element` reused the rule's cached `child_transformation` (compiled for the declared `attribute_type`) regardless of the value's actual class. When a caller assigned an unrelated `Lutaml::Model::Serializable` to a typed slot, the wrong transformation was applied silently: 1. The declared type's `compiled_rules` were iterated 2. `extract_rule_value` called `value.public_send(rule.attribute_name)` 3. If the value's class didn't have that attribute name, NoMethodError The cache itself was correct: `TransformationRegistry#transformation_key` includes `object_id`, so cache keys are unique per class. The bug was in the dispatch logic, not the cache. ## Fix Dispatch on `value.class` whenever value is a `Serializable` that differs from the declared `attribute_type`. This subsumes the existing `is_polymorphic_subtype` branch (subtypes have `value.class != rule.attribute_type` by definition). The fast path (`value.class == rule.attribute_type`) preserves the cached `rule.child_transformation` lookup, so no performance regression for the common case. ## Real-world trigger sts-ruby had two duplicate `<fn>` classes — `Sts::TbxIsoTml::Fn` (Ruby attr `:p`) and `Sts::NisoSts::Fn` (Ruby attr `:paragraph`). When a `TbxIsoTml::Fn` was nested inside a `NisoSts::Back` (whose `:fn_group` is typed `NisoSts::FnGroup`), the cached `NisoSts::Fn`-transformation was applied to the `TbxIsoTml::Fn` value, calling `fn.public_send(:paragraph)` and raising `NoMethodError`. Reverse direction also failed. sts-ruby worked around by deleting the duplicate classes (PR #47). The upstream fix here prevents the trap for any downstream consumer with similar duplicate-class patterns. ## Verification - New spec: `spec/lutaml/xml/cross_class_dispatch_spec.rb` (3 examples) - unrelated Serializable dispatch - declared-type fast path unchanged - XML round-trip preserved - Existing polymorphic specs (`spec/lutaml/model/polymorphic_spec.rb`) unchanged: 17 examples pass - Full suite: 5316 examples, 0 failures, 1 pending (pre-existing) ## Backwards compatibility Existing callers that rely on declared-type rules being applied to unrelated values will see different (correct) behaviour. This is the intentional fix — the prior behaviour was a silent wrong-rules application. See `BUGREPORT.element-builder-dispatch-on-value-class.md` for the original analysis and the downstream trigger.
JS build checkTriggered [ The result will appear as a |
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.
Summary
Fixes the dispatch bug documented in
BUGREPORT.element-builder-dispatch-on-value-class.md.ElementBuilder#create_nested_model_elementreused the rule's cachedchild_transformation(compiled for the declaredattribute_type) regardless ofvalue.class. When a caller assigned an unrelatedLutaml::Model::Serializableto a typed slot, the wrong transformation was applied silently — eventually raisingNoMethodErroron whichever Ruby attribute name differed between the declared type and the value's class.The cache itself was correct (
TransformationRegistry#transformation_keyincludesobject_id); the bug was in the dispatch logic.Fix
Dispatch on
value.classwhenever value is aSerializablethat differs from the declaredattribute_type:This subsumes the existing
is_polymorphic_subtypebranch (subtypes havevalue.class != rule.attribute_typeby definition). The fast path (value.class == rule.attribute_type) preserves the cachedrule.child_transformationlookup, so no performance regression for the common case.Real-world trigger
sts-ruby had two duplicate
<fn>classes —Sts::TbxIsoTml::Fn(Ruby attr:p) andSts::NisoSts::Fn(Ruby attr:paragraph). When aTbxIsoTml::Fnwas nested inside aNisoSts::Back(whose:fn_groupis typedNisoSts::FnGroup), the cachedNisoSts::Fn-transformation was applied to theTbxIsoTml::Fnvalue, callingfn.public_send(:paragraph)and raisingNoMethodError. Reverse direction also failed.sts-ruby worked around by deleting the duplicate classes (PR #47). The upstream fix here prevents the trap for any downstream consumer with similar duplicate-class patterns.
Verification
spec/lutaml/xml/cross_class_dispatch_spec.rb(3 examples):spec/lutaml/model/polymorphic_spec.rbunchanged: 17 examples passBackwards compatibility
Existing callers that relied on declared-type rules being applied to unrelated values will see different (correct) behaviour. This is the intentional fix — the prior behaviour silently applied wrong rules.
Test plan
bundle exec rspec— 5316 examples, 0 failuresbundle exec rubocop— clean