Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Ruby 3.2.0 Preview 2とRack 3.0リリース、packwerkでアプリコードの境界を強制ほか(20220920)

こんにちは、hachi8833です。RubyKaigi 2022お疲れさまでした。来年5月は長野県松本市ですね。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

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

🔗Rails: 先週の改修(Rails公式ニュースより)

🔗 Ruby 3.1のerror_highlight機能でエラー発生位置が詳しく表示されるようになった

概要
このプルリクは、Ruby 3.1から利用可能になったerror_highlightを用いて、エラー発生位置を詳しく表示する。対象コード片の行番号だけではなく発生位置の範囲もハイライトされる。
以下はasubasuubとスペルミスした場合の例。

この機能は1行の中にメソッドチェインがある場合にさらに有用。[]のメソッドチェイン内でundefined method '[]' for nil:NilClassエラーが発生したときに、どの[]でエラーが発生したかがひと目で分かる。以下は:articles:articleとスペルミスした場合の例。

その他情報
このプルリクには以下の2つのステップがある。

  • Exception#backtraceよりもException#backtrace_locationsが望ましい(error_highlightが位置情報を特定するのにException#backtrace_locationsが必要)。
  • エラーが発生した位置を特定するのにErrorHighlight.spotを用いている。

error_highlightについて詳しくはFeature #17930: Add column information into error backtraceを参照。
同PRより


つっつきボイス:「この間取り上げたRubyのerror_highlightがRailsのエラー画面に取り入れられました🎉(ウォッチ20220906)」「RubyKaigi 2022でも話されていたトピックですね」

以下はつっつき後に見つけた会期中のツイートです。

🔗 routes --grepでパスにマッチするルートをフィルタできるようになる

概要
/users/orhantoy/settingsのようなパスを見ただけでは、どのコントローラとアクションがこのルートに対応するかがわからないこともある。そこで、パスを渡したらどのコントローラとアクションにマッチするかを調べられたら便利だろうと考えた。これは実はブラウザでhttp://localhost:3000/rails/info/routesを開けば調べられることがわかった(知らなかった!)が、rails routesコマンドでも同じ機能が使えると便利だろうと思った。


#45874 (comment)以後の改修では以下のようになる(元の改修では別途--pathオプションを導入していた)

$ bin/rails routes -g /cats/1
Prefix Verb   URI Pattern         Controller#Action
   cat GET    /cats/:id(.:format) cats#show
       PATCH  /cats/:id(.:format) cats#update
       PUT    /cats/:id(.:format) cats#update
       DELETE /cats/:id(.:format) cats#destroy

同PRより


つっつきボイス:「rails routesの結果をパイプでgrepにつなぐとかは普段からやっているけど、この-gオプションはたとえば/cats/1を渡すと/cats/:idで絞り込んでくれるのがいいですね👍」「インテリジェントに絞り込めるんですね」

参考: §6.1 既存のルールを一覧表示する -- Rails のルーティング - Railsガイド

🔗 304 Not ModifiedレスポンスでCSPヘッダーを返さないよう修正

概要
CVE-2022-22577の修正後のRailsは、すべてのレスポンスでCSPヘッダーを送信するようになり、レスポンスにHTMLが含まれていない場合も送信していた。
しかしこれが有害となるケースが1つある。HTMLを含まない304 Not Modifiedを返すとブラウザはCSPヘッダーを更新するが、それ以外の場合はキャッシュ済みHTMLを再利用する。このHTMLにnonceを持つscriptタグが含まれていると、このnonceがCSPヘッダーの新しいnonceとマッチしなくなる可能性がある。

これは、request.session.id.to_sをnonceジェネレータとして使っていれば通常は問題にならないが、古いSecureRandom.hex(16)のバリアントを使っている人にとっては引き続き問題となる点に注意。
このプルリクは、304でCSPヘッダーの返送をスキップするというシンプルな修正。ブラウザは元のレスポンスのCSPヘッダーを使い続けるので、これで問題ないはず。
同PRより


w3c/webappsec-csp#161でブラウザの振る舞いがバグではないことが確認できたので、この修正でよいと思う👍
同PRコメントより


つっつきボイス:「CVE-2022-22577は今年4月のセキュリティリリースで修正されていました↓」「#44635ではJSON APIなどのHTML以外のレスポンスを返す場合に一律にCSPヘッダーを返すよう修正されたけど、現在のブラウザの振る舞いを考慮すると304 Not Modifiedのnonceで問題が生じる可能性があるので、304ではCSPヘッダーを返すべきではないと判断されたようですね」

