この記事はRails AdventCalenderの23日目の記事への飛び入りです。
概要
parameter_parsersはRails5から ActionDispatch::ParamsParser
に代わり追加された、MimeTypeに応じてリクエストパラメータをパースしてcontrollerのparamsに渡すための機能です。
register_encoderはRails5からの新機能で、controllerやrequestのテストの中で特定のMimeTypeのリクエスト・レスポンスのパラメータをエンコード・デコードしてテストを書きやすくするためのものです。
(4-2-stableだとgit grepしても見つからなかったので新機能だと言ってますが間違ってたら指摘してください )
今回はMessagePackを追加する場合を例として解説しますが、jsonの場合も知っ得な情報だと思うのでAPI開発している方の参考になればと思います!
(なお、例では msgpack
と msgpack-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: :msgpack
と as:
で登録した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_msgpack
や MessagePack.unpack
などを呼ばなくてよくなり、テストコードが簡潔になります。
なお、jsonのresponse_parserはrailsがデフォルトで定義しているので、すぐに利用することが出来ます。
param_encoderも利用したい場合は自分で定義してあげると良いでしょう。
まとめ
Rails5からrails-apiだけでなく、こういったAPIのサポートも増えてきたというのが所感です。
今回はMessagePackを例に上げましたが、例えばJSONAPIなどといった特殊なフォーマット+ContentTypeのときなど、開発がやりやすくなるケースも少なくないと思いますので活用していきましょう
おまけ
下記のような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を自分で定義することも出来るみたいです。