- Ruby / Rails関連
週刊Railsウォッチ: システムテストでPlaywrightをサポート、to_paramのデリミタを変更可能にほか(20230906前編)
こんにちは、hachi8833です。
お待たせしました!Kaigi on Rails 2023の一般参加チケットの販売を開始しました🎉
今回は先着順での販売となっており、売り切れ次第販売終了となります、参加希望の方はお早めにご購入ください。https://t.co/m0HStQcNvE#kaigionrails— Kaigi on Rails (@kaigionrails) September 4, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Active Jobにafter_discardフックが追加
after_discardは、ジョブが破棄される直前に実行されるブロックを定義する。このメソッドによって、ジョブが失敗したときにブロックを実行可能になるので、ジョブの作成者と、ジョブに含まれるgemやモジュールの双方にとって有用。
after_discardは、ジョブの既存のリトライ動作を尊重するが、リトライの例外がretry_onやdiscard_onに渡されたブロックで処理される場合にも実行する。動機/背景
after_discardは、既にShopifyのモノリスで長年使われている。開発者はこれを利用してクリーンアップや失敗処理のコードを定義しており、ジョブの失敗を追跡する機能もこれに依存している。現在のActive Jobにおける例外処理システムの他の部分は、この振る舞いに近づいているが、ジョブが失敗した場合にコードブロックを明確に実行する方法があることに価値があると思う。
詳細
このプルリクは
after_discardを追加する。既存の振る舞いは変更されない。
after_discardは、ジョブが失敗により破棄される直前のあらゆる状況で呼び出される。例:
retry_onやdiscard_onを使わないジョブが例外をraiseする場合- 例外が
discard_onで処理される場合(ブロックの有無を問わない)- ジョブがその例外のリトライ回数を使い果たした後の
retry_onで例外が処理される場合(ブロックの有無を問わない)after_discardブロックを実行するときに複数の例外がraiseした場合は、最後の例外だけがraiseする。この振る舞いによって、例外発生時であってもすべてのブロックが実行されるようになる。同PRより
「Shopify内部で既に使われているafter_discardがマージされました」「失敗して詰まったジョブは、たいていRedisなどのジョブキューから直接削除してたな〜」「ジョブが失敗したときにJavaのfinallyみたいな感じで後処理を書けるんですね: 既存の振る舞いを変えない形になっているし、あれば便利👍」
# 同Changelogより
class AfterDiscardJob < ActiveJob::Base
after_discard do |job, exception|
Rails.logger.info("#{job.class} raised an exception: #{exception}")
end
def perform
raise StandardError
end
end
参考: Active Job の基礎 - Railsガイド
参考: Rails API ActiveJob::Exceptions::ClassMethods
🔗 テストのresponse.parsed_bodyでパターンマッチング構文をサポート
JSON形式の
response.parsed_bodyをActiveSupport::HashWithIndifferentAccessでパースするようになった
response.parsed_bodyのJSONコンテンツをActiveSupport::HashWithIndifferentAccessでパースしてパターンマッチング互換にすることで、minitestの新しいassert_patternと統合される。同Changelogより
動機/背景
NokogiriとMinitestが、Rubyのパターンマッチングを統合する以下のプルリクをマージした。詳細
このコミットは、新しいアサーションのカバレッジを追加し、
response.parsed_bodyメソッドのAPIドキュメントにサンプルコードを追加する。追加情報
このコミットは、JSONレスポンスでパターンマッチングをサポートするために、
JSON.parseにobject_class: ActiveSupport::HashWithIndifferentAccessを指定して呼び出すようレスポンスパーサーを変更する(理由は、HashのキーがStringインスタンスになるとRubyのパターンマッチング構文と互換性がなくなるため)。例:
irb(main):001:0> json = {"key" => "value"} => {"key"=>"value"} irb(main):002:0> json in {key: /value/} => false irb(main):001:0> json = {"key" => "value"} => {"key"=>"value"} irb(main):002:0> json in {"key" => /value/} .../3.2.0/lib/ruby/gems/3.2.0/gems/irb-1.7.4/lib/irb/workspace.rb:113:in `eval': (irb):2: syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END (SyntaxError) json in {"key" => /value/} ^ .../ruby/3.2.0/lib/ruby/gems/3.2.0/gems/irb-1.7.4/exe/irb:9:in `<top (required)>' .../ruby/3.2.0/bin/irb:25:in `load' .../ruby/3.2.0/bin/irb:25:in `<main>'以下のように
StringキーをSymbolキーに対応付けると、パターンマッチングが可能になる。irb(main):005:0> json = {"key" => "value"}.with_indifferent_access => {"key"=>"value"} irb(main):006:0> json in {key: /value/} => true同PRより
つっつきボイス:「parsed_bodyに渡したJSONでwith_indifferent_accessが呼ばれるようになったことでin {key: /value/}のようなパターンマッチング構文が使えるようになったのか」「テストでパターンマッチング構文を使って複数のものとマッチするかどうかを調べたいことは割りとあるのでよさそう👍」
参考: Rails API parsed_body -- ActionDispatch::TestResponse
参考: § 11.8 ハッシュキーのシンボルと文字列を同様に扱う(indifferent access) -- Active Support コア拡張機能 - Railsガイド
なお、以下はプルリクしたseanpdoyleさんのメモ的なコメントです。
# actionpack/lib/action_dispatch/testing/request_encoder.rb#L55 register_encoder :html, response_parser: -> body { Rails::Dom::Testing.html_document.parse(body) } - register_encoder :json, response_parser: -> body { JSON.parse(body) } + register_encoder :json, response_parser: -> body { JSON.parse(body, object_class: ActiveSupport::HashWithIndifferentAccess) }これは潜在的に後方互換性のないbreaking changeになる可能性がある。
この変更はテストハーネスに対して行われたものであり、ホストアプリケーションの実装に影響しないため、どんな影響があるかが不明。
リスクがあるとすれば、
response.parsed_bodyの呼び出し側で、本来の意図を超えて柔軟に使われてしまう可能性があることだが、文字列キーとシンボルキーは相互変換可能なので、「現在」パスしているテストは引き続きパスするだろう。シンボルを使っている既存のテストスイートが今後変更されるときに、以前ならパスしなかった場合でもパスするようになると問題。
この変更の別案:
JSON.parseでビルドされたHashまたはArrayを新しいクラスにラップし、#deconstruct_keysを呼び出すときにのみHashにwith_indifferent_accessすることで、変更の影響をパターンマッチングのみに限定する。- 7.1に、
object_class: ActiveSupport::HashWithIndifferentAccessを使う新しいフレームワークのデフォルトを追加し、利用できない場合はobject_class: Hashを使うようにする。- この実装変更を取り消して、「Rubyのパターンマッチングを使うときは
.with_indifferent_accessか.deep_symbolize_keysをチェインすること」と呼び出し側に促す。
🔗 システムテストでPlaywrightをサポート
動機/背景
Playwrightは、新しいブラウザ自動化ツールとして人気が高まりつつある。コミュニティによってメンテナンスされているPlaywrightのRubyクライアントやcapybaraドライバもある。
自分がしばらく使ってみたところ、非常に優れたパフォーマンスを発揮しているので、Railsが対応できるとよいだろう。詳細
このプルリクは、PlaywrightドライバのサポートをRailsに追加する。
同PRより
つっつきボイス:「以前も取り上げたPlaywrightはWebのE2Eテストのフレームワークでしたね(ウォッチ20210803)」「使ったことなかったけど、Chromeのドライバとしては結構いいらしいですね」「EdgeやFirefoxやSafariでも使える、なるほど」「Playwrightを出しているのがMicrosoftだということを今回初めて知りました」「これまでもPlaywrightを使うことはもちろんできたけど、この改修でPlaywrightが公式にRailsでサポートされることになったのはいいですね👍」
参考: マイクロソフト、Webアプリテストの自動化サービス「Microsoft Playwright Testing」プレビューを開始 - Publickey
「プルリクで言及されているplaywright-ruby-clientを書いたのが、以下のスライドを作ったYusukeIwakiさんです↓」
🔗 config/application.rbにconfig.autoload_libが追加されるようになった
このパッチによって、新たに生成される
config/application.rbに以下が含まれるようになる。config.autoload_lib(ignore: %w(assets tasks))実用上は、新しい7.1アプリケーションでは
libに置かれているものをデフォルトでオートロードするようになることを意味する。無視するサブディレクトリのリストを動的に算出する方法や、何らかの形で抽象化する方法も検討したが、このパッチのコードのシンプルさと明瞭さの方が、私の好みにおいて勝っている。
同PRより
つっつきボイス:「Zeitwerkをメンテしているfxnさんのプルリクです」「Rails 7.1からはassetsとtasksのみを明示的にオートロードから除外する、つまりlibがオートロードされるようになる」
「libディレクトリに何を置くのかという問題はありますけどね」「それそれ、rakeタスクみたいにどこに置いたらいいのか迷うものってありますけど、それをlibに置いていいのかどうか考えちゃいますよね」「バッチファイルみたいなものはそれ用のサブディレクトリを作る方がよさそう」「ユーティリティクラスのような他にちょうどいい置き場所のないものが仕方なくlibに置かれがちだけど、本当はgemにするのが理想ではあるんですよね」「そう思います」
🔗 to_paramのデリミタをparam_delimiterで変更可能になった
このコミットにより、
to_keyが複数の値を返すときのデリミタ(区切り文字)をカスタマイズ可能になる。これにより、Active Recordでさまざまな種類の複合主キーをサポート可能になる。このプルリクには以下の2つのコミットが含まれている。
- Active Modelへの追加:
param_delimiter属性を追加して、Active Modelでデリミタを設定可能にする。- Active Record:
to_paramを微調整して、複合主キーをサポートし、新たに追加されたparam_delimiterとActive Recordのデフォルトのデリミタ(_)を定義する。
to_paramに複合主キーのサポートを追加するために、デリミタを抽象化せずに特定の値をハードコーディングすることは可能だが、複合主キーで可能な値をすべて考慮しつくすのは無理なので、競合を避けてデリミタを選ぶのは非常に難しい。また、アプリケーションに対してto_param全体をオーバーライドするよう求めるのも不便そう。自分はこの2つのコミットをsquashせずに分けておく方が断然いいと思うので、これらの変更を2つのプルリクに分けてsquashしないようにしてよいならなお嬉しい。
同PRより
つっつきボイス:「複合主キーなどでデリミタをデフォルトの"_"から変更したいときに、param_delimiterオプションでカンマやセミコロンなどに変えられるようになったんですね: 複合主キーの採番ルール(任意コードを明示的に付与していて、その中に _ が含まれる可能性があるなど)によってはデリミタを変更したい場合がありそう👍」「ちなみにUUIDではハイフン"-"が区切り文字ですね」
# activemodel/test/cases/conversion_test.rb#57
test "#to_param_delimiter allows redefining the delimiter used in #to_param" do
old_delimiter = Contact.param_delimiter
Contact.param_delimiter = "_"
assert_equal("abc_xyz", Contact.new(id: ["abc", "xyz"]).to_param)
ensure
Contact.param_delimiter = old_delimiter
end
test "#to_param_delimiter is defined per class" do
old_contact_delimiter = Contact.param_delimiter
custom_contract = Class.new(Contact)
Contact.param_delimiter = "_"
custom_contract.param_delimiter = ";"
assert_equal("abc_xyz", Contact.new(id: ["abc", "xyz"]).to_param)
assert_equal("abc;xyz", custom_contract.new(id: ["abc", "xyz"]).to_param)
ensure
Contact.param_delimiter = old_contact_delimiter
end
end
参考: UUID - Wikipedia
🔗 主キーが:idでない場合に主キーを返すread_attribute(:id)を非推奨化
主キーが
:idでない場合にread_attribute(:id)が主キーを返すのを非推奨化する。Rails 7.2以後は、
read_attribute(:id)はモデルの主キーに関係なくidカラムの値を返すようになる。主キーの値を取得するには、代わりに#idを使うこと。複合主キーモデルでは、read_attribute(:id)は今後もidカラムの値を返すようになる。Adrianna Chang
同Changelogより
動機/背景
このプルリクは、モデルの主キーがidカラムでない場合に、
read_attribute(:id)が主キーを返すことを非推奨化する。Rails 7.2以後は、read_attribute(:id)はidカラムの値を返すようになる。このコミットによって、複合主キーモデルの
read_attribute(:id)も複合主キーではなくidカラムの値を返すように変更される。
read_attributeは、"id"を「主キー」という意味に翻訳すべきではない。このメソッドは、アプリケーションがモデルのid属性に容易にアクセス可能にするため、モデルの主キーであるかどうかに関係なく生の属性値を返すべき。詳細
このプルリクは、
read_attributeで:idを読み取っているが、idが主キーでない場合に非推奨警告を表示する。さらに、複合主キーモデルではidカラムの値を返すように振る舞いを変更する。ただし、これによりcpk_record["id"]も複合主キーではなくid カラムを返すようになる点に注意すること。私は代わりに
#idを使うことをお勧めしたい。主キーを返す#idを廃止しようという他の議論(例: #48930コメント)もあったが、そのためには複合主キーの値を取得するゲッターメソッドを新たに導入しなければならないだろう。現状では、そういう変更は当分先の話となる😄同PRより
つっつきボイス:「read_attribute(:id)が今まで主キー(primary key)を返していたのを、今後はidカラムの値を返すようになるのか」「これも複合主キーに関連しているんですね」「7.1でリリースされる予定の複合主キーも、この改修でread_attribute(:id)で主キーではなくidを返すようになった: 複合主キーでは主キーを取得すると配列が返されるので、たしかにread_attribute(:id)の方は常にidというカラム名の値を返すのがいいでしょうね」「主キーがidとは限らない問題、ややこしい」
参考: Rails API read_attribute -- ActiveRecord::AttributeMethods::Read
🔗 モデルのid属性をid_valueで取得できるようになった
レコードのidカラムの生の値にアクセスするための
ActiveRecord::Base#id_valueエイリアスを追加する。このエイリアスは、
:idカラムを宣言するモデルでのみ提供される。Adrianna Chang
同Changelogより
動機/背景
#48533を拡張する。
これにより、idカラムが存在するが主キーではないレコードで、生の
idカラム値にアクセス可能になる(これは複合主キーを持つモデルで一般的)。詳細
alias_attributeを用いて、uidをid属性のエイリアスとして提供する。これは上記のプルリクで説明されている主要なユースケースの1つなので、Railsでデフォルトで提供することにした。追加情報
#uidのAPIドキュメントを提供するのに、alias_attribute呼び出しの上にコメントを挿入するよりもよい方法はあるだろうか?同PRより
つっつきボイス:「これも同じAdrianna Changさんのプルリク」「idカラムの値をid_valueという分かりやすいエイリアスメソッドで取れるようにしたんですね」「最初はエイリアスをuidにしようとしてたみたいだけど、レビューの結果id_valueにしたのか」
🔗 メーラーのプレビュー表示をアルファベット順にした
動機/背景
メーラーのプレビューページに表示されるメーラーは、
descendants配列内で見つかった順に表示されるので、アルファベット順になるかどうかが不確定になる。アプリケーション内で定義されているメーラーが増えるにつれて、この直感的でない並び順のために特定のメーラーをプレビューページ上で見つけにくくなる。このプルリクは、メーラープレビューページでメーラーのリストを常にアルファベット順(単語の冒頭を大文字にした名前)で返すようにし、リストで特定のメーラーを見つけやすくする。
なお、あるメーラーに複数のメソッドがある場合、それらのメソッドは既に
emailsメソッドによってソート済みの形で返されることに注意。同PRより
つっつきボイス:「これはわかりやすい改修」「ソートがアルファベット順じゃないとプレビューが増えたときに探すのが大変ですよね」
参考: Action Mailer の基礎 - Railsガイド
つっつき後に、Active Supportのdescendantsが返す結果の並び順は継承順に左右されると教わりました。
参考: §4.2.2 descendants -- Active Support コア拡張機能 - Railsガイド
🔗 production環境のRailsコンソールを常にsandboxモードにするコンフィグが追加
Railsコンソールをデフォルトでsandboxモードで起動するオプションを追加
デフォルトでRailsコンソールをsandboxモードで起動するために、
sandbox_by_defaultオプションが追加された。このオプションを有効にすると、sandboxモードをオフにしてRailsコンソールを起動するには--no-sandboxを指定しなければならなくなる。このオプションは、Railsのdevelopment環境とtest環境では無視されることに注意。
Shouichi Kamiya
同Changelogより
productionデータベースに誤って書き込んでしまうのを防ぐために、自分は常にRailsコンソールをsandboxモードで起動している。productionデータベースに書き込みたい場合を除いて、sandboxモードをオフにしてRailsコンソールを起動することはない。
Railsコンソールをデフォルトでsandboxモードで起動するために、
sandbox_by_defaultオプションを追加した。このオプションを有効にすると、sandboxモードをオフにしてRailsコンソールを起動するには--no-sandboxを指定しなければならない。このオプションは、Railsのdevelopment環境とtest環境では無視される。
同PRより
つっつきボイス:「config.sandbox_by_defaultオプションをtrueにすると、--no-sandboxを付けない限りproductionのRailsコンソールが常にsandboxモードになるのか」「デフォルトはfalseなんですね」「うっかりsandboxモードにし忘れて本番データを壊したりしないようにできるので、Railsコンソールをよく使うプロジェクトで便利そう👍」
🔗 X_FORWARDED_HOSTの値がヘッダーに表示される可能性があるバグを修正
- PR: Fix host display when X_FORWARDED_HOST authorized by skipkayhil · Pull Request #48941 · rails/rails
HTTP_HOSTがブロックされている場合に、HostAuthorizationがX_FORWARDED_HOSTの値をヘッダーで表示する可能性がある問題を修正。Hartley McGuire, Daniel Schlosser
同Changelogより
動機/背景
従来は、認可されていない
HTTP_HOSTの値がリクエストのヘッダーにある場合でも、認可済みのHTTP_X_FORWARDED_HOSTの値がヘッダーに表示されていた。しかし、この値が既にconfig.hostsに追加されている場合にユーザーが混乱する可能性がある。詳細
このコミットは、ブロックされたホストの表示方法を微調整する形で問題を解決する。
Request#hostを常に表示する(X_FORWARDED_HOSTが存在すると、ホストがブロックされているかどうかにかかわらず常にX_FORWARDED_HOSTを返してしまう)のではなく、ブロックされるホストを個別に表示するようになる。追加情報
cc @Eusebius1920: 前のプルリクで共に作業したので共著に加えた。
同PRより
つっつきボイス:「config.hostsの設定がHostAuthorizationのHTTP_X_FORWARDED_HOSTによるX-Forwarded-Hostヘッダー表示に正しく反映されていなかったのを修正した: ちなみにこのHostAuthorizationはRackミドルウェアですね」「あ、たしかに」「X-Forwarded-Hostヘッダーはログに出ることが多いんですが、これとconfig.hostsの設定および遮断の振る舞いが整合していないと混乱しそう」「X-Forwarded-Hostはただでさえプロキシが多段になると一気にややこしくなりがちですよね」
参考: §3.5.1 ActionDispatch::HostAuthorization -- Rails アプリケーションを設定する - Railsガイド
参考: X-Forwarded-Host - HTTP | MDN
前編は以上です。
バックナンバー(2023年度第3四半期)
- 20230829前編 Active Storageのミラーアップロードが非同期に、Rackアプリを手作りほか
- 20230824後編 ArelでCAST関数サポート、webdrivers依存を解消、YJIT高速化ほか
- 20230823前編 Rails 7.0.7に含まれているRails 7.0.6のバグ修正ほか
- 20230809 Rails 7.0.5のcreate_association挙動変更取り消し、YJITの性能を最大限引き出す方法ほか
- 20230803後編 Railsフラグメントキャッシュ経由の情報漏洩に注意ほか
- 20230802前編 Active Storageバリアントの事前変換、Linkヘッダープリロードのオプトアウトほか
- 20230727後編 Rubyにdefp導入の提案、IRB 1.7.3リリースほか
- 20230725前編 config.autoload_libとconfig.autoload_lib_onceが追加ほか
- 20230721後編 Kaigi on Rails 2023プロポーザル募集、rubocop-magic_numbersほか
- 20230719前編 複合主キー関連の実装進む、Action TextでHTML5サニタイザほか
- 20230705後編 AWS LambdaでRailsをRackで動かすLambyほか
- 20230704前編 productionのforce_ssl=trueがデフォルトで有効に、rakeタスクをthorで書くほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)