Could/should we defer request verb matches to the Rack Request?
pote opened this issue · comments
I'm reading the code to get a feel of this, and wanted to ask about the reasoning behind the post?
, get?
and other http verb boolean matchers.
In short: is there a specific reason those are implement in Syro instead of just encouraging the usage of req.post?
, req.get?
and all the equivalent Rack request methods? Seems like we could avoid some unnecessary code that way, specifically:
# Request methods
GET = 'GET'.freeze
POST = 'POST'.freeze
PATCH = 'PATCH'.freeze
DELETE = 'DELETE'.freeze
#...
METHODS = [GET, POST, PATCH, DELETE].freeze
#...
def get?
@syro_method == 0
end
def post?
@syro_method == 1
end
def patch?
@syro_method == 2
end
def delete?
@syro_method == 3
end
Running a benchmark shows that - as you would expect - there is maybe a very small percentage gain over reimplementing the method (in the rehearsal section), but doing something like this appeals to me more from the point of view of removing unnecessary code as well as reducing the pollution of the namespace with extra methods, a patch for this would delete the code mentioned above and replace those methods with their Rack response equivalent where needed.
What do you think @soveran ? Am I missing anything?
The reasoning for all that started with the idea that maybe only a small subset of request methods are needed for most applications, and then (arbitrarily) I selected get
, post
, patch
and delete
. I left put
out because having both put
and patch
seems redundant. Then @ehq noted OPTIONS
should probably be included too, so there's room to revisit that decision. With the current implementation, if you want to do something similar but with OPTIONS
, you need to use root { on(req.options?) { ... } }
, and the same goes for any other method. I'm very open to discuss this a lot, it looks like there's room for improvement.
For not using req.post?
, I was thinking it could be better to avoid string comparisons each time a match to the request method is made. It is fairly normal to have something like this:
get {
# ...
}
patch {
#...
}
delete {
#...
}
In the example above we are comparing strings three times, and I wanted to avoid that. But with the current implementation, the result is more or less the same: in one case we do the string comparison before, in the other we do it at the end. At the start of that decision, I was thinking we could pass the cached method to sub-apps, and then the performance gain would be bigger. But given the way all requests terminate with method matchers it is the case that there's probably no performance gain at all. In short, I think you are right and unless we find very good benchmarks for the current approach, or unless we find a way to profit from that pseudo-caching, we can replace the implementation. Another initial thought was that maybe we could call get?
and then get
in application code, and saving an extra string comparison was tempting. But we have to think if that's really a use case, because right now it looks artificial.
Ah, yeah, I thought it was probably because of the unfrozen string comparisons, makes sense.
I'm looking at Rack's code though, and they are doing pretty much the same we're doing here:
GET = 'GET'.freeze
POST = 'POST'.freeze
PUT = 'PUT'.freeze
PATCH = 'PATCH'.freeze
DELETE = 'DELETE'.freeze
HEAD = 'HEAD'.freeze
OPTIONS = 'OPTIONS'.freeze
LINK = 'LINK'.freeze
UNLINK = 'UNLINK'.freeze
TRACE = 'TRACE'.freeze
#... In Rack::Request
# Checks the HTTP request method (or verb) to see if it was of type DELETE
def delete?; request_method == DELETE end
# Checks the HTTP request method (or verb) to see if it was of type GET
def get?; request_method == GET end
# Checks the HTTP request method (or verb) to see if it was of type HEAD
def head?; request_method == HEAD end
# Checks the HTTP request method (or verb) to see if it was of type OPTIONS
def options?; request_method == OPTIONS end
# Checks the HTTP request method (or verb) to see if it was of type LINK
def link?; request_method == LINK end
# Checks the HTTP request method (or verb) to see if it was of type PATCH
def patch?; request_method == PATCH end
# Checks the HTTP request method (or verb) to see if it was of type POST
def post?; request_method == POST end
# Checks the HTTP request method (or verb) to see if it was of type PUT
def put?; request_method == PUT end
# Checks the HTTP request method (or verb) to see if it was of type TRACE
def trace?; request_method == TRACE end
# Checks the HTTP request method (or verb) to see if it was of type UNLINK
def unlink?; request_method == UNLINK end
Rack Source:
- https://github.com/rack/rack/blob/master/lib/rack.rb#L42-L51
- https://github.com/rack/rack/blob/master/lib/rack/request.rb#L121-L149
I get wanting to stick with an int comparison as opposed to a string one, I think it might be one of those things that end up not making a huge difference in performance though, but we should probably run more exhaustive benchmarks to find out for sure.
This approach has the added benefit of already including OPTIONS
and a few others, I also like that we're scoping these things inside the req
object instead of cramming them into the top-level namespace, feels a bit cleaner and makes sense to me logically, but that is very subjective.
Do check my reasoning though, because I'm very prone to jumping to conclusions hastily, but I think we can get away with removing that code without much of a change, I'll send a Pull Request for it, and of course feel free to not merge it, I just want to write the changes before I forget about all this. :)
BTW: beautiful work as always! Thanks for sharing it! \o/
(I also think we probably want to provide both put
and patch
helpers, as well as options
and all that, but that is probably another discussion entirely)
Insta-merged.