railsのオーバーライドPOSTで独自のHTTPメソッドを扱う

みなさんは確認画面のURIってどうしてるんでしょうか?

例えばユーザの作成時に/users/confirmってするのはなんかしっくり来ないな、と思って調べて以下の記事にたどり着きました。

"confirm"などの追加のアクションはオーバーロードPOST*2であるから、URLは動詞でよいしGETできなくてかまわない、という考えがあって、たぶんRailsもその考えに沿っている感じがするのですが、これにはあまり同意できません。URLとして現れるものは、基本的にGETできる(リソースとして意味がある)べきだと思います。

多くのWebアプリケーションは、オーバーロードPOSTを通じてサポートする操作のために、新しいURIを作成する。たとえば/weblog/myweblog/rebuild-indexのようなURIは、そのURIにリンクしても意味はない。このようにURIメソッド情報を含める代わりに、既存のリソース(/weblog/myweblog)でオーバーロードPOSTをサポートし、入力表現でメソッド情報(method=rebuild-index)を要求すればよい。 「RESTful Webサービス」p.229

これを読んで、確かに、と思ったのでmethod=confirmとして送る方法を調べてみました。

環境

rails4で試しましたが、おそらく3でも同じはず。

view

rails4では編集画面などでPUT/PATCHとして送るべき場面でも、デフォルトではPOSTで投げ、代わりに_methodというパラメタにPUT/PATCHといった値を積んでます。

そのため、単に

<%= form_for @user, method: :confirm do |f| %>
  ...
<% end %>

のようにすれば_method=confirmとして送ってくれます。スゴイ!

routing

とりあえず、なにも聞かずに以下のようなファイルを作ってください。ファイル名は何でも可。

# config/initializers/http_methods.rb

%w(confirm).each do |method|
  Rack::MethodOverride::HTTP_METHODS << method.upcase

  ActionDispatch::Request::HTTP_METHODS << method.upcase 
  ActionDispatch::Request::HTTP_METHOD_LOOKUP[method.upcase] = method.to_sym

  ActionDispatch::Routing::Mapper::HttpHelpers.class_eval do
    define_method(method.to_sym) do |*args, &block|
      map_method method.to_sym, args, &block
    end
  end
end

これはCONFIRMメソッドをサーバ側で処理できるようにするためにあれこれしている処理です。

まずRack::MethodOverride::HTTP_METHODSのところ。

これはrack側でオーバライドPOSTを実現するための仕組みで、POSTメソッド_methodパラメタやHTTP_X_HTTP_METHOD_OVERRIDEヘッダで指定された値で上書きます。 しかし、デフォルトではRFC2616で定義されてるメソッド(CONNECTTRACEを除く)以外が渡された場合はなにもせず、ルーティング側にはそのままPOSTとして渡されてしまうため、ホワイトリストに追加しています。

次にActionDispatch::Request::HTTP_METHODS*のところ。

これはルーティング時に走る処理で、許可されたHTTPメソッドかチェックしています。 RFC2616、2518、3253、3648、3744、5323、5789で定義されているメソッド以外を渡そうとするとinternal server errorが発生してしまうため、ここでもホワイトリストに追加します。

最後にActionDispatch::Routing::Mapper::HttpHelpersのところ。

これはroutes.rb

confirm :users, to: 'users#confirm’

のように書きたかったのでこのように定義してます。

match :users, to: 'users#confirm', via: :confirm

でも問題なく動くので、必要ない場合は削除しても問題ないです。

最後に

とまあrails初心者なりに色々調べてみました。が、こういうことをしている情報はあまり見つからなかったのでrails的には行儀が悪いのかも。もっとスマートな方法があればぜひ教えてください!

追記(2014/08/27)

rspecのrequest specなどでテストする際にconfirm '/hoge'のように書きたい場合は、以下のコードを追加すればOKです。

# config/initializers/http_methods.rb

%w(confirm).each do |method|

  ...

  ActionDispatch::Integration::RequestHelpers.class_eval do
    define_method(method.to_sym) do |path, parameters = nil, headers_or_env = nil|
      process method.to_sym, path, parameters, headers_or_env
    end
  end
end