Skip to content

Conversation

@seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented Dec 1, 2025

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.

@rafaelfranca
Copy link
Member

Do you have benchmarks to show if this change changed the performance of reading and writing attributes?

@seanpdoyle
Copy link
Contributor Author

seanpdoyle commented Jan 5, 2026

@rafaelfranca after making the following benchmark-related changes:

diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb
index e34714e..f43675d 100644
--- a/lib/active_resource/base.rb
+++ b/lib/active_resource/base.rb
@@ -371,6 +371,7 @@ module ActiveResource
       @@logger = logger
     end
 
+    class_attribute :define_methods, default: true
     class_attribute :lazy_collections, default: true, instance_accessor: false
     class_attribute :_query_format
     class_attribute :_format
@@ -463,7 +464,7 @@ module ActiveResource
             @schema[k] = v
             @known_attributes << k
           end
-          define_attribute_methods @known_attributes
+          define_attribute_methods(@known_attributes) if define_methods
 
           @schema
         else
@@ -493,7 +494,7 @@ module ActiveResource
           # purposefully nulling out the schema
           @schema = nil
           @known_attributes = []
-          undefine_attribute_methods
+          undefine_attribute_methods if define_methods
           return
         end

I executed the following script:

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "activeresource", path: "."
  gem "benchmark-ips"
end

class WithAttributeMethods < ActiveResource::Base
  self.define_methods = true

  schema do
    attribute :known, :string
  end
end

class WithoutAttributeMethods < ActiveResource::Base
  self.define_methods = false

  schema do
    attribute :known, :string
  end
end

require "benchmark/ips"

Benchmark.ips do |x|
  x.report("with") do |n|
    with = WithAttributeMethods.new

    n.times do
      with.known = "value"
      with.known
      with.unknown = "value"
      with.unknown
    end
  end

  x.report("without") do |n|
    without = WithoutAttributeMethods.new

    n.times do
      without.known = "value"
      without.known
      without.unknown = "value"
      without.unknown
    end
  end

  x.compare!
end

The with is code for this branch's changes, the without is code prior to the proposed changes.

❯ ruby benchmark.rb 
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
                with    51.419k i/100ms
             without    41.498k i/100ms
Calculating -------------------------------------
                with    513.968k (± 1.1%) i/s    (1.95 μs/i) -      2.571M in   5.002721s
             without    415.405k (± 1.1%) i/s    (2.41 μs/i) -      2.116M in   5.095418s

Comparison:
                with:   513968.3 i/s
             without:   415404.6 i/s - 1.24x  slower

While improving performance (or at least not hindering performance) is one motivation, this change is also required for the ActiveModel::Dirty integration proposed in #466 (and improves ActiveModel::Attributes-compliance explored in #410).

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants