From 26f06e025de91e1ab7b446b1660419f54607d6ac Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 30 Nov 2025 19:40:47 -0500 Subject: [PATCH] Mix `ActiveModel::AttributeMethods` into `ActiveResource::Base` The problem --- The current attribute reading and writing implementation relies upon `#method_missing` and `#respond_to_missing?` for access. When attributes are defined by a schema, it's possible to define methods for them at "declaration time", rather than implementing access entirely through method missing reflection. The solution --- Mix [ActiveModel::AttributeMethods][] into `ActiveResource::Base`, then hook into `schema` assignment. When assigning attributes, invoke [define_attribute_methods][]. When the schema is reset to be `nil`, invoke [undefine_attribute_methods][]. [ActiveModel::AttributeMethods]: https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods.html [define_attribute_methods]: https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods/ClassMethods.html#method-i-define_attribute_methods [undefine_attribute_methods]: https://edgeapi.rubyonrails.org/classes/ActiveModel/AttributeMethods/ClassMethods.html#method-i-undefine_attribute_methods --- lib/active_resource/base.rb | 6 ++++- test/cases/attribute_methods_test.rb | 34 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb index 8ecd62718f..2b53cdee18 100644 --- a/lib/active_resource/base.rb +++ b/lib/active_resource/base.rb @@ -463,6 +463,7 @@ def schema(&block) @schema[k] = v @known_attributes << k end + define_attribute_methods @known_attributes @schema else @@ -492,6 +493,7 @@ def schema=(the_schema) # purposefully nulling out the schema @schema = nil @known_attributes = [] + undefine_attribute_methods return end @@ -1730,6 +1732,7 @@ def read_attribute(attr_name) name = self.class.primary_key if name == "id" && self.class.primary_key @attributes[name] end + alias_method :attribute, :read_attribute def write_attribute(attr_name, value) name = attr_name.to_s @@ -1737,6 +1740,7 @@ def write_attribute(attr_name, value) name = self.class.primary_key if name == "id" && self.class.primary_key @attributes[name] = value end + alias_method :attribute=, :write_attribute protected def attributes=(attrs) @@ -1908,7 +1912,7 @@ class Base extend ActiveResource::Associations include Callbacks, CustomMethods, Validations, Serialization - include ActiveModel::Conversion + include ActiveModel::Conversion, ActiveModel::AttributeMethods include ActiveModel::ForbiddenAttributesProtection include ActiveModel::Serializers::JSON include ActiveModel::Serializers::Xml diff --git a/test/cases/attribute_methods_test.rb b/test/cases/attribute_methods_test.rb index 920a6f8331..ffb27e3615 100644 --- a/test/cases/attribute_methods_test.rb +++ b/test/cases/attribute_methods_test.rb @@ -4,7 +4,39 @@ require "fixtures/person" class AttributeMethodsTest < ActiveSupport::TestCase - setup :setup_response + setup do + setup_response + @previous_schema = Person.schema + end + + teardown do + Person.schema = nil + Person.schema = @previous_schema + end + + test "setting the schema defines attribute methods" do + assert_changes -> { Person.public_instance_methods.include?(:name) }, from: false, to: true do + Person.schema { attribute :name, :string } + end + end + + test "setting the schema to nil undefines attribute methods" do + Person.schema { attribute :name, :string } + + assert_changes -> { Person.public_instance_methods.include?(:name) }, from: true, to: false do + Person.schema = nil + end + end + + test "reads and writes attribute methods declared by the schema without method missing" do + Person.schema { attribute :name, :string } + + resource = Person.new + + assert_changes -> { resource.name }, from: nil, to: "changed" do + resource.name = "changed" + end + end test "write_attribute string" do matz = Person.find(1)