f l a m e . o r g

organized flames

ActiveResource and Shallow Nested Routes

Posted on November 04, 2009

I recently tried to dive into ActiveResource, which allows a Ruby (usually Ruby on Rails) application to connect to a remote RESTful API and treat it almost as if it were local. There were some problems with my API from ActiveResource’s point of view.

Firstly, I use nested routes. This isn’t really a problem as it is possible to specify a prefix to add to the path, and even to substitution on this path. However, Rails added the concept of a “shallow nested route” a while back.

A shallow nested route is a short-hand notation which allows one to appear to scope out collections (that is, a list of things) from the actual thing itself. For example, in my application (https://dlv.isc.org/) I have:

/users/123 <– specific userid

/users/123/zones <— list of all zones for user 123

/zones/456 <— specific zone

This allows me to look at a specific user’s zones (or them, their own zones) without having to do some sort of special back-end processing which relies on something not in the path, such as the @current_user instance variable. After all, how would an admin list a user’s zones if /zones returned the current user’s zones only? Through an admin interface probably, but that seems messy. Shallow routes are seemingly more clean.

However, they break ActiveResource’s concept of the world.

With a little trickery, however, I managed to get shallow nested routes to work without having to do much additional work. I did have to pass in an additional argument to the collection list(s) however.

 1 class Zone < ActiveResource::Base
 2   self.site = SITE
 3   self.user = USERNAME
 4   self.password = PASSWORD
 6   def self.collection_path(prefix_options = {}, query_options = nil)
 7     prefix_options, query_options = split_options(prefix_options) if query_options.nil?
 8     "/users/#{USERID}#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
 9   end
10 end
12 class Dnskey < ActiveResource::Base
13   self.site = SITE
14   self.user = USERNAME
15   self.password = PASSWORD
17   def self.collection_path(prefix_options = {}, query_options = nil)
18     prefix_options, query_options = split_options(prefix_options) if query_options.nil?
19     z = ''
20     if query_options.has_key?(:zone_id)
21       z = "/zones/#{query_options[:zone_id]}"
22       query_options.delete(:zone_id)
23     end
24     "#{z}#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
25   end
26 end

In my case, I used a constant USERID to the Zone’s collection, but left the specific item (aka “element_path”) alone. For Dnskeys, where I needed to pass in different zone_id values, I had to do a small trick.

I call this as:

 1 all_zones = Zones.find :all # returns a list of all zones for the USERID
 2 z = Zone.find(1) # returns only Zone with the id of 1
 3 z.destroy # destroys the zone, uses the element_path()
 5 z = Zone.find(2)
 6 keys = Dnskey.find(:all, { :zone_id => z.id }) # disgusting, but works
 8 #
 9 # This is a hack.  I want to use Dnskey.create here, but it won't work since I cannot
10 # pass the zone_id along, and there are no association hints so I can't use
11 # zone.dnskeys.create() like I can with ActiveRecord.
12 #
13 d = Dnskey.new(:flags => key.flags, :algorithm => key.algorithm.to_i, ...)
14 d.prefix_options[:zone_id] = zone.id
15 d.save