Skip to content
Permalink
Browse files

Add config.default_method_for_update to support PATCH

PATCH is the correct HTML verb to map to the #update action. The
semantics for PATCH allows for partial updates, whereas PUT requires a
complete replacement.

Changes:
* adds config.default_method_for_update you can set to :patch
* optionally use PATCH instead of PUT in resource routes and forms
* adds the #patch verb to routes to detect PATCH requests
* adds #patch? to Request
* changes documentation and comments to indicate support for PATCH

This change maintains complete backwards compatibility by keeping :put
as the default for config.default_method_for_update.
  • Loading branch information...
dlee committed May 6, 2011
1 parent 66b7eb1 commit 002713c64568114f3754799acc0723ea0d442f7a
Showing with 402 additions and 151 deletions.
  1. +1 −0 Gemfile
  2. +2 −2 actionpack/lib/action_controller/metal/http_authentication.rb
  3. +7 −6 actionpack/lib/action_controller/metal/responder.rb
  4. +6 −1 actionpack/lib/action_controller/test_case.rb
  5. +6 −0 actionpack/lib/action_dispatch/http/request.rb
  6. +1 −0 actionpack/lib/action_dispatch/railtie.rb
  7. +9 −6 actionpack/lib/action_dispatch/routing.rb
  8. +71 −54 actionpack/lib/action_dispatch/routing/mapper.rb
  9. +19 −7 actionpack/lib/action_dispatch/testing/integration.rb
  10. +2 −0 actionpack/lib/action_view/base.rb
  11. +2 −2 actionpack/lib/action_view/helpers/form_helper.rb
  12. +4 −4 actionpack/lib/action_view/helpers/url_helper.rb
  13. +1 −0 actionpack/lib/action_view/railtie.rb
  14. +1 −1 actionpack/test/controller/caching_test.rb
  15. +23 −1 actionpack/test/controller/integration_test.rb
  16. +35 −0 actionpack/test/controller/mime_responds_test.rb
  17. +14 −1 actionpack/test/controller/request_forgery_protection_test.rb
  18. +16 −9 actionpack/test/controller/resources_test.rb
  19. +14 −1 actionpack/test/controller/routing_test.rb
  20. +11 −4 actionpack/test/dispatch/request_test.rb
  21. +9 −0 actionpack/test/template/form_helper_test.rb
  22. +6 −0 actionpack/test/template/form_tag_helper_test.rb
  23. +3 −3 activemodel/lib/active_model/lint.rb
  24. +7 −0 activeresource/lib/active_resource/connection.rb
  25. +12 −4 activeresource/lib/active_resource/custom_methods.rb
  26. +3 −3 activeresource/lib/active_resource/http_mock.rb
  27. +8 −4 activeresource/test/cases/format_test.rb
  28. +2 −2 activeresource/test/cases/http_mock_test.rb
  29. +1 −1 railties/guides/source/action_controller_overview.textile
  30. +2 −1 railties/guides/source/ajax_on_rails.textile
  31. +5 −2 railties/guides/source/configuring.textile
  32. +10 −5 railties/guides/source/form_helpers.textile
  33. +25 −17 railties/guides/source/routing.textile
  34. +4 −2 railties/guides/source/testing.textile
  35. +2 −1 railties/lib/rails/application/configuration.rb
  36. +3 −3 railties/lib/rails/generators/active_model.rb
  37. +3 −0 railties/lib/rails/generators/rails/app/templates/config/application.rb
  38. +2 −2 railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
  39. +45 −2 railties/test/application/configuration_test.rb
  40. +5 −0 railties/test/generators/app_generator_test.rb
@@ -8,6 +8,7 @@ else
gem 'arel'
end

gem 'rack-test', :git => "https://github.com/brynary/rack-test.git"
gem 'bcrypt-ruby', '~> 3.0.0'
gem 'jquery-rails'

@@ -279,7 +279,7 @@ def secret_token(request)
#
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
# POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# of this document.
#
# The nonce is opaque to the client. Composed of Time, and hash of Time with secret
@@ -293,7 +293,7 @@ def nonce(secret_key, time = Time.now)
end

# Might want a shorter timeout depending on whether the request
# is a PUT or POST, and if client is browser or web service.
# is a PATCH, PUT, or POST, and if client is browser or web service.
# Can be much shorter if the Stale directive is implemented. This would
# allow a user to use new nonce without prompting user again for their
# username and password.
@@ -53,7 +53,7 @@ module ActionController #:nodoc:
# end
# end
#
# The same happens for PUT and DELETE requests.
# The same happens for PATCH/PUT and DELETE requests.
#
# === Nested resources
#
@@ -116,8 +116,9 @@ module ActionController #:nodoc:
class Responder
attr_reader :controller, :request, :format, :resource, :resources, :options