Railsセキュリティ修正7.0.2.4、6.1.5.1、6.0.4.8、5.2.7.1がリリースされました

参考: CSP: script-src - HTTP | MDN

🔗 ActiveRecord::Persistence#becomesをvirtual attributeに適応させる

ソースクラスとターゲットクラスにある属性のセットが異なる場合に、ターゲットに余分にある属性が追加されるように属性を適応させる。

class Person < ApplicationRecord
end

class User < Person
  attribute :is_admin, :boolean
  after_initialize :set_admin

  def set_admin
    write_attribute(:is_admin, email =~ /@ourcompany\.com$/)
  end
end

person = Person.find_by(email: "email@ourcompany.com")

person.respond_to? :is_admin
# => false
person.becomes(User).is_admin?
# => true

Jacopo Beschi, Sampson Crowley
同Changelogより


つっつきボイス:「上のサンプルコードでPersonモデルをUserbecomesしたときに、子のPersonにない親モデルのis_admin属性にもUserが応答するように改修したんですね」「改修後の挙動で若干副作用がありそうな気もするけど、becomesの使われ方を考えると影響はそれほどなさそうかな」「なるほど」「becomesっていうメソッドは前からActive Recordにありますね↓」「そうそう、あります」

参考: Rails API becomes -- ActiveRecord::Persistence

becomesメソッドはSTIで必要になるときがあります」「あ、たしかに」「自分はそういうときにキャストしたりしましたけど、becomesメソッドはRubyの機能によるクラスキャストではなくActive Recordにより適した形でキャストしてくれるんでしょうね」「becomesはSTIを使ったことがないと知らない人も多いかも」

参考: §シングルテーブル継承 (STI) -- Active Record の関連付け - Railsガイド


指定されたklassのインスタンスに現在のレコードの属性を含めたものを返す。becomesが最も有用なのは、シングルテーブル継承(STI)構造に関連して、あるサブクラスをスーパークラスのように見せたい場合。becomesをAction Packのレコードidと併用すると次のようなことができる。たとえばClient < Companyのときに@client.becomes(Company)とすると、パーシャルのレンダリングでclients/clientパーシャルの代わりにcompanies/companyパーシャルを用いてそのインスタンスをレンダリングできるようになる。

注: 新しいインスタンスは元のクラスと同じ属性へのリンクを共有するので、STIカラムの値も同じになる。いずれかのインスタンス属性を変更すると両方のインスタンスに影響する。STIカラムも変更したい場合は、代わりにbecomes!を使う。
Rails API becomes -- ActiveRecord::Persistenceより

🔗Rails

🔗 RailsのAttributes APIでステートレスなフォームを作る(Ruby Weeklyより)


つっつきボイス:「Active Recordを使わないステートレスなフォームをAttributes API↓で作るのは、Railsでよく行われますね」

Rails: ActiveRecord標準のattributes APIドキュメント(翻訳)

「以下は検索のフォームをActiveModel::Attributesでやっている↓」「Active Modelをincludeすれば例のform_withでRailsらしく書けて便利」「Railsに慣れている人なら普通に使っている方法ですね」

# 同記事より
class Filter
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :available_on, :date
end
<-- 同記事より -->
<%= form_with model: @filter, url: items_path, method: :get do |f| %>

Rails 5.1〜7.0: ‘form_with’ APIドキュメント(翻訳)

「この記事ではカスタム型も追加していますね: そのままではActive Modelに新しい型を追加できないので、以下のように書いてActiveModel::Type.register(:cents, Cents)とすることで使えるようになる↓」「ActiveModel::Type.registerという書き方ができるって知らなかった」

# 同記事より
class Cents < ActiveRecord::Type::Integer
  def cast(value)
    return super if value.is_a?(Numeric)

    price_in_dollars = value.to_s.delete("$").to_d
    price_in_cents = (price_in_dollars * 100).to_i
    super(price_in_cents)
  end
end

参考: register -- ActiveModel::Type

registerしておくと、attribute :min_price, :float, default: 0.00のように型をシンボルで:floatのように指定できるようになる↓」「なるほど」

