Pattern MatchingでParametersの残余キーが取得できるように deconstruct_keys を改良
Pattern matching で ActionController::Parameters を { key:, **rest } の形で分解できなかった問題を、deconstruct_keys がキー指定なしの場合に全パラメータを返すよう修正し、Hash#deconstruct_keys と同等の振る舞いを実現しました。
背景
パターンマッチングが rest バインディングで失敗していた ことが、本変更の出発点です。ActionController::Parameters に対して { name:, **rest } のようなパターンを使用すると、deconstruct_keys が空ハッシュを返すためマッチしませんでした。実例として、params = ActionController::Parameters.new(name: "Bob", age: 22).permit! を case 文で分解しようとすると、name が取得できず rest も空になるという挙動が報告されていました。これにより、Rails のパラメータオブジェクトと Ruby の Hash の挙動に不整合が生じ、開発者が期待通りにパターンマッチングを利用できない状況が生まれていました。
技術的な変更
deconstruct_keys の実装がキー指定なしで全体を返すよう変更されました。従来は slice(*keys).each... のみで、keys が nil または空の場合は空ハッシュが生成されていました。新しい実装は次の通りです。
def deconstruct_keys(keys)
params = keys ? slice(*keys) : self
params.each.with_object({}) { |(key, value), hash| hash.merge!(key.to_sym => value) }
end
テストが追加され、期待動作が検証されました。equality_test.rb に新たに deconstruct_keys matches a pattern with a rest binding テストが導入され、{ person: { age:, **rest } } のようなパターンで rest が正しく残りのキーを保持することが確認されています。
test "deconstruct_keys matches a pattern with a rest binding" do
@params => { person: { age:, **rest } }
assert_equal "32", age
assert_equal %i[name addresses].sort, rest.keys.map(&:to_sym).sort
end
設計判断
Hash#deconstruct_keys との挙動を合わせる方針が採用されました。ActionController::Parameters は内部的に HashWithIndifferentAccess を継承しているため、Hash の deconstruction と同様の振る舞いを提供すれば、開発者は追加のラッピングや変換なしにパターンマッチングを利用できます。キーが明示されている場合は従来通り slice で限定し、指定が無い場合は自インスタンス全体を走査するシンプルな分岐を追加しただけで、既存コードへの破壊的変更は回避されています。
まとめ
この変更により、ActionController::Parameters でも `rest` バインディングが正しく機能**し、パラメータの部分抽出が Ruby の Hash と同等に扱えるようになりました。実装は最小限の条件分岐に留められ、既存の挙動を壊さずに API の一貫性が向上したことが最大の意義です。