From ebcc2a5a15a1c881076691c5b387f45fe04d99e5 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 24 Oct 2025 10:13:07 -0400 Subject: [PATCH] Base: read from `known_attributes` in `respond_to_missing?` Prior to this commit, the `respond_to_missing?` implementation had a conditional to supports attributes being able to be written to through methods "did not respond to" (like `send("#{attr_name}=", value)`), as well as `?`-suffixed predicate methods. The `method_missing` implementation has a set of conditionals similar to `respond_to_missing`, but with subtle variations through checking for an attribute name's inclusion in `known_attributes` (attributes defined by the Schema **or** assigned during `Base#load`) rather than the attribute name's inclusion in `attributes` (the current instance's assigned values, without any attributes declared in the Schema). Without the implementation changes, the following tests introduced in this commit fail: ``` 1) Failure: BaseTest#test_respond_to_known_attributes [test/cases/base_test.rb:972]: Expected # (Person) to respond to #name=. ``` This commit changes the `respond_to_missing?` conditional to check for the presence of the key in `known_attributes` rather than `attributes` so that instances will respond to `=`-suffixed writers and `?`-suffixed predicates for attributes that are declared in the resource's Schema that have not yet been assigned to. --- lib/active_resource/base.rb | 2 +- test/cases/base_test.rb | 47 ++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb index 1fb8c6d8e5..a49fc7a215 100644 --- a/lib/active_resource/base.rb +++ b/lib/active_resource/base.rb @@ -1653,7 +1653,7 @@ def respond_to_missing?(method, include_priv = false) super elsif known_attributes.include?(method_name) true - elsif method_name =~ /(?:=|\?)$/ && attributes.include?($`) + elsif method_name =~ /(?:=|\?)$/ && known_attributes.include?($`) true else # super must be called at the end of the method, because the inherited respond_to? diff --git a/test/cases/base_test.rb b/test/cases/base_test.rb index 7935d62637..1aa573f205 100644 --- a/test/cases/base_test.rb +++ b/test/cases/base_test.rb @@ -960,7 +960,52 @@ def test_respond_to assert_respond_to matz, :name assert_respond_to matz, :name= assert_respond_to matz, :name? - assert_not matz.respond_to?(:super_scalable_stuff) + assert_not_respond_to matz, :super_scalable_stuff + assert_not_respond_to matz, :super_scalable_stuff= + end + + def test_respond_to_known_attributes + previous_schema = Person.schema + Person.schema = { name: "string" } + + person = Person.new + + assert_respond_to person, :name + assert_respond_to person, :name= + assert_not_respond_to person, :super_scalable_stuff + assert_not_respond_to person, :super_scalable_stuff= + ensure + Person.schema = previous_schema + end + + def test_reading_an_unknown_attribute_raises_NoMethodError + assert_raises NoMethodError, match: "unknown_attribute" do + Post.new.unknown_attribute + end + end + + def test_writing_an_unknown_attribute_assigns_a_value_that_can_be_read + post = Post.new + + post.unknown_attribute = "assigned" + + assert_respond_to post, :unknown_attribute + assert_equal "assigned", post.unknown_attribute + end + + def test_writing_nil_to_an_existing_attribute_can_be_read + post = Post.new unknown_attribute: "assigned" + + post.unknown_attribute = nil + + assert_nil post.unknown_attribute + assert_respond_to post, :unknown_attribute + end + + def test_predicate_for_an_unknown_attribute_returns_nil + post = Post.new + + assert_not_predicate post, :unknown_attribute? end def test_custom_header