ParamsWrapperの挙動
- rails
spec記述中に見つけた挙動
試みたこと
「POSTリクエストを受け付けてファイル出力ジョブを予約する」というAPIエンドポイントを追加したく、ほぼ同じ挙動をする FugasController
をコピペしつつ HogesController
を作った。エンドポイントも /api/v1/exports/hoges
と /api/v1/exports/fugas
という感じなのだけど、この命名は RESTful APIとしてアプリケーション内のリソース名を用いたものではなく、出力されるファイル名に基づいていた。 これは今回のケースで大事な前提である。
追加した HogesController
にrequest specを書きたかったので、 FugasController
に対するspecをパクって書いた。
RSpec.describe 'Api::V1::Exports::Hoges', type: :request do
describe 'POST /api/v1/exports/hoges' do
before do
headers['Content-Type'] = 'application/json'
params['year'] = year
end
describe '正常系' do
context 'yearが正しい西暦の場合' do
let(:year) { 2022 }
it '202 accepted のレスポンスが返ること' do
subject
expect(response).to have_http_status(:accepted)
assert_response_schema_confirm
end
end
end
end
end
起きたこと
なぜかパラメータが足りなくて失敗した。
既存の ...fugas
が飛んでくる FugasController
で受け取る params
は以下のような形になる。
{ "year": 2022, "fuga": {"year": 2022} }
なので、 FugasController
では次のようにして params
を受け取っている。
def valid_params
params.require(:fuga).permit(:year)
end
これで valid_params[:year]
は正常に受け取れているのである。
一方、新たに追加した ...hoges
の params
はこうなる。
{ "year": 2022, "hoge": {} }
なので、 FugasController
と同じノリでパラメータを絞ると「 year
がない」ということでコケてしまうのである。
あるべき姿がどうかはともかく、とりあえず既存のエンドポイントに関するコントローラはすべて同じ挙動をしているので、今回追加した FugasController
も HogesController
と同じように params[:hoge][:year]
で取得できるようになってほしい。
この Hoge
と Fuga
の間にある挙動の差は何なのか全然わからなくて詰んだ。
原因
ParamsWrapperという存在
同僚から ParamsWrapper というモジュールの存在を教えてもらい、これはコントローラがクラス名に応じてリクエストをラップしてくれるというものだった。
たとえば、 UsersController
に以下のようなリクエストを投げると
{ "name": "zoshigayan", "age": 27 }
以下のようにしてくれるというものらしい。
{ "name": "zoshigayan", "age": 27, "user": { "name": "zoshigayan", "age": 27 } }
まさにこれである。これが今回の件の鍵を握っている気がするッ。
挙動の差
ドキュメントを読んでいると、しれっと最後にすごいことが書いてあった。雑に訳すとこういう感じ。
もしラップすべきキーを指定しなかった場合、
ParamsWrapper
はそのコントローラに紐づくmodelが存在するかどうかによってキーを決定します。たとえば、以下のようなコントローラの場合:class Admin::UsersController < ApplicationController end
これは
Admin::User
またはUser
モデルが存在するなら、そのモデルをキーの特定に使います。どちらも存在しない場合、フォールバックしてuser
をキーとして用います。
これだけだと具体的に何が起きるのか微妙に分かりづらいけど、つまり HogesController
の挙動は Hoge
というモデルがアプリケーション上に存在するかどうかで異なるということである。そしてこれは同僚調べで分かったことだけど、 モデルが存在した場合、モデルのプロパティ (=テーブルのカラム名) に含まれないキーはラップしてくれなくなる のである。
確かに Hoge
は単なるファイル名だけど Fuga
は別ドメインでモデルとしても使われている。そのため、ActiveRecord
としての Fuga
にプロパティとして存在しない year
はラップしてもらえなかった。
解決策
とりあえず Fuga
の挙動を Hoge
に合わせるには、以下のようにして HogeController
にラップしてほしいキーを明示すればよい。
class Api::V1::Exports::HogesController < Api::V1::ApplicationController
+ wrap_parameters :hoge, include: [:year]
def create
# ...
end
こうしたら確かに HogesController
でも冒頭のような形で params[:hoge][:year]
を受け取ることができた。
そもそもファイル出力用APIなら ParamsWrapper
を有効化しないほうがいいのではないかという気もしつつ、とりあえず既存のAPIと挙動を揃えられたのでよかったよかった。おわり。