# 同記事より
module Criteria
  def self.type_for(name)
    all.find { |criteria_type|
      criteria_type.type_name.to_s == name.to_s
    }
  end

  def self.all
    [
      Price,
      AvailableOn,
      Colors
    ]
  end

  class Base
    include ActiveModel::Model
    include ActiveModel::Attributes

    attribute :id, :integer
  end

  class Price < Base
    attribute :min_price, :float, default: 0.00
    attribute :max_price, :float, default: 100.00

    def self.type_name
      :price
    end
  end

  class AvailableOn < Base
    attribute :date, :date, default: Time.zone.today

    def self.type_name
      :available_on
    end
  end

  class Colors < Base
    attribute :colors, array: true, default: ["royal_blue"]

    def self.type_name
      :colors
    end

    def self.supported_colors
      [
        ["blue", "Blue"],
        ["royal_blue", "Royal Blue"],
        ["navy_blue", "Navy Blue"],
        ["raspberry_blue", "Blue Raspberry"]
      ]
    end

    def colors=(colors)
      super(colors.select(&:present?))
    end
  end
end

「この記事のようにここまでみっちり書くかどうかは状況次第かなとは思いますが、とてもRailsらしい書き方なので知っておくとよいと思います👍」「ところで、ガイドにはActiveModel::AttributeMethodsのことは書かれているけどカスタム型やregisterの話までは書かれていないみたいですね↓」「Attributes APIドキュメントの翻訳にありました↓」

参考: Active Model の基礎 - Railsガイド

Rails: ActiveRecord標準のattributes APIドキュメント(翻訳)

🔗 GibLab CI/CDキャッシュのビジュアル解説ガイド


つっつきボイス:「"cache vs artifacts"はビルドエンジンを使うときなどによく見かける言い回し」「artifactは"成果物"みたいなニュアンスで使われますね」「そうそう、ビルドで生成されたものをbuild artifactsと言ったりしますね」

「ジョブが直前のジョブの結果に依存していない場合はキャッシュを使い、依存する場合はartifactsと依存関係を使う、なるほど」「記事はGitLab CIのキャッシュに関する解説だけど、GitHub Actionsのキャッシュと似たような手法も載っていて参考になりそう👍」


同記事より

actions/cache - GitHub

🔗 packwerk: Railsアプリケーション内の境界を定めてモジュール化

Shopify/packwerk - GitHub


つっつきボイス:「TechRacho翻訳記事でもお世話になっているNate Berkopecさんが、型付けよりもShopifyのpackwerkの方が好きだと書いていたのが気になって拾いました」「Railsのコードが名前空間を超えられないように境界を敷くgemのようですね↓」「なるほど、コードの越境を防ぐんですね」「似たようなgemが他にもあったかも」「あった気がするけど思い出せない...」

  • ファイルをグループ化してパッケージにまとめる
  • パッケージレベルの定数可視性を定義する(publicアクセス可能な定数を持たせる)
  • パッケージ間のプライバシー(inbound)や依存関係(outbound)の境界を強制する
  • 開発を妨げずに既存コードベースのモジュール化を支援する
    同リポジトリより

「packwerkはzeitwerkに依存しているそうなのでパックベルクと読むのかなと想像しました」「zeitwerkに依存するということはローダーレベルで境界をチェックしているのかも🤔」

「ドキュメントはUSAGE.mdに載っていますね」「なお、この概念図↓はパッケージ同士が接するpublic APIはなるべく少なくて疎結合なのが望ましいというよくある説明ですが、ここでむしろ重要なのは1個のパッケージ内部は密結合していても構わないという点を理解しておくことですね」「たしかに」


同リポジトリより

「packwerkはpackage.ymlファイルで境界を定義するのか、なるほど↓」「enforce_privacyenforce_dependenciesみたいな指定もできるんですね」

# 同リポジトリより
    # components/sales/package.yml
    metadata:
      stewards:
      - "@Shopify/sales"
      slack_channels:
      - "#sales"

「packwerkでRailsエンジンの越境を防ぐこともできるでしょうか?」「Sidekiqのジョブ画面あたりなら問題なくできそうですけど、admin専用の管理画面のようにRailsエンジン自体がマウントする側に依存していたりすると、そのままだと動かないかも」「あ、そうか」「packwerkを使えるかどうかは状況や用途にもよるでしょうね」

Railsエンジンは使いすぎに注意(翻訳)

「packwerkを眺めた限りでは事前定義されているクラスが対象のようなので、動的に定義されるクラスまではチェックできなさそうかな?」「あ、そうかも」「越境を実行時に警告する機能があればそうした部分もCIでカバーできますけど、見た限りではまだなさそうですね」

