ActiveRecord::Enum の無効値エラーメッセージに有効値一覧を追加
ActiveRecordのEnum属性に無効な値を代入した際に発生する ArgumentError のメッセージが拡張され、有効な値の一覧が含まれるようになりました。これにより、デバッグ時にモデル定義を開かずともエラーメッセージだけで対処方法を把握できます。
背景
これまでのエラーメッセージは、何が問題かを伝えるだけで、何が正しいのかを教えてくれませんでした。'bogus' is not a valid status というメッセージは無効な値を指摘しますが、開発者はどの値が有効なのかを知るためにモデルの定義ファイルを別途参照する必要がありました。
Enumのメンバーが数個であれば暗記も容易ですが、実際のアプリケーションでは状態遷移が多いモデルも珍しくなく、その都度ファイルを確認するのはデバッグの流れを断ち切るコストになります。この変更はエラーメッセージにコンテキストを持たせることで、そのコストをゼロにします。
技術的な変更
変更は activerecord/lib/active_record/enum.rb の assert_valid_value メソッド内の1行のみです。エラーメッセージの文字列生成部分に、マッピングキーの一覧を追記する式が追加されました。
変更前:
raise ArgumentError, "'#{value}' is not a valid #{name}"
変更後:
raise ArgumentError, "'#{value}' is not a valid #{name}. Valid values are: #{mapping.keys.map(&:inspect).join(", ")}"
キーの列挙には map(&:inspect) が使われています。Enumのキーがシンボルで定義されている場合は :proposed のように、文字列で定義されている場合は "proposed" のように、それぞれ型に即した表現でレンダリングされます。これにより、メッセージを読んだだけでそのEnumがシンボルベースか文字列ベースかが判別でき、代入時の記述方法を誤らずに済みます。
テストも合わせて更新されています。変更前は assert_equal でメッセージ全体を文字列照合していましたが、変更後は2つの assert_match による正規表現マッチに切り替わりました。1つ目はプレフィックス部分 'unknown' is not a valid status. を、2つ目は有効値の1つ "proposed" がメッセージに含まれることを確認します。これにより、Enumメンバーの順序が将来変わってもテストが壊れない設計になっています。
設計判断
mapping.keys のみを列挙し、mapping.values(内部の整数値など)は含めない設計が採用されています。開発者がEnum属性に代入する際に使用するのは常にキー側であるため、値側の情報はノイズにしかなりません。エラーメッセージの読み手にとって必要な情報に絞った判断です。
また、例外クラス・トリガー条件はいずれも変更されていません。メッセージの末尾に情報を追記する形をとることで、既存の rescue ArgumentError ブロックや e.message を使ったログ処理への影響を最小化しています。ただし、assert_equal でメッセージ全文を照合しているテストコードは修正が必要になるため、本PR自身がその対応例を示しています。
まとめ
1行の変更でありながら、EnumのデバッグUXを実質的に改善した本PRは、「エラーメッセージは問題を伝えるだけでなく、解決策のヒントも提供すべき」という設計思想の体現です。inspect による型安全な表示と、順序非依存なテスト設計という細部にも、そのこだわりが表れています。