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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Zone < ActiveResource::Base self.site = SITE self.user = USERNAME self.password = PASSWORD def self.collection_path(prefix_options = {}, query_options = nil) prefix_options, query_options = split_options(prefix_options) if query_options.nil? "/users/#{USERID}#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}" end end class Dnskey < ActiveResource::Base self.site = SITE self.user = USERNAME self.password = PASSWORD def self.collection_path(prefix_options = {}, query_options = nil) prefix_options, query_options = split_options(prefix_options) if query_options.nil? z = '' if query_options.has_key?(:zone_id) z = "/zones/#{query_options[:zone_id]}" query_options.delete(:zone_id) end "#{z}#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}" end end |
In my case, I used a constant USERID to the Zone’s collection, but left the specific item (aka “elementpath”) alone. For Dnskeys, where I needed to pass in different zoneid values, I had to do a small trick.
I call this as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
all_zones = Zones.find :all # returns a list of all zones for the USERID z = Zone.find(1) # returns only Zone with the id of 1 z.destroy # destroys the zone, uses the element_path() z = Zone.find(2) keys = Dnskey.find(:all, { :zone_id => z.id }) # disgusting, but works # # This is a hack. I want to use Dnskey.create here, but it won't work since I cannot # pass the zone_id along, and there are no association hints so I can't use # zone.dnskeys.create() like I can with ActiveRecord. # d = Dnskey.new(:flags => key.flags, :algorithm => key.algorithm.to_i, ...) d.prefix_options[:zone_id] = zone.id d.save |