Nesting and make_resourceful

Posted November 15, 2007

One of the many cool things about make_resourceful is that all the accessors and helper methods that make up the default actions and are used by the user are so concise. Most of them are one line. The rest are more, but just because they check properties of the controller to see how they should behave. If you implemented them for each controller individually, they’d also be one line.

Except for one: parent_objects.

def parent_objects
  return [] if parents.empty?
  return @parent_objects if @parent_objects

  first = parent_models[0].find(parent_params[0])
  @parent_objects = [first]
  parent_params.zip(parents)[1..-1].inject(first) do |object, arr|
    id, name = arr
    @parent_objects << object.send(name.pluralize).find(id)
    @parent_objects[-1]
  end
  @parent_objects
end

This is the method that goes through each of the parent models for the current model, looking up that parent’s object, and ensuring that it’s properly nested beneath the previous parent’s object. For /grandparent/457/parent/12/child/90/grandchild/12, it looks up GrandParent.find(457).parents.find(12).children.find(90).grandchildren.find(12), assigning all the proper variables along the way.

The problem is that it seems to be pretty much as simple as it’s going to get. We can’t even move much of it into another method, because it relies so heavily on instance variables.

However, a while back it was pointed out on the make_resourceful Google Group that deeply nested resources aren’t actually very useful, and what’s more are considered bad style.

Also, assuming that there’s going to be one consistent deep hierarchy of parents actually limits make_resourceful’s flexibility. You have to struggle to get both /posts/16/comments and /comments/14 to work. Polymorphic nesting – using both /posts/16/comments and /tumbles/189/comments - is a royal pain.

For a long time, there was a patch floating around to fix this. Jonathan Linowes came up with a set of modifications that would break normal deep nesting but allow polymorphic nesting to work.

As of revision 161, this patch has been merged with make_resourceful trunk… in a sense. Actually, none of Jonathan’s code ended up in m_r, but all its ideas and capabilities are there.

One big difference between the patch and the polymorphism support in make_resourceful is that while Jonathan tried to maintain the normal m_r parent API, we’ve felt no such need. All the old parent methods – parents, parent_objects, etc. have all been removed. In their place are methods that assume there’s only one parent at a given time - parent_name, parent_object, and so forth.

The nesting of a controller is declared in the same way it always was: using the belong_to declaration. However, the semantics are different than they used to be. Instead of declaring a hierarchy of nesting, it declares a list of parent models that are each valid. For example:

class BeansController
  make_resourceful do
    actions :all
    belongs_to :can, :burrito
  end
end

Then the routes /cans/15/beans, /burritos/22/beans, and /beans/76 will all work. You also get a bunch of handy accessors. What they return depends on what parameters are available.

If the current path is /cans/15/beans, params[:can_id] is defined, which indicates to make_resourceful that “can” is the current parent. If it’s /burritos/22/beans, params[:burrito_id] is defined, which means that the parent is “burrito.” If it’s just /beans/76, neither parameter is there, so m_r knows there’s no parent.

The first new accessor is parent?. It returns whether or not there’s a parent. Make sure you check it before dealing with the other parent accessors - they’ll die noisily if a parent doesn’t exist.

# /cans/15/beans
parent? #=> true

# /burritos/22/beans
parent? #=> true

# /beans/76
parent? #=> false

parent_name returns the name of the current parent.

# /cans/15/beans
parent_name #=> "can" 

# /burritos/22/beans
parent_name #=> "burrito"

parent_model returns the actual model class.

# /cans/15/beans
parent_model #=> Can

# /burritos/22/beans
parent_model #=> Burrito

parent_object is probably the most useful. It returns the actual record referenced by the parent id.

# /cans/15/beans
parent_object #=> Can.find(15)

# /burritos/22/beans
parent_object #=> Burrito.find(22)

parent_path and parent_url return the path and URL for parent objects, respectively. By default, the path is for parent_object, but they can be any object of the proper type.

# /cans/15/beans
parent_url  #=> "http://beanworld.com/cans/15" 
parent_path #=> "/cans/15" 
parent_path(Can.find(12)) #=> "/cans/12" 

# /burritos/22/beans
parent_url  #=> "http://beanworld.com/burritos/22" 
parent_path #=> "/burritos/2" 
parent_path(Burrito.find(12)) #=> "/burritos/12"

So I hope everyone enjoys this much-requested change. For those of you using polymorphism, this should make your lives easier. For those of you who are still doing deep nesting, hopefully this will encourage you to switch.

We plan to release a stable 0.3 release with this and a few more bits of polish In the coming week or so. Until then, if you want to give it a whirl, just grab trunk:

./script/plugin install http://svn.hamptoncatlin.com/make_resourceful/trunk
mv vendor/plugins/trunk vendor/plugins/make_resourceful
Jeremy said November 16, 2007:

Mind if I fork a parallel version (make_nested? ;)) and keep the deep nested stuff in there? It seems that a lot of the projects I’ve worked on lately have really needed deeply nested resources (e.g., working with school grades, books for those grades, units, chapters, sections, and activities within those sections). In that case, it doesn’t make sense to de-nest it with URLs like chapter/44/sections/9 because that makes the URL essentially useless.

I think its a good move to take it out (I’d like to not have to do that…and polymorphic nesting++), but it makes more sense in some cases. Any advice on cases like that or am I just screwed?

Rahsun McAfee said November 16, 2007:

This just keeps getting better!

That was a problem I was dealing with and now it seemed to just melt away. Awesome!

Helps keep my controllers light and I love the DRY-ness!

