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