Introducing resource_controller: Focus on what makes your controller special. 18

Posted by james
on Friday, October 19

If you're using rails, and you've kept up with what's happening in the community, and in the framework, you're probably writing RESTful apps. You may have noticed, like I did, that most of your controllers follow the same basic pattern - the one that the scaffold_resource (just scaffold in edge/2.0) generator spits out.

I wanted a great way to hide that pattern, and describe the unique features of my controllers through a more domain-specific API. Since I tend to write a lot of nested, and polymorphic resources, I also wanted to DRY up all of that code, and make generating urls a lot more intuitive. Finally, I wanted all of that code to be well-tested, so that I could count on hacking it up when I needed new features, without breaking things.

So, I created it, and here it is.

resource_controller

A basic, out of the generator resource just requires inheriting from ResourceController::Base.
class PostsController < ResourceController::Base  
end

API

Before/after callbacks, responses, and flash messages are managed through an extremely flexible API. I tried my best, here, to make syntax succinct when your needs are succinct, and allow API calls to be organized in an intuitive way. What I came up with was a scoping system, where calls to the various API methods can be chained, or scoped with a block.

Take the create action, for example. You might want to add a before block, to assign the author attribute of your post model to the current user.
class PostsController < ResourceController::Base
  create.before do
    @post.author << current_user
  end
end
As your application matures, you may want to add an RJS response to your create method, for some AJAXy goodness. Easy.
class PostsController < ResourceController::Base
  create.before do
    @post.author << current_user
    
    response do |wants|
      wants.html
      wants.js
    end
  end
end
Actually, there's even a shortcut for adding responses. You can omit the response block, if you just want to add responses to what's already provided by resource_controller (just HTML) or has been added elsewhere.
class PostsController < ResourceController::Base
  create do
    before do
      @post.author << current_user
    end
    
    wants.js
  end
end
Since I commonly use the same RJS template for several actions, I've found this short-form really handy, because I can use a one-liner to add the same response to several actions.
class PostsController < ResourceController::Base
  [create, update].each { |action| action.wants.js {render :template => "post.rjs"} }
end
Oh yeah, and you can change the flash too.
class PostsController < ResourceController::Base
  create.flash "Wow! RESTful controllers are so easy with resource_controller!"
end
For more API details, and examples, see the RDocs.

Helpers

Helpers are used internally to manage objects, generate urls, and manage parent resource associations.

If you want to customize certain controller behaviour, like member-object, and collection fetching, overriding helper methods is all it takes.

Note:For certain resource_controller functionality to work properly, user-defined helpers must make use of the other r_c helpers. The following examples do not follow that convention for clarity purposes - see the docs for more details.

If you wanted, for example, to use a permalink for your post, you'd need to alter the way that posts are fetched. Just override the object method.
class PostsController < ResourceController::Base
  private
    def object
      @object ||= Post.find_by_permalink(param[:id])
    end
end
You'll probably also want to add pagination to your index method. In the same way we altered member object fetching, we can change collection fetching behavior.
class PostsController < ResourceController::Base
  private
    def collection
      @collection ||= Post.find(:all, :page => {:size => 10, :current => params[:page]})
    end
end
Details and examples in the RDocs.

Namespaced Resources

...are handled automatically, and any namespaces are available, symbolized, in array form from the namespaces helper method.

Nested Resources

Again, handled automatically. This can be a real pain, and it's a lot easier with r_c. With an ActiveRecord-like syntax, just say belongs_to :model, and resource_controller takes care of the associations for you.
class CommentsController < ResourceController::Base
  belongs_to :post
end

Polymorphic Resources

This is a concept that can be found in a lot of my apps. Prior to resource_controller, it was a real pain on various levels. I solved some of my problems with urligence, and took things the rest of the way with resource_controller. It really does pretty much everything for you.