Nathan said November 16, 2007:

Jeremy: After thinking it through, I think it’s likely some support for deep nesting will come back, although possibly in a more limited form. Note that your example wouldn’t be handled by the default deep nesting code anyway, because (presumably) /chapters/44/sections/9 refers to section 9 of chapter 44, not the section with absolute id 9. The new modifications don’t prevent you from dealing with nesting, they just don’t do it automatically. You can still override current_model to deal with all the nesting you want.

I’d much rather not see m_r forked, for obvious reasons. I don’t think it’s necessary, though; make_resourceful is quite extensible, so you should be able to re-add the methods on a case-by-case basis, either by copying them into your ApplicationControllers, or creating a module and including it. You can even add declarations to the make_resourceful block by including modules in Resourceful::Builder.

Jared Haworth said November 16, 2007:

These new changes look fantastic!

Does it lead to scoped actions, so that create in BeansController will call Can.find(15).beans.create(params[:beans]) or Burrito.find(22).beans.create(params[:beans]) when appropriate?

Nathan said November 16, 2007:

Jared: Yes, it does. When a parent object is present, the current_model accessor returns the association object (e.g. Can.find(15).beans), which duck-typically responds to the same methods as the actual model. Then all the other accessors – current_object, build_object, etc. work properly.

Chris Vincent said November 16, 2007:

It just keeps getting better! This is the one addition to m_r which I was getting ready to go about doing via my own modifications. It’s great to see the official source code taking care of the task instead.

I can’t imagine what else might make m_r any better, it seems it is quickly reaching an epitomous approach to its problem domain. I’m eager to see what else is in store for the next release. Keep up the great work!

Will Farrington said November 17, 2007:

Congratulations on the continued improvements you’re making available for the Ruby on Rails community. =)

Evgeny said April 27, 2008:

Why not add

response_for(:create, :update) do
  set_default_redirect nested_object_path
end

as a default when using belongs_to in m_r ?

Nathan said April 27, 2008:

Because make_resourceful promotes flat routes – i.e. object_path-style – by default. nested_object_path exists because sometimes it’s useful to have knowledge of the parent encoded in the url (e.g. for polymorphic routes), but this isn’t the case often enough to warrant making it the default.

You can always make it the default for your app by adding the response_for block to application.rb.

Evgeny said April 28, 2008:

How about making nested_object_path == object_path by default, but change to be parent->object when there is a parent? It seems more natural that when you create/update an object from a given parent—your url includes that parent when the object is presented to you after the action.

Nathan said April 28, 2008:

I disagree. Unless you’re using polymorphic routes, all the information a URL with a parent path gives you is also implicit in the bit of the URL of the child. Thus is you have, say, /posts/55/comments/472, the /posts/55 is unnecessary – you already know that from comment.post_id.

Evgeny said May 06, 2008:

Won’t this add extra complexity – like when you have comments 470-480 on post 55, and you access the comment via

/comments/472
... then the users will probably want want to see the comment with the post, and not just hanging there in the air detached from context.

There is this extra complexity now in the mechanics for showing the “post” and the “comment” at the same page after the submit of the “comment”.

And what about the comment’s rating for example – (ignore ajax for a sec) – someone clicks on a 1-5 star on the comment to rate it, and it’s also a resource. Now the rating has a comment.id, and the comment has a post.id but the mechanics to show the “post” and all the comments, and that new rating on that specific comment becomes extra complex.

On the other hand—

when you submit the comment from

/posts/55/comment/472
and you get the flash[:notice]="Comment submitted" shown on
/post/55
it’s more natural.

Same with the rating for that comment—by sending a POST to

/post/55/comment/472/rating?stars=4
it would be nice to jump to
/post/55
with a flash, and not to
/rating/554545
that hangs there in the air without a comment and without a post.

Or am I getting it all wrong somehow?

Glenn said August 19, 2008:

Feature Request

I have one improvement to this that I would assume to be fairly necessary. If you have a CommentsController that can handle a polymorphic commentable record, then wouldn’t you want to be able to handle any record that comes along with the appropriate association, and not just the ones specified in belongs_to?

For instance if you add another record somewhere down the road (Event we’ll say), who’s model also has:

has_many :comments, :as => :commentable

You would, necessarily, have to update the routes.rb as well:

map.resources :events, :has_many => [ :comments ]

However, it would be tedious also have to go into CommentsController and add :event to the belongs_to. Not to mention the situation if the CommentsController is located inside a vendor plugin.

Instead wouldn’t it be nicer if the Controller was smart enough to read whatever parent record was passed in (by simply parsing the path and params). Then taking that record and making sure it has a :as => :commentable association. And then finally using that record as the parent as normal. Furthermore setting the @commentable instance variable to this record would also be nice.

I have some code I’ve been playing around with that parses the path and params pretty well, but I’m sure there are multiple solutions to this. I do think it would significantly improve the usability of this plugin in modern site development.

Glenn said August 19, 2008:

one thing I forgot to mention is that you’d need to allow the belongs_to method in the m_r Builder to handle the polymorphic name:

belongs_to :commentable

Or perhaps a new method all together.

Glenn said August 22, 2008:

I have a branch of the “nearest to original” version of make_resourceful from github.

I have implemented the above described feature. As well as a couple other useful methods described in the README (member_actions and collection_actions).

http://github.com/glennpow/make_resourceful/tree

Feel free to give feedback.

Nathan said September 05, 2008:

I’m no longer maintaining make_resourceful. Your best bet for this sort of thing is the mailing list.

Make your comments snazzy with Textile!