Object#with_options
In Named, explicit routes I mentioned Object#with_options
in passing, only to discover later that this super-useful method is not documented in Rails’ API docs! I’ve since corrected the situation in trunk, but the method is useful enough (particularly in conjunction with routing) that I figured it was worth blogging about.
The pain point that inspired the method is this: suppose you have a bunch of method calls, all of which accept an options hash as the last parameter, and all of which share one or more of the same options. Route definitions are the canonical examples of this:
1 2 3 4 |
map.create_message "/msg/create/:id", :controller => "message", :action => "create" map.delete_message "/msg/delete/:id", :controller => "message", :action => "delete" map.message "/msg/:id", :controller => "message", :action => "get" # etc, etc, etc |
Ugly! And definitely not very DRY. One way around this is to define a separate variable that contains the hash of common options, and use Hash#merge
to add in the difference in each call
1 2 3 4 5 |
common = { :controller => "message" } map.create_message "/msg/create/:id", common.merge(:action => "create") map.delete_message "/msg/delete/:id", common.merge(:action => "delete") map.message "/msg/:id", common.merge(:action => "get") # etc, etc, etc |
Better, but still not very DRY. Object#with_options
is Rails’ answer to this pattern:
1 2 3 4 5 6 |
map.with_options :controller => "message" do |msg| msg.create_message "/msg/create/:id", :action => "create" msg.delete_message "/msg/delete/:id", :action => "delete" msg.message "/msg/:id", :action => "get" # etc, etc, etc end |
Ah! Duplication, be gone! Much nicer.
Although I’ve personally used this primarily in route definitions, it can be used anywhere that option hashes appear as the last parameter…which describes most of the interfaces in Rails. Have a bunch of associations on a model, all of which are declared dependent on the parent?
1 2 3 4 5 6 7 |
class Blog < ActiveRecord::Base with_options :dependent => :destroy do |parent| parent.has_many :authors parent.has_many :posts parent.has_many :themes end end |
Great stuff! (And yet another example of the power of blocks in Ruby.) For the curious, you read can read the implementation of with_options
in Rails’ ActiveSupport project, here and here. All told, it’s under 30 lines of code, so it’s pretty easy to grasp. (Props to Sam Stephenson for the beautiful implementation!)
Reader Comments
Very nice!
24 Jan 2007
I should mention, I also love with_options because it adds a natural grouping to things, like routes. Each of your routes is suddenly grouped by controller, because with_options adds that block around them. Makes it really easy to get your routes organized.
24 Jan 2007
I’m not sure I like the has_many example. I agree with your above comment that this encourages grouping of related statements, but the has_many example groups the statements by something entirely irrelevant, making the code more complicated to follow. I’m finding it hard to articulate what I mean but let’s just say you have only one statement. You wouldn’t dream of doing this:
with_options :dependent => :destroy do |parent| parent.has_many :authors end
But you may well do this (as it shows the structure of the route):
map.with_options :controller => “message” do |msg| msg.create_message ”/msg/create/:id”, :action => “create” end
24 Jan 2007
I hear you, Jon, and I agree to some extent, which is why I only really use with_options in the routing definitions. Still, I don’t think I’d use with_options just to define a single route. It obscures the intention, I think, and it’s more to type. :)
24 Jan 2007
For me, the with_options look like a way to scope pretty much anything. Just need to be careful, the more “magic” the most difficult to follow, again, balance is everything.
Nice tip Jamis!
24 Jan 2007
What about the case where you want to a add options like :collection=>{:recent=>:get} to every nested resource route?
Using with_options would only apply to the outermost resource block, so you would need to do a with_options on each block.
Applying options to all nested resource routes, would require alias_method_chain( :resources, :custom_options ). Is that right?
24 Jan 2007
RCB, it would probably be easier to write your own helper method to add common functionality to all sets of nested resources:
Something like that, anyway.
24 Jan 2007
Jamis, you’re recent slew of posts have been incredibly helpful and illuminating. Thanks for taking the time to share your knowledge of the intricacies of the framework.
One minor styliing point: In Safari the code blocks don’t seem to handle horizontal overflow into your nav links. Instead of wrapping the block in a scroll box the text overlaps the nav section, making it difficult to read (this doesn’t happen in FF).
24 Jan 2007
Shalev, yah, I know about that Safari problem, but alas my CSS-fu is not sufficient to figure out how to fix that in Safari. If anyone has any suggestions, I’m all ears.
24 Jan 2007
Both Safari and IE6 have this issue. If you supply a fixed width to .CodeRay .code pre, it will fix this. About 38em should do the trick.
24 Jan 2007
Thanks, Lyle. It does some wierdness to the code snippets in comments now, but I suppose that’s a fair compromise.
24 Jan 2007
Perhaps this is a bit of a newbish question, but with all this talk about named routes, do you do this for all of your major controller actions, or do you have another rule of thumb for their use?
24 Jan 2007
James, to be honest, I use RESTful routing, via “map.resources”. But for the few oddball actions that don’t fit in map.resources, I define with an explicitly named route. I don’t use implicit routes (named or otherwise) at all these days.
24 Jan 2007
RCB, One pattern I like with this question is to just use ruby:
25 Jan 2007
Wayne, I think that pattern would work great when the routes are not nested. However, if you have nested route blocks, you cannot use the pattern and you would have to repeat the :collection opts to add :recent to every resource:
opts = {:collection=>{:recent=>:get}} map.resources :schools, opts do |s| s.resources :teachers, opts do |t| t.resources :classes, opts do |c| c.resources :lessons, opts end end s.resources :students,opts do |s| t.resources :classes, opts do |c| c.resources :assignments, opts end end end
Applying with_options on the routes above would only help for schools since s is referenced more than one. Using decorate() on the routes above is more trouble than just passing opts to each :resources call. You could argue this is a pathological example, but I actually have something worse in a real project.
Using alias_method_chain on :resources to merge the :collection opts still appears like the best way to add an action to all collections unless someone has a better idea.
As a related question: how would you remove one of the action routes that :resources automatically defines (eg. edit)?
I know you can omit/block/hide/redirect the edit action in the controller, but the route would still remain, and could still be generated by url_for. Is there a clean way to remove the route to a specific action on a resource?
27 Jan 2007
RCB, there is not a simple way of removing a generated route. You can always go digging through the routing code itself to see what arrays and hashes you would need to manipulate, but at the moment there is no exposed API for removing routes.
27 Jan 2007
I don’t really understand the last example here. The one with :dependent and has_many. Could you describe it in details ?
2 Feb 2007
Mike, it simply calls “has_many”, but appends the :dependent argument to each call. The “parent” object is simply a proxy that wraps the “Blog” class, and which knows to add the hash to all calls.
It’s not really a practical example, just a demonstration of how to use with_options in a non-routing scenario.
2 Feb 2007