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と挙動を揃えられたのでよかったよかった。おわり。