- 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ウォッチタグ)