The maze book for programmers!
mazesforprogrammers.com

Algorithms, circle mazes, hex grids, masking, weaving, braiding, 3D and 4D grids, spheres, and more!

DRM-Free Ebook

The Buckblog

assorted ramblings by Jamis Buck

Object#with_options

24 January 2007 — 2-minute read

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!

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.

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

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. :)

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!

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?

RCB, it would probably be easier to write your own helper method to add common functionality to all sets of nested resources:

1
2
3
4
5
6
7
map.resources :people do |people|
  decorate(people, :email_addresses)
end

map.resources :companies do |companies|
  decorate(companies, :email_addresses)
end

Something like that, anyway.

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).

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.

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.

Thanks, Lyle. It does some wierdness to the code snippets in comments now, but I suppose that’s a fair compromise.

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?

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.

RCB, One pattern I like with this question is to just use ruby:


[:users, :posts, :events].each do |resource|
  map.resources resource, :collection=>{:recent=>:get}
end

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?

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.

I don’t really understand the last example here. The one with :dependent and has_many. Could you describe it in details ?

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.