LoginSignup
12

More than 5 years have passed since last update.

Rails5のparameter_parserとregister_encoderについて

Last updated at Posted at 2017-01-03

この記事はRails AdventCalenderの23日目の記事への飛び入りです。

概要

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

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

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

事前準備

MimeTypeの登録

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

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

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

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

respond_toの追加

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

  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に応じてパラメータをパースしてくれていました。

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

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 を使います。

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

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

register_encoder

今までのテストコード

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

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に登録するだけでテストコードを簡潔にすることが出来ます。

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: :msgpackas: で登録したsymbolを指定することでparam_encoder/response_parserが利用されるようになります。
(事前準備で Mime::Type に追加したシンボルとは無関係)

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_msgpackMessagePack.unpack などを呼ばなくてよくなり、テストコードが簡潔になります。

なお、jsonのresponse_parserはrailsがデフォルトで定義しているので、すぐに利用することが出来ます。
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を自分で定義することも出来るみたいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12