Rails5のparameter_parserとregister_encoderについて


この記事は[Rails AdventCalender](http://qiita.com/advent-calendar/2016/ruby_on_rails)の23日目の記事への飛び入りです。

## 概要

parameter_parsersはRails5から `ActionDispatch::ParamsParser` に代わり追加された、MimeTypeに応じてリクエストパラメータをパースしてcontrollerのparamsに渡すための機能です。

register_encoderはRails5からの新機能で、controllerやrequestのテストの中で特定のMimeTypeのリクエスト・レスポンスのパラメータをエンコード・デコードしてテストを書きやすくするためのものです。
（4-2-stableだとgit grepしても見つからなかったので新機能だと言ってますが間違ってたら指摘してください :smiley: ）

今回はMessagePackを追加する場合を例として解説しますが、jsonの場合も知っ得な情報だと思うのでAPI開発している方の参考になればと思います！
（なお、例では `msgpack` と `msgpack-rails` のgemを使っています）

## 事前準備

### MimeTypeの登録

RailsやRackのデフォルトで登録されていないものを扱う場合、MimeTypeにMessagePackのContent-Typeを登録する必要があります。
（JSONはRailsがデフォルトで登録しているので扱うことが出来ます）


```ruby
Mime::Type.register "application/x-msgpack", :msgpack
```

initializerにmime_types.rbがあるのでその中も参考にしてください。
第一引数にContent-Type、第二引数に呼び出すためのシンボルを渡します。

ちなみにここで呼びされている `Mime::Type` というクラスは `mime-type` gemのクラスでなく、Railsが定義しているクラスです（ややこしい）。

https://github.com/rails/rails/blob/c082a7aae4a41b52e58c7dea43e7e9c6c41d11ad/actionpack/lib/action_dispatch/http/mime_type.rb#L183

### respond_toの追加

MimeTypeに登録することで、 `format.xxx` のところに登録したシンボルを使用することが出来ます。

```diff
  def create
    @foo = Foo.new(foo_params)

    respond_to do |format|
      if @foo.save
        format.html { redirect_to @foo, notice: 'Foo was successfully created.' }
        format.json { render :show, status: :created, location: @foo }
+       format.msgpack { render body: @foo.to_msgpack, status: :created }
      else
        format.html { render :new }
        format.json { render json: @foo.errors, status: :unprocessable_entity }
+       format.msgpack { render body: @foo.errors.to_msgpack, status: :unprocessable_entity }
      end
    end
  end
```

Content-Typeも登録したものを自動的に付与してくれます。

## parameter_parsers

### Rails4まで

Rails4までは `ActionDispatch::ParamsParser` というパラメータをパースするためのRackミドルウェアが刺さっており、MimeTypeに応じてパラメータをパースしてくれていました。

https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/params_parser.rb

使い方としてはこんな感じです。

```ruby
Rails.application.config.middleware.swap(
  ::ActionDispatch::ParamsParser, ::ActionDispatch::ParamsParser,
  ::Mime::Type.lookup("application/x-msgpack") => Proc.new { |raw_post|
    MessagePack.unpack(raw_post)
  }
)
```

こうすることで、Content-Typeに `application/x-msgpack` が指定されたリクエストが送られてくるとProcを呼び出してパースし、controllerに渡してparamsとして利用可能になります。



### Rails5以降

Rails5では `ActionDispatch::ParamsParser` はduplicateになったため、 `ActionDispatch::Request.parameter_parsers` を使います。


```ruby
ActionDispatch::Request.parameter_parsers[:msgpack] = -> (raw_post) do
  MessagePack.unpack(raw_post)
end
```

parameter_parsersに事前準備のところで定義したsymbolを渡して上げることで、リクエストのContent-Typeが登録したものと一致した場合、このブロックに渡されてパースが行われるようになります。

## register_encoder

### 今までのテストコード

APIのテストを書く場合、めんどうなテストコードになることが多く、独自でsupportを作ったりしてました。

```ruby
describe "Hoge", type: :request do
  describe 'POST /api/v1/hoge' do
    before { post '/api/v1/hoge', params: params, headers: { 'Content-Type' => 'application/x-msgpack' }
    
    let(:params) { { name: 'foo' }.to_msgpack } # msgpackに変換する
    let(:parsed_body) { MessagePack.unpack(response.body) || {} } # msgpackをパースする
    
    it { expect(parsed_body['id']).to eq 1 }
  end
end
```

### register_encoderを使った場合

Rails5からはregister_encoderに登録するだけでテストコードを簡潔にすることが出来ます。

```ruby
ActionDispatch::IntegrationTest.register_encoder(
  :msgpack,
  param_encoder: -> params { params.to_msgpack },
  response_parser: -> body { MessagePack.unpack(body) || {} } # nilを返すとエラーになるので注意
)
```

これを追加して、 `post '/api/v1/hoge', params: params, as: :msgpack` と `as:` で登録したsymbolを指定することでparam_encoder/response_parserが利用されるようになります。
（事前準備で `Mime::Type` に追加したシンボルとは無関係）

```ruby
describe "Hoge", type: :request do
  describe 'POST /api/v1/hoge' do
    before { post '/api/v1/hoge', params: params, as: :msgpack } # as: :msgpackを指定する
    
    let(:params) { { name: 'foo' } } # パラメータはHashでOK
    
    it { expect(response.parsed_body['id']).to eq 1 } # parsed_bodyにparameter_parserの結果が入っている
  end
end
```

param_encoderはリクエスト時に自動的に `params:` に渡した値をエンコードし、response_parserはレスポンスのパース結果を `parsed_body` に格納します。
こうすることで `to_msgpack` や `MessagePack.unpack` などを呼ばなくてよくなり、テストコードが簡潔になります。

なお、jsonのresponse_parserは[railsがデフォルトで定義している](https://github.com/rails/rails/blob/143fc87cbfee7eb281937be1b404ad18e09dc301/actionpack/lib/action_dispatch/testing/request_encoder.rb#L51)ので、すぐに利用することが出来ます。
param_encoderも利用したい場合は自分で定義してあげると良いでしょう。

## まとめ

Rails5からrails-apiだけでなく、こういったAPIのサポートも増えてきたというのが所感です。
今回はMessagePackを例に上げましたが、例えばJSONAPIなどといった特殊なフォーマット＋ContentTypeのときなど、開発がやりやすくなるケースも少なくないと思いますので活用していきましょう :smile: 

## おまけ

下記のようなsupportを書いておくと、デフォルトのrequest formatがmsgpackになります。

```
module DefaultFormatAsMsgPack
  def post(path, *args)
    args[0] = { as: :msgpack }.merge(args[0].presence || {})
    super(path, *args)
  end
end

RSpec.configure do |c|
  c.include DefaultFormatAsMsgPack, type: :request
end
```

また、 `render` メソッドの `format: :json` のようなformatを自分で定義することも出来るみたいです。

https://github.com/rails/rails/blob/3e45dd988cad539fe2cbc79b505fde99a44472e8/actionpack/lib/action_controller/metal/renderers.rb#L158-L181