ACTIONS_FOR_VERBS = {
DEFAULT_ACTIONS_FOR_VERBS = {
:post => :new,
:patch => :edit,
:put => :edit
}

@@ -132,7 +133,7 @@ def initialize(controller, resources, options={})
end

delegate :head, :render, :redirect_to, :to => :controller
delegate :get?, :post?, :put?, :delete?, :to => :request
delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request

# Undefine :to_json and :to_yaml since it's defined on Object
undef_method(:to_json) if method_defined?(:to_json)
@@ -259,11 +260,11 @@ def has_errors?
resource.respond_to?(:errors) && !resource.errors.empty?
end

# By default, render the <code>:edit</code> action for HTML requests with failure, unless
# the verb is POST.
# By default, render the <code>:edit</code> action for HTML requests with errors, unless
# the verb was POST.
#
def default_action
@action ||= ACTIONS_FOR_VERBS[request.request_method_symbol]
@action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
end

def resource_errors
@@ -225,7 +225,7 @@ def exists?
# == Basic example
#
# Functional tests are written as follows:
# 1. First, one uses the +get+, +post+, +put+, +delete+ or +head+ method to simulate
# 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
# an HTTP request.
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
# the controller's HTTP response, the database contents, etc.
@@ -392,6 +392,11 @@ def post(action, *args)
process(action, "POST", *args)
end

# Executes a request simulating PATCH HTTP method and set/volley the response
def patch(action, *args)
process(action, "PATCH", *args)
end

# Executes a request simulating PUT HTTP method and set/volley the response
def put(action, *args)
process(action, "PUT", *args)
@@ -97,6 +97,12 @@ def post?
HTTP_METHOD_LOOKUP[request_method] == :post
end

# Is this a PATCH request?
# Equivalent to <tt>request.request_method == :patch</tt>.
def patch?
HTTP_METHOD_LOOKUP[request_method] == :patch
end

# Is this a PUT request?
# Equivalent to <tt>request.request_method_symbol == :put</tt>.
def put?
@@ -23,6 +23,7 @@ class Railtie < Rails::Railtie
ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
ActionDispatch::Routing::Mapper.default_method_for_update = app.config.default_method_for_update

ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
@@ -182,10 +182,13 @@ module ActionDispatch
#
# == HTTP Methods
#
# Using the <tt>:via</tt> option when specifying a route allows you to restrict it to a specific HTTP method.
# Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>.
# If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>.
# The default value is <tt>:any</tt> which means that the route will respond to any of the HTTP methods.
# Using the <tt>:via</tt> option when specifying a route allows you to
# restrict it to a specific HTTP method. Possible values are <tt>:post</tt>,
# <tt>:get</tt>, <tt>:patch</tt>, <tt>:put</tt>, <tt>:delete</tt> and
# <tt>:any</tt>. If your route needs to respond to more than one method you
# can use an array, e.g. <tt>[ :get, :post ]</tt>. The default value is
# <tt>:any</tt> which means that the route will respond to any of the HTTP
# methods.
#
# Examples:
#
@@ -198,7 +201,7 @@ module ActionDispatch
# === HTTP helper methods
#
# An alternative method of specifying which HTTP method a route should respond to is to use the helper
# methods <tt>get</tt>, <tt>post</tt>, <tt>put</tt> and <tt>delete</tt>.
# methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
#
# Examples:
#
@@ -283,6 +286,6 @@ module Routing
autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes'

SEPARATORS = %w( / . ? ) #:nodoc:
HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] #:nodoc:
HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
end
end
@@ -7,6 +7,8 @@
module ActionDispatch
module Routing
class Mapper
cattr_accessor(:default_method_for_update) {:put}

class Constraints #:nodoc:
def self.new(app, constraints, request = Rack::Request)
if constraints.any?
@@ -465,7 +467,7 @@ module HttpHelpers
#
# Example:
#
# get 'bacon', :to => 'food#bacon'
# get 'bacon', :to => 'food#bacon'
def get(*args, &block)
map_method(:get, args, &block)
end
@@ -475,17 +477,27 @@ def get(*args, &block)
#
# Example:
#
# post 'bacon', :to => 'food#bacon'
# post 'bacon', :to => 'food#bacon'
def post(*args, &block)
map_method(:post, args, &block)
end

# Define a route that only recognizes HTTP PATCH.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
#
# patch 'bacon', :to => 'food#bacon'
def patch(*args, &block)
map_method(:patch, args, &block)
end