Just use the belongs_to syntax in your controller, and r_c infers whichever association (if any) is present when an action is called. The arguments passed to belongs_to are single possible parents; there is no support for deeply nested resources (although, I'm not necessarily opposed to adding it, if there is demandwe).
class CommentsController < ResourceController::Base
  belongs_to :post, :product
end
In the above example, the controller will automatically infer the presence of either a parent Post, or Product, and scope all the comments to that parent. The controller will also respond without a parent if you have that in your routes.

Thanks to urligence, generating urls in your polymorphic controller's views is really easy. The object_url, and collection_url helpers will maintain your parent resource's scope automatically.
# /posts/1/comments
object_url          # => /posts/1/comments/#{@comment.to_param}
object_url(comment) # => /posts/1/comments/#{comment.to_param}
edit_object_url     # => /posts/1/comments/#{@comment.to_param}/edit
collection_url      # => /posts/1/comments

# /products/1/comments
object_url          # => /products/1/comments/#{@comment.to_param}
object_url(comment) # => /products/1/comments/#{comment.to_param}
edit_object_url     # => /products/1/comments/#{@comment.to_param}/edit
collection_url      # => /products/1/comments

# /comments
object_url          # => /comments/#{@comment.to_param}
object_url(comment) # => /comments/#{comment.to_param}
edit_object_url     # => /comments/#{@comment.to_param}/edit
collection_url      # => /comments
More in the RDocs.

Getting it

resource_controller is available under the MIT License.

It is currently available in two streams:

1.2.3+ Compatible

Install it:

svn export http://svn.jamesgolick.com/resource_controller/tags/stable vendor/plugins/resource_controller

SVN (stable): http://svn.jamesgolick.com/resource_controller/tags/stable

SVN (ongoing): http://svn.jamesgolick.com/resource_controller/trunk

Note: If you want to run the tests, cd in to the test directory, and type rake test.

Edge/Rails 2.0 Compatible

Install it:

svn export http://svn.jamesgolick.com/resource_controller/tags/edge_compatible/stable vendor/plugins/resource_controller

SVN (stable): http://svn.jamesgolick.com/resource_controller/tags/edge_compatible/stable

SVN (ongoing): http://svn.jamesgolick.com/resource_controller/branches/edge_compatible

Note: If you want to run the tests in the edge-compatible version, cd in to the test directory, and type rake rails:freeze:edge. I didn't want people to have to download all of edge rails just to get the plugin.

Also, check out the rdoc

Feedback

Drop me a line and let me know what you think. I'd love to hear your suggestions for API or implementation improvements, or anything else!

Comments

Leave a response

  1. macournoyerOctober 19, 2007 @ 12:34 PM

    Nice work James!

    I like the API. I might give it a try on my next project.

    How did you test it? I saw you setup a full Rails app under test/ interesting!

  2. jfcoutureOctober 19, 2007 @ 01:47 PM

    Interesting.

    What's the difference between this and the make_resourceful plugin you presented at montreal on rails (except from a different api)?

  3. Henrik NOctober 19, 2007 @ 02:02 PM

    I like your objecturl helper and using belongsto to enumerate parent resources.

    I've hacked together some stuff for handling polymorphic resources (might tidy up and release at some point), but it's not as nice. I've been waiting for something better to come along, and perhaps be merged into Rails, so I don't have to bother. ;) This seems like a step in the right direction.

    Have you found yourself wanting to render in the parent's layout? E.g. /posts/1/comments with the "posts" layout; /products/1/comments with the "products" layout. I'm currently doing something like http://pastie.textmate.org/private/4qzqfi2ji0eybu4dj3nhgw which works well enough for my purposes, but I don't really like it. I'm not sure if rendering in the parent layout is a good idea at all – there could be issues with not having access to the parent's helpers, the layout may rely on data that the parent controller would set in filters (e.g. find_post)...

    Since you seem to have good ideas in other polymorphic resource matters, do you have any here? :)

  4. JeremyOctober 20, 2007 @ 07:31 AM

    jfcouture: I don't see a lot of difference in purpose, but the API is a good bit different (e.g., you inherit from a class rather than some metaprogramming, which is sure to boost performance, the callbacks are structured differently, etc.).

    The big win I see here, though, is the formats. I don't think you can easily do that with make_resourceful IIRC.

  5. James GolickOctober 20, 2007 @ 08:52 AM

    Differences between this and m_r:

    • API (one-liner to add RJS to several actions; set the flash independently from the responses; etc).
    • m_r has no support for polymorphic resources (there is a patch around, but even then, it doesn't compare to r_c's support).
    • r_c's test coverage means that you can hack it, if need be, and not worry as much about breaking something (and, write tests while you're at it to make sure your changes work right). Also, development was and will be test-driven, so all components are designed with testability in mind.
    • edge/2.0 support.
    • r_c is missing the publish feature that m_r recently added. That's coming soon, though.
  6. Andrew NesbittOctober 24, 2007 @ 04:36 AM

    So you've not seen this then: http://agilewebdevelopment.com/plugins/resources_controller

    Almost exactly the same name and functionality made by ian white a few months ago.

  7. Jason GreenOctober 24, 2007 @ 04:38 AM

    Hi James,

    Great plugin! Have you seen resources_controller ?

    http://svn.ardes.com/railsplugins/resourcescontroller/

    plugin to facilitate inheritance and DRYness in your resources controllers.

  8. James GolickOctober 24, 2007 @ 05:42 AM

    Nope, I had not heard of that... very very similar functionality. His looks like a great plugin too!

    Maybe resource_controller (mine) needs a name change. Anybody have any ideas?

  9. HamptonOctober 24, 2007 @ 07:52 AM

    Just to be clear...

    • m_r currently runs from 1.2 to 2.0 with a single version.
    • JS and HTML are automatically enabled on all actions, so you need to do nothing to get them working. Its just baked right in.
    • I'm not really sure how this is more testable. Perhaps you have more hooks for Controller unit tests.
    • mr doesn't have *built-in* polymorphic routes (just nested). However, I rarely use them anyway. I prefer to override my currentmodel method in m_r myself because I believe some parts of the controller just should be written by hand for clarity.

    Anyhow, this isn't an mr/rc pissing contest. Different styles, different strengths.

    I do have one major concern.... what if you only want a few actions? I find myself often just wanting a "show" action and nothing else. Is this possible?

  10. James GolickOctober 24, 2007 @ 08:18 AM

    Hampton is 100% right; it's not a pissing contest. Just two great plugins with different styles and strengths.

    You can specify certain actions by saying:

    actions :all, :except => [:show, :index] or actions :show, :index

    It's a bit broken right now, though. I will correct that with a maintenance release before the end of the week.

  11. OshOctober 25, 2007 @ 04:20 PM

    The URL helpers in helpers.rb and urligence seem to assume the object's class name is always the correct name to use within the URL path. What is the best way to override this with a custom resource name?

    Example: We have a model SpecialPost, and a controller SpecialPostsController. We want to expose paths such as, "/user/1/posts/", rather than "/user/1/special_posts/". So in routes.rb we do: map.resources :users do |user| user.resources :posts, :controller => 'specialposts', :nameprefix => 'user_' end

  12. James GolickOctober 25, 2007 @ 08:45 PM

    Osh: Sorry your comment didn't appear for so long. It got stuck in my filter. More and more seem tobe getting stuck in there.

    The url issue is due to a current limitation with Urligence. The way smart_url works is by serializing all of the parameters, and calling the url helper that it thinks is responsible for generating that route. There's one last bit of syntax to add to urligence that will solve the problem, and I can bake it in to r_c, so that the free urls will work for non-standard controller names.

    Shoot me an email, or send a post to the mailing list (see my latest blog entry), and we can chat further about this. I'll have you a fix very, very soon (tomorrow, hopefully).

  13. Jeff JonesNovember 21, 2007 @ 03:03 PM

    +1 for developing the ability to handle deeply nested resources. I am just starting REST and deeply nested resources and was almost tearing my hair out (messy messy messy) before discovering these plugins existed.

  14. MatthewDecember 19, 2007 @ 05:22 PM

    Sadly, none of the available resource controller plugins seem to help much with the hardest part of polymorphic/nested controllers: Different views/layouts and different before_filters depending on what the resource is nested under. /users/3/posts is going to look and work a lot differently than /forums/3/posts, for example.

    Do deeply nested resources work now? I see Jeff gave you a +1 for it, but I wasn't sure if that was to encourage you to do it or a reward for doign it. I could really use it.

    What about the model class name vs. route path limitation? Has that been accounted for since the this blog post was made? Also important for me.

    Frankly, I'm almost starting to regret this huge overhaul of one of my applications to be totally (or as much as possible) RESTful. Multiplexing (nested) controllers is kind of a pain in the butt, even with the plugins.

  15. Matt MitchellDecember 26, 2007 @ 06:17 PM

    Think I found a little missing piece...

    The ResourceController::Controller self.included method calls helpermethod, but it doesn't include ":parentparam". I added it in my code and works as I'd except.

    Thank you for this wonderful plugin! Matt

  16. Micah WinkelspechtMarch 24, 2008 @ 02:28 PM

    I love the plugin, thanks for releasing it!

    Very often, I run into a situation where I want to scope a resource by the current_user. For instance, for a 'new' action, I might do:

    def new current_user.resources.build end

    In rc, I can't just use belongsto :user because the user is not explicitly included in my route, it's assumed. How can I easily change the base object so that when I use the endof_associationchain method or similar methods, it uses my properly scoped resource?

    Thanks!

  17. Ed MoulinApril 08, 2008 @ 03:49 PM

    First of all,

    Congratulations for your work, but I wold like to install resource_controller in Aptana. Is it possible?

  18. Ben JohnsonMay 08, 2008 @ 05:18 PM

    Hi James,

    Great plugin! I changed something in the plugin and thought I would make you aware and get your thoughts.

    def namespaces names = self.class.name.split("::") names.pop

    names.map(&:underscore).map(&:to_sym) end

    I change the map function to :underscore instead of :downcase. For example, I had a my_account name space. It was trying to create urls like the following:

    newmyaccountaddress_path

    instead of the correct route

    newmy_accountaddress_path

    Not sure if this was intentional or not, but thought I'd let you know.

Comment






Clicky Web Analytics