[uf-rest] RESTified Rails Controller

Dan Kubb dan.kubb at autopilotmarketing.com
Tue Nov 15 00:57:33 PST 2005


Hi all,

I created a proof of concept controller that implements some of the
ideas we've been talking about for a more RESTful Rails:

   http://www.onautopilot.com/oss/rails/rest_controller.rb

Here's an example of its usage:

===================================================================

# config/routes.rb

ActionController::Routing::Routes.draw do |map|
   map.connect ':controller/:action', :action =>  
'index', :requirements => { :action => /(?:[a-z](?:[-_]?[a-z]+)*)/ }
   map.connect ':controller/:id',     :action =>  
'show',  :requirements => { :id     => /\d+/                       }
   map.connect ':controller/:id/:action',
end

===================================================================

# app/controllers/person_controller.rb

class PersonController < RestController
   verbs_for :index do
     def get
       @person_pages, @people = paginate(:people, :per_page => 20)
       @etag        = @people.collect { |p| p.lock_version }.join(',')
       @modified_at = @people.collect { |p| p.updated_at   }.sort.last
     end

     def post
       @person = Person.new(params[:person])
       if meets_conditions? and @person.save
         flash[:notice] = 'The person was successfully created'
         render_post_success(:id => @person)
       end
     end
   end

   verbs_for :show do
     def load_person
       person       = Person.find(params[:id])
       @etag        = person.lock_version
       @modified_at = person.updated_at
       person
     rescue ActiveRecord::RecordNotFound
       flash[:warning] = 'No person found'
     end

     def get
       @person = load_person
     end

     def put
       if @person = load_person
         @person.attributes = params[:person]
         if meets_conditions? and @person.save
           flash[:notice] = 'The person was successfully updated'
           render_put_success(:id => @person)
         end
       end
     end

     def delete
       if @person = load_person and meets_conditions? and  
@person.destroy
         flash[:notice] = 'The person was successfully deleted'
         render_delete_success(:id => @person)
       end
     end
   end
end

===================================================================

A couple of things will happen:

   - For the index and show actions, depending on the HTTP method
     a different method is dispatched to.  So if someone does
     a GET on /person/1 the get() method inside the "show" action
     is executed.

     This is based on the verbs_for example David came up with last
     week on the list.

     Since the method "handlers" are defined explicitly it will
     handle HTTP OPTIONS requests automatically.  Through introspection
     an exact list of supported methods will be returned for any
     given URI.

   - Conditional GET is automatically supported when the @etag
     and @modified_at attributes are set.

     Conditional GET basically allows you to skip rendering of
     a view if the copy the client has is the same as what you
     were going to output -- there may be some big performance
     benefits for large pages or complex views.

     Most people will glance over this and not give it a second
     thought, but I really encourage you to look at it in more
     detail -- IMHO, other than the method-based dispatching, I
     think this is the coolest thing in the controller.

     Especially be sure to check out the methods than handle
     the conditional logic, I'd like a second pair of eyes
     familiar with RFC 2616 to look them over:

       if_match?
       if_none_match?
       if_modified_since?
       if_unmodified_since?
       conditional_status
       meets_conditions?
       etag

   - As per David's suggestion the render() method can support
     an :etag and :modified_at arguments -- however to support
     things like conditional PUT/DELETE there needs to have
     a way to set this data way before the render stage.

   - You'll note that the example above properly supports
     conditional PUT and DELETE.  For people doing AJAX apps
     this can be nice to avoid the stale update problem --
     when a client takes a copy of an object, changes it, and
     goes to commit the changes, but the underlying data has
     changed.  With conditional PUT the client specifies the
     ETag and the Last-Modified date of the original object
     when updating it, and the server will ONLY perform the
     update if the server state has not changed.

   - This supports HTTP method tunneling of PUT and DELETE over
     POST using specifically named input tags, using the
     naming conventions here:

       http://www.microformats.org/wiki/rest/rails#PUT_.26_DELETE

     While I don't really like doing this, I would prefer to
     do this than create alternate pathways to handle crippled
     user agents or rely 100% on AJAX for submitting forms with
     methods other than GET or POST.

Ideally I'd like to create a patch so some of this can go into the
rails core, but I'm not sure what would stay in a base REST
controller, and what would be merged into the existing code base.

I'd love to get some feedback -- advice from more experienced rails
developers would be greatly appreciated.  I've done a fair amount
with HTTP, but I've only been coding in Ruby for a couple of weeks.

--

Thanks,

Dan
__________________________________________________________________

Dan Kubb                  Email: dan.kubb at autopilotmarketing.com
Autopilot Marketing Inc.  Phone: 1 (604) 820-0212
                             Web: http://www.autopilotmarketing.com
__________________________________________________________________





More information about the microformats-rest mailing list