Hi Peter and Charlie,
Sorry about my delay in responding -- I'm planning a wedding
and my future bride had two days off, so I spent them with
her getting some details ironed out.
Okay, had a chance to look through this. The code snippet you
include is very interesting:
def collection
conditions << @books = Book.find(:all)
resource.post do
@book = @books.build(params[:book])
if @book.save
render_post_success :action => 'by_id', :id => @book
else
render :action => 'new', :status => HTTP::Status::BAD_REQUEST
end
end
end
I have to say its quite clever, I wouldn't have thought about it
myself. First, let me make sure I understand the mechanics
(please correct me if I make any mistakes).
* Using this approach, the url would be http://mysite/mycontroller/
collection and I could POST to it. Assumedly, I could also GET
and PUT if I did this:
def collection
resource.post do
end
resource.get do
end
end
That's correct, although I'm kinda particular about
how my URI's look, so I'd probably use a route to
remove the word the "collection" from the URI.
As with normal rails actions, all resources respond
to get/head requests without you needing to specifically
define a handler for it. The only reason in my framework
to do so is to specify the caching headers.
The jump shouldn't be that far for most rails developers
as its technically equivalent to:
def collection
if request.post?
end
if request.get?
end
end
There are four key differences though:
1. It handles OPTIONS requests. Each time resource.<method>
is called it will "register" the method as allowed, and
respond to the OPTIONS request by setting the proper
Allow response header.
2. If the request is for a method that isn't "registered",
we return a 405 Method Not Allowed response.
3. If the request is for a method not defined in RFC 2616
or elsewhere, like WebDAV, we return a 501 Not Implemented.
4. Before the method handler is executed it will check the
"conditions" object, and compare it against the If-* request
headers. If the conditions on the server match what the
client last saw, we return a 304 Not Modified in the
case of GET/HEAD requests, or 412 Precondition Failed
in the case of all other HTTP methods.
The conditions object is basically just a collection of
all the model objects that we're using in the view or
to make logic decisions that affect what's placed in
the view -- by view I mean representation in REST terms.
In this way we have transparent Conditional request
support. Conditional requests allows you to do
Optimistic Locking in your web service without having
to resort to any "hacks".. I think this will be
useful for AJAX apps too.
Of course there's conditional GET support too which
I think has the potential to give large speed increases
in rails apps with minimal work. Not only do you
cut out the transfer time -- making the application
perform faster from the client's POV -- but you also
cut out rendering of the view on the server. With
some of my apps this is like 1/3 of the execution time.
* What template gets invoked - is it always collection.rhtml? I
suppose thinking about this, in the end you only render templates
for GETs so that's probably ok.
I've found this to be fine in every case I test. Here's the
typical responses I use for key HTTP methods:
GET - 200 OK - Response Body
POST - 201 Created - No Response Body, Location header to
newly created resource
PUT - 204 No Content - No Response Body
DELETE - 204 No Content - No Response Body
Even in the case of the PUT and DELETE if you returned
200 and the response body, you could return the same
representation as for a GET request so it would work out
well.
Now, I'm talking about Hi-REST here (oh how I hate that term,
but you both know what I mean, so its useful in disucssion).
For Lo-REST I'd do the following: (assume I'm "tunneling" the
other methods over POST)
GET - 200 OK - Response Body
POST - 303 See Other - No Response Body, Location header to
newly created resource
PUT - 303 See Other - No Response Body, Location header to
resource
DELETE - 303 See Other - No Response Body, Location header to
collection resource
Now in the case of PUT you could also return 200 OK and just
return the same representation as a GET request to the
same URI. I prefer to use 303 so that I don't get into
any weird issues where if someone hits refresh the same
request is sent .. although I guess in the case of a PUT
it wouldn't matter if it was sent again.
* How do you deal with editors - ie., pages that let you create
new items or edit an existing one. Do you just fall back to Rails
default new and edit methods?
I'm not sure I understand exactly what you mean by this.. I'll
try to answer with what I think you're asking, but let me know
if I misunderstood.
For a web service I wouldn't make any special contingency for
"edit" representations. There would be one single resource and
you'd fetch the representation, change it, and then PUT it back
to the resource's URI.
Now web apps are different, since you can't style an HTML page
so that it can be used for both viewing and editing in a
cross-browser way; unless you wanted to rely on javascript
DOM rewriting, which I don't think is an option yet. For a
web app here's how I'd normally lay it out:
/book/1 - Viewable representation of book #1
/book/1/editor - Editable representation of book #1
For a web service I'd PUT to /book/1. With a web app, I'd
have the form on /book/1/editor tunnel a PUT over top of
POST to /book/1/editor, so that in the case of errors I
could bring up the same view.
I think its important to design for pure REST, and only
add on special cases to handle web browsers afterwards.
* How do you deal with filters?
There's nothing special that needs to be done with filters. This
is a normal rails action, so all the same rules would apply.
Now onto a couple issues I see. First, I wonder if the approach
above would be more confusing to other people than an approach
based on the resource do end block. The problem with the approach
above is that it looks just like an action, but behaves fairly
different than an action (or standard Ruby methods for that
matter). Maybe there would be value in just calling it out as a
resource.
I don't see it as being that far removed from a normal action. From
the developers point of view its not much different from a series of
if blocks or a case statement, except that it does a few extra things
behind the scenes which the developer doesn't have to worry about
anyway.
Its not even using any special dispatching for the action.. It just
adds the "resource" method to the controller which executes a block
if the request method matches the "name" of the block.
In that case, what if we combined your ideas above with the
resource do end block approach. Something like this:
resource collection do conditions << @books = Book.find(:all)
method.get do
@book = @books.build(params[:book])
if @book.save
render_post_success :action => 'by_id', :id => @book
else
render :action => 'new', :status => HTTP::Status::BAD_REQUEST
end
end
method.post do etc.
end
end
Is this a workable syntax in Ruby?
Sure it is, no problem.
Would "resource :collection" just create a normal rails action,
or do something different?
Not sure I like the name "method" though. I'd rather see us
overload the request.get? method to execute a block if the
condition matches.. there are other ruby libraries that work
in this way: you have a method that evaluates to true/false,
and if you pass it a block it will execute that block when
it is true.
Here's two options to chew on:
resource :collection do
conditions << @books = Book.find(:all)
request.post? do
@book = Book.new(params[:book])
if @book.save
render_post_success :action => 'by_id', :id => @book
else
render :action => 'new', :status => HTTP::Status::BAD_REQUEST
end
end
end
OR #2
resource :collection do |r|
conditions << @books = Book.find(:all)
r.post do
@book = Book.new(params[:book])
if @book.save
render_post_success :action => 'by_id', :id => @book
else
render :action => 'new', :status => HTTP::Status::BAD_REQUEST
end
end
end
I actually prefer the second option because I'm not sure
its a good idea to repurpose request.post? to execute
a block given the 4 special actions I outlined above
are being performed.
I guess method.get would have to evaluate to true to evalute the
block. Have to go try that out... If it doesn't work, then you
could model the respond_to do syntax in Rails 1.1 as you mentioned
but that seems a bit like overkill to me.
I agree about overkill. The respond_to is sort of a declarative
syntax, and its not really necessary in our case.. we can just execute
the code in-line when its reached, no need to defer execution until
later like with respond_to.
Last, for this solution, how would templates and filters work? If
this would provide a solution to the method renaming I do now I'd
be all for it.
They could work as before with no changes.
Second, I think the cache control functionality is really useful
and should become a part of rails. However, I think it doesn't
belong in the method definitions, just as I don't think that
content types should be in method definitions either (like Peter
did). Instead, I'd would prefer to see a Caching Rails plugin
that would use some metaprogramming to work. Perhaps something
like a filter?
cache :as => :public, :for => 1.hour
cache :as => :public, :for => 1.hour, only => [<resource_name or
action>]
I think you're right about it not needing to be part of the method
definition. The only thing we need to be aware of is that
you would rarely want to apply caching rules across an entire
resource. More likely you'd want to apply caching on a
per-method basis. Most people won't want to cache the response
to DELETE methods, but they will with GET methods.. Peter
said more about this in his reply, so I'll save some comments
when I reply to that email.
Of course, the only person's opinion that matters on this is
David's. Did he give you any feedback on cache control?
He only said that he wanted a simpler syntax so that more people
would use caching properly. IMHO the API in rails for caching
is sort of clumsy.
Hope this helps some...mind if I post parts of the discussion on
my blog?
Go for it, the more discussion the better.
Not trying to steal any of your thunder for the XML.com article or
anything... just more for letting others follow along with our
discussion. Also, should we be having this conversation on the
REST formats mailing list...hopefully would get some feedback from
David (it would be nice to come up with something he likes so it
gets into Rails someday)? Feel free to post your response there
if you think its appropriate.
Do you guys want to repost these on the REST microformats
mailing list? I'd be all for that.