「メンバーの出入りが多い大規模なプロジェクトでは何らかの形でこうやって制約を与えられるツールが欲しくなるので、時間があるときに調べてみよう👍」「CIでチェックできるとよさそうですね」「この種のツールを後から導入するとつらくなりそうなので、可能ならプロジェクト立ち上げ時に導入したい」「ですよね」

🔗 graphql-rubyのvalidate_max_errors


つっつきボイス:「ruby-jp Slackで見かけたんですが、graphql-rubyでDoS攻撃防止のためにvalidate_max_errorsが追加されていました」「たしかにGraphQLで入れ子が深くなったときのバリデーションエラーが大量に出力されるのは十分ありえますね: 大事な修正👍」

# lib/generators/graphql/templates/schema.erb#L26

+ # Stop validating when it encounters this many errors:
+  validate_max_errors(100)
end
<% end -%>

参考: Method: GraphQL::Schema.validate_max_errors — Documentation for rmosolgo/graphql-ruby (master)

🔗Ruby

🔗 Ruby 3.2.0 Preview 2(Ruby公式ニュースより)


つっつきボイス:「RubyKaigi 2022の会期中に3.2.0 Preview 2がリリースされました🎉」「Preview 1のリリースノートにも書かれていますが、WASIベースのWebAssemblyサポートがRubyのビルドに含まれるようになったんですね、凄い」「そういえばRubyKaigi 2022のキーノートスピーチでも言及していました」

「breaking changeも少し入ってますね: double splat **を含むprocの扱いが修正された↓」「あまり使わない書き方だと思うけど」「私は使ってます😆」「3.2では注意しないといけませんね」

# 同リリースノートより
proc{|a, **k| a}.call([1, 2])
# Ruby 3.1 and before
# => 1
# Ruby 3.2 and after
# => [1, 2]

参考: Bug #18633: proc { |a, **kw| a } autosplats and treats empty kwargs specially - Ruby master - Ruby Issue Tracking System

「以下のような定数への代入が左から右に評価されていなかった↓というバグの修正は、Jeremy EvansさんがRubyKaigi 2022のDay 3で発表していました」「多重代入で複雑な渡し方をしたときの挙動はやってみないとわからないところがありますよね」

# 同リリースノートより
foo1::BAR1, foo2::BAR2 = baz1, baz2

参考: Bug #15928: Constant declaration does not conform to JIS 3017:2013 - Ruby master - Ruby Issue Tracking System
参考: Fix evaluation of order of constant assignment by jeremyevans · Pull Request #4450 · ruby/ruby

🔗 Rack 3.0リリース(Ruby Weeklyより)

rack/rack - GitHub


つっつきボイス:「Rack 3.0も少し前にリリースされました🎉」「ちょうど今日gem install rackを実行したら知らないバージョンのRackがインストールされたんですが、よく見たら3.0でしたね」「3.0.0.beta1の時点でbreaking changeが少し入っている...」「メジャーバージョンアップだとbreaking changeがあってもおかしくないでしょうね」「ミドルウェア系のgemの中にはRackを直接使っているものもあるので、3.0にアップグレードする前に念のためチェックが必要かな」

🔗 書籍『why's (poignant) Guide to Ruby』


つっつきボイス:「数か月前に知り合ったロンドン在住のRuby開発者の方とRubyKaigiの場でお会いしたときに、以下と同じ書籍をお土産にいただきました🙇」「これは知らない本...」「私もいただくまで知らなかったんですが、_whyさんという方がかなり昔に書いたRubyに関する読み物が最近表紙を改めて書籍化されたらしくて、調べているうちにプロ翻訳者の青木靖さんによる日本語訳をネット上で見つけたのが上のリンクです」

「Rubyを知らない人に向けた、異色のRuby読み物という感じですね❤️」「長くて自分はまだ読み終えていませんが、日本語版は翻訳のクォリティが本当に見事でした」

参考: why's (poignant) Guide to Ruby - Wikipedia
参考: 「だから、作れ」と_whyは言った:Rails Hub情報局:エンジニアライフ


今週は以上です。

バックナンバー(2022年度第3四半期)

週刊Railsウォッチ: syntax_suggestがRuby標準ライブラリに追加、RubyのVisitorパターンほか(20220906後編)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。