# Define a route that only recognizes HTTP PUT.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
#
# put 'bacon', :to => 'food#bacon'
# put 'bacon', :to => 'food#bacon'
def put(*args, &block)
map_method(:put, args, &block)
end
@@ -495,7 +507,7 @@ def put(*args, &block)
#
# Example:
#
# delete 'broccoli', :to => 'food#broccoli'
# delete 'broccoli', :to => 'food#broccoli'
def delete(*args, &block)
map_method(:delete, args, &block)
end
@@ -522,13 +534,13 @@ def map_method(method, args, &block)
# This will create a number of routes for each of the posts and comments
# controller. For <tt>Admin::PostsController</tt>, Rails will create:
#
# GET /admin/posts
# GET /admin/posts/new
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PUT /admin/posts/1
# DELETE /admin/posts/1
# GET /admin/posts
# GET /admin/posts/new
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PUT/PATCH /admin/posts/1
# DELETE /admin/posts/1
#
# If you want to route /posts (without the prefix /admin) to
# <tt>Admin::PostsController</tt>, you could use
@@ -556,13 +568,13 @@ def map_method(method, args, &block)
# not use scope. In the last case, the following paths map to
# +PostsController+:
#
# GET /admin/posts
# GET /admin/posts/new
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PUT /admin/posts/1
# DELETE /admin/posts/1
# GET /admin/posts
# GET /admin/posts/new
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PUT/PATCH /admin/posts/1
# DELETE /admin/posts/1
module Scoping
# Scopes a set of routes to the given default options.
#
@@ -651,13 +663,13 @@ def controller(controller, options={})
#
# This generates the following routes:
#
# admin_posts GET /admin/posts(.:format) admin/posts#index
# admin_posts POST /admin/posts(.:format) admin/posts#create
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
# admin_post PUT /admin/posts/:id(.:format) admin/posts#update
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
# admin_posts GET /admin/posts(.:format) admin/posts#index
# admin_posts POST /admin/posts(.:format) admin/posts#create
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
# admin_post PUT/PATCH /admin/posts/:id(.:format) admin/posts#update
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
#
# === Options
#
@@ -972,12 +984,12 @@ def resources_path_names(options)
# the +GeoCoders+ controller (note that the controller is named after
# the plural):
#
# GET /geocoder/new
# POST /geocoder
# GET /geocoder
# GET /geocoder/edit
# PUT /geocoder
# DELETE /geocoder
# GET /geocoder/new
# POST /geocoder
# GET /geocoder
# GET /geocoder/edit
# PUT/PATCH /geocoder
# DELETE /geocoder
#
# === Options
# Takes same options as +resources+.
@@ -1002,8 +1014,10 @@ def resource(*resources, &block)
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
if parent_resource.actions.include?(:update)
send default_method_for_update, :update
end
end
end

@@ -1020,13 +1034,13 @@ def resource(*resources, &block)
# creates seven different routes in your application, all mapping to
# the +Photos+ controller:
#
# GET /photos
# GET /photos/new
# POST /photos
# GET /photos/:id
# GET /photos/:id/edit
# PUT /photos/:id
# DELETE /photos/:id
# GET /photos
# GET /photos/new
# POST /photos
# GET /photos/:id
# GET /photos/:id/edit
# PUT/PATCH /photos/:id
# DELETE /photos/:id
#
# Resources can also be nested infinitely by using this block syntax:
#
@@ -1036,13 +1050,13 @@ def resource(*resources, &block)
#
# This generates the following comments routes:
#
# GET /photos/:photo_id/comments
# GET /photos/:photo_id/comments/new
# POST /photos/:photo_id/comments
# GET /photos/:photo_id/comments/:id
# GET /photos/:photo_id/comments/:id/edit
# PUT /photos/:photo_id/comments/:id
# DELETE /photos/:photo_id/comments/:id
# GET /photos/:photo_id/comments
# GET /photos/:photo_id/comments/new
# POST /photos/:photo_id/comments
# GET /photos/:photo_id/comments/:id
# GET /photos/:photo_id/comments/:id/edit
# PUT/PATCH /photos/:photo_id/comments/:id
# DELETE /photos/:photo_id/comments/:id
#
# === Options
# Takes same options as <tt>Base#match</tt> as well as:
@@ -1104,13 +1118,13 @@ def resource(*resources, &block)
#
# The +comments+ resource here will have the following routes generated for it:
#
# post_comments GET /posts/:post_id/comments(.:format)
# post_comments POST /posts/:post_id/comments(.:format)
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
# comment PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
# post_comments GET /posts/:post_id/comments(.:format)
# post_comments POST /posts/:post_id/comments(.:format)
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
# comment PUT/PATCH /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
#
# === Examples
#
@@ -1138,11 +1152,14 @@ def resources(*resources, &block)
get :new
end if parent_resource.actions.include?(:new)

# TODO: Only accept patch or put depending on config
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
if parent_resource.actions.include?(:update)
send default_method_for_update, :update
end
end
end

Oops, something went wrong.

3 comments on commit 002713c

@sobrinho

This comment has been minimized.

Copy link
Contributor

sobrinho replied Feb 22, 2012

\o/

@rinaldifonseca

This comment has been minimized.

Copy link
Contributor

rinaldifonseca replied Feb 26, 2012

isn't "HTTP" verb?

@avocade

This comment has been minimized.

Copy link

avocade replied May 28, 2013

Great patch, no pun intended.

Please sign in to comment.
You can’t perform that action at this time.