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