ActiveResource relations – a bit of magic to make it look and feel more like ActiveModel relations

ActiveResource collection new problem

ActiveResource can be pretty helpful when you have a RESTful JSON API. Although it has some limitations. One of the most irritating is a lack of nested resources new scope method. When you have structure like this:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats
end

class DailyStat  < ActiveResource::Base
  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

You can do some basic stuff:

User.last #=> User instance
User.all #=> [User, User]
user = User.last
user.daily_stats #=> [DailyStat, DailyStat]

But unfortunately if you try something like this:

user = User.last
stat = user.daily_stats.new
stat.save!

You'll get following error:

user.daily_stats.new
NoMethodError: undefined method `new' for #<ActiveResource::Collection:0x000000080d1a30>

Of course we can do this the other way around:

user = User.last
stats = DailyStat.new(user_id: user.id)
stats.save!

But it just doesn't seem right (well at least after working with ActiveRecord). ActiveResource::Collection doesn't support building resources through it.

alias_method and a bit of magic as a solution

To obtain such a behaviour we have to:

  • save original relation method using alias_method
  • create a module that will contain our extension that will allow us to build new resources directly
  • define relation method that will mix the module with original relation method output
  • return mixed relation output

Once we have all of this, we will be able to just:

user = User.last
new_stat = user.daily_stats.new
new_stat.save!

overwriting method without losing the original one

Ok, so we have our relation daily_stats method that will return us a given ActiveResource::Collection. We will have to overwrite it, but we can't lose the original one. To obtain this, we can use alias_method:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :native_daily_stats, :daily_stats

  def daily_stats
    # Now we can do whatever we want here, because we can get
    # the ActiveResource::Collection of DailyStat via
    # native_daily_stats method
    # Something fancy will happen here...
    # and after that we will just return native_daily_stats
    native_daily_stats
  end
end

It's worth pointing out, that you can use this trick to redefine/change method without losing the original one. Especially when you can't use super (because it's not an inherited one, etc).

Extension module for our new daily_stats

Now we have to create our extension that will be used to modify the ActiveResource::Collection (but only in a daily_stats scope):

class DailyStat < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  # Here should go previously declarated DailyStat class code...
end

Hooking it all togethers

Finally, we can join all previously created elements in a new daily_stats method:

class User < ActiveResource::Base
  # User code...  

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

Now you can create resources that belong to other, directly via their scope.

TLl;DR version

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :original_daily_stats, :daily_stats

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

class DailyStat  < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      # Here magic happens - original params contain relation details (user_id: user.id)
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

Categories: Rails, Ruby, Software

1 Comment

  1. Great post! I’m faced same problem about ActiveResource.
    This post will be helpful for me!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