Deprecated `namespace` ハッシュオプションの処理を正しく適用
Rails のルーティングで namespace はハッシュ形式とキーワード形式の両方を受け付けるよう設計されています。ハッシュ形式は旧来からの互換性を保つために残っている一方、内部実装では DEFAULT という真値オブジェクトがデフォルトとして設定されています。この sentinel が既に真であるため、||= 演算子が期待通りに働かず、ハッシュで渡された :path・:shallow_path・:shallow_prefix が無視されていました。その結果、生成されるパスが二重になったり、shallow オプションが無視されたりして、ミドルマイグレーション中のアプリで予期しないルートが作られる状態になっていました。
背景
namespace のハッシュ形式は deprecated_options 引数として渡され、内部で assign_deprecated_option が個々のキーを処理します。コードは path ||= assign_deprecated_option(...) という形で代入しており、path が既に DEFAULT(truthy)で初期化されているため代入が実行されず、ハッシュの値が捨てられます。このロジックは as だけが = で代入されているのとは不整合です。結果として namespace :admin, { path: "adm" } が生成する URL は /admin/adm/... ではなく /admin/... となり、さらにハッシュキーが **options に再流入して二重パスが生成されます。
この不整合はテストで検出されず、本番環境にデプロイされたまま静かに動作するリスクがありました。特に shallow_path と shallow_prefix も同様に無視され、ネストされたリソースの shallow 生成が壊れていました。
技術的な変更
本 PR では namespace メソッド内の deprecated オプション代入を ||= から = へ置き換えることで、ハッシュ形式でも正しく値が反映されるようにしました。key? ガードはそのまま残し、キーが存在すれば直接代入するパターンに統一しています。
def namespace(name, deprecated_options = nil, as: DEFAULT, path: DEFAULT, shallow_path: DEFAULT, shallow_prefix: DEFAULT, **options, &block)
if deprecated_options.is_a?(Hash)
as = assign_deprecated_option(deprecated_options, :as, :namespace) if deprecated_options.key?(:as)
- path ||= assign_deprecated_option(deprecated_options, :path, :namespace) if deprecated_options.key?(:path)
- shallow_path ||= assign_deprecated_option(deprecated_options, :shallow_path, :namespace) if deprecated_options.key?(:shallow_path)
- shallow_prefix ||= assign_deprecated_option(deprecated_options, :shallow_prefix, :namespace) if deprecated_options.key?(:shallow_prefix)
+ path = assign_deprecated_option(deprecated_options, :path, :namespace) if deprecated_options.key?(:path)
+ shallow_path = assign_deprecated_option(deprecated_options, :shallow_path, :namespace) if deprecated_options.key?(:shallow_path)
+ shallow_prefix = assign_deprecated_option(deprecated_options, :shallow_prefix, :namespace) if deprecated_options.key?(:shallow_prefix)
assign_deprecated_options(deprecated_options, options, :namespace)
end
変更前は ||= によってデフォルトの DEFAULT が保持され、代入がスキップされていました。= に置き換えることでハッシュで渡された値が path・shallow_path・shallow_prefix に直接設定され、以降のロジックで正しく利用されます。テストファイルにもハッシュ形式の期待動作を検証するケースが追加され、すべてのテストがパスすることが確認されています。
この修正は既存コードへの侵入リスクを最小限に抑え、as と同様の代入パターンに揃えるだけで機能することから、メンテナンスコストも低く抑えられました。
設計判断
修正の基本方針は「既存 API の振る舞いをキーワード形式と一致させ、後方互換性を維持する」ことでした。そのため新しい設定キーやオプションを導入せず、namespace のシグネチャは変更せずに内部ロジックだけを調整しています。key? による存在チェックを残すことで、ハッシュが渡されなかった場合はデフォルトの DEFAULT がそのまま使用され、既存の挙動を壊さない設計となっています。
PR の議論では代替案として別キーの追加が示唆されたものの、最小限の変更でバグを解消できる点が強調され、結局 = への置き換えが採用されました。これにより、ハッシュ形式とキーワード形式の二重サポートが一貫した実装となり、開発者はどちらの記法でも同じ結果が得られます。
結果として、デプリケートされたハッシュ形式は依然として利用可能でありながら、意図したルートが生成されることが保証されました。コードベースへの変更は 3 行の代入修正とテスト追加だけというスコープに収まり、リリースリスクを抑えた実装となっています。
まとめ
namespace の deprecated ハッシュオプションが正しく評価されるようになり、パスの二重生成や shallow オプションの無視といった不具合が解消されました。修正は代入ロジックの統一だけで実現でき、既存のキーワード形式と同等の振る舞いを提供します。追加されたテストはすべて成功し、ハッシュ形式でも期待通りのルートが生成されることが確認されています。この変更により、ミドルマイグレーション中のアプリケーションがサイレントエラーに陥るリスクが低減され、Rails のルーティング設定の信頼性が向上しました。