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

週刊Railsウォッチ: Turbo 8リリース、Railsドキュメント改善プロジェクトほか(20240215)

こんにちは、hachi8833です。技術評論社のRuby 3.3記事、まだまだありました🙇。

週刊Railsウォッチについて

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

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

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

今回は小さめのバグ修正が中心です。

🔗 ActiveRecord::DelegatedType<role>_typesクラスメソッドが追加

DelegatedTypeのイントロスペクションを可能にするため、<role>_typesクラスメソッドを ActiveRecord::DelegatedTypeに追加する。

JP Rosevear
同Changelogより

動機/背景

APIリクエストのstrong parametersを処理中に、やや抽象的な方法でDelegatedTypeのポリモーフィックな型フィールドをバリデーションする場合、許可されている型にアクセスできると便利。

詳細

このプルリクは、ActiveRecord::DelegatedTypeで定義されるメソッドを変更する。
同PRより


つっつきボイス:「DelegatedType<role>_typesというクラスメソッドが生えてくるようになったそうです」「従来の<role>_class<role>_nameとかはdefine_methodでインスタンスメソッドを生やすようになっているけど、今回の<role>_typesdefine_singleton_methodでクラスメソッドとして生えてきて、そのクラスのtypesのリストを返してくれるんですね: DelegatedType自体はモジュールなので、これをincludeextendするとそこで<role>_typesがクラスメソッドになるということみたい」

# activerecord/lib/active_record/delegated_type.rb#L216
    private
      def define_delegated_type_methods(role, types:, options:)
        primary_key = options[:primary_key] || "id"
        role_type = options[:foreign_type] || "#{role}_type"
        role_id   = options[:foreign_key] || "#{role}_id"

+       define_singleton_method "#{role}_types" do
+         types.map(&:to_s)
+       end
+
        define_method "#{role}_class" do
          public_send(role_type).constantize
        end
        define_method "#{role}_name" do
          public_send("#{role}_class").model_name.singular.inquiry
        end
        define_method "build_#{role}" do |*params|
          public_send("#{role}=", public_send("#{role}_class").new(*params))
        end
        ...

参考: Object#define_singleton_method (Ruby 3.3 リファレンスマニュアル)
参考: Module#define_method (Ruby 3.3 リファレンスマニュアル)

DelegatedTypeってほとんど使ってなかったけど、ドキュメント↓を見た感じではなかなかいい子のようですね: 今回の<role>_typesも含めて使ってみようかな👍」

Rails: ActiveRecord::DelegatedType APIドキュメント(翻訳)

「ところでイントロスペクション(introspection)ってリフレクションプログラミングの文脈でよく出てきますけど、今ひとつよくわからないです😅」

後で調べてみました:

コンピューターサイエンスにおけるリフレクティブプログラミング(リフレクション)とは、プロセスが自身の構造と動作について検査やイントロスペクションを行い、変更する能力のこと。
Reflective programming - Wikipediaより


コンピューティングにおける型イントロスペクションとは、実行時にオブジェクトの型やプロパティを検査するプログラムの機能。一部のプログラミング言語がこの機能を有している。
Type introspection - Wikipediaより

🔗 schema_dumpなどをDATABASE_URLでコンフィグ可能になった

以下をDATABASE_URLでコンフィグ可能にする。

  • schema_dump
  • query_cache
  • replica
  • database_tasks

従来は、ブーリアン値が文字列として解釈されてできないことがあった。

改修後は、DATABASE_URL=postgres://localhost/foo?schema_dump=falseのように書くことでスキーマキャッシュのダンプを正しく無効にできるようになった。

Mike CoutermarshJean Boussier
同Changelogより


つっつきボイス:「DATABASE_URL?schema_dump=falseのようなコンフィグパラメータを追加できるようになったんですね」「to_boolean!っていうprivateメソッドが追加されてる」「ちょっとややこしいけど、文字列で入ってくる"true""false"を検査して、その結果でハッシュconfiguration_hash[key]を破壊的に更新しているので、メソッド名に!がついているのか、なるほど」

# activerecord/lib/active_record/database_configurations/url_config.rb#L43
        @url = url
-       @configuration_hash = @configuration_hash.merge(build_url_hash).freeze
+       @configuration_hash = @configuration_hash.merge(build_url_hash)
+
+       if @configuration_hash[:schema_dump] == "false"
+         @configuration_hash[:schema_dump] = false
+       end
+
+       if @configuration_hash[:query_cache] == "false"
+         @configuration_hash[:query_cache] = false
+       end
+
+       to_boolean!(@configuration_hash, :replica)
+       to_boolean!(@configuration_hash, :database_tasks)
+
+       @configuration_hash.freeze
      end

      private
+       def to_boolean!(configuration_hash, key)
+         if configuration_hash[key].is_a?(String)
+           configuration_hash[key] = configuration_hash[key] != "false"
+         end
+       end
+

🔗 ActiveSupport::MessagePackIPAddrをシリアライズするとprefixが含まれていなかったのを修正

ActiveSupport::MessagePackシリアライザでIPAddrをシリアライズしたときにIPAddr#prefixが含まれるようにした。この変更には下位互換性と上位互換性があり、古いペイロードは引き続き読み取り可能になり、新しいペイロードは古いバージョンのRailsでも読み取り可能になる。

Taiki Komaba
同Changelogより

動機/背景

このプルリクを作成した理由は、ダンプ時にmsgpackシリアライザでIPAddrオブジェクトのプレフィックス(ネットマスク)データが欠落していることに気付いたため。

詳細

このプルリクは、IPAddrmsgpackシリアライザでダンプしたときにプレフィックス(ネットマスク)データが失われないよう修正する。

追加情報

to_sにはプレフィックス(ネットマスク)データが含まれない

 IPAddr.new('192.168.0.1/24').to_s
=> "192.168.0.0"

このため、msgpackシリアライザでダンプ/読み込みを行ったときにto_sにはプレフィックス(ネットマスク)データが含まれない。

irb(main):032> serializer = ActiveSupport::MessagePack::CacheSerializer
irb(main):033> serializer.load(serializer.dump(IPAddr.new('192.168.0.1/24')))
=> #<IPAddr: IPv4:192.168.0.0/255.255.255.255>

IPAddrオブジェクトのアサーション

IPAddrインスタンスの==アサーションは、同じデータのアサーションが正確ではない。このためテストではinspectを使った。

IPAddr.new('192.168.0.1/24') == IPAddr.new('192.168.0.0/32')
=> true
irb(main):027> IPAddr.new('192.168.0.1/24').inspect == IPAddr.new('192.168.0.0').inspect
=> false

同PRより


つっつきボイス:「これはバグ修正ですね」「プレフィックスって何だろうと思ったら、IPアドレスのサブネットマスクを/24とか/25とかで表すヤツを指してそう言っているのね」「==でアサーションするとサブネットマスクが違っていてもtrueになるのでinspectでアサーションしたんですって」「これは残念」「今さら変えられなさそう...」

参考: サブネットマスク - Wikipedia

🔗 デフォルトの読み込みパスにファイルが含まれないよう修正

Railsのデフォルトの読み込みパスにディレクトリ以外のものが存在しないよう修正。

従来はappディレクトリにある一部のファイルが読み込みパスを汚染していた。
このコミットは、Railsフレームワークで設定されるデフォルトの読み込みパスからファイルを削除する。

これで、以下のパスにデフォルトでディレクトリだけが含まれるようになった。

  • autoload_paths
  • autoload_once_paths
  • eager_load_paths
  • load_paths

Takumasa Ochi
同Changelogより


つっつきボイス:「これもバグ修正です」「読み込みパスにディレクトリの他にファイルが混じることがあったとは」「existentexistent_directoriesは前から同じPathsモジュールにあるんですね」

# railties/lib/rails/paths.rb#L105
    private
      def filter_by(&block)
        all_paths.find_all(&block).flat_map { |path|
-         paths = path.existent
-         paths - path.children.flat_map { |p| yield(p) ? [] : p.existent }
+         paths = path.existent_directories
+         paths - path.children.flat_map { |p| yield(p) ? [] : p.existent_directories }
+       }.uniq
      end
# railties/lib/rails/paths.rb#L208
      def existent
        expanded.select do |f|
          does_exist = File.exist?(f)

          if !does_exist && File.symlink?(f)
            raise "File #{f.inspect} is a symlink that does not point to a valid file"
          end
          does_exist
        end
      end

      def existent_directories
        expanded.select { |d| File.directory?(d) }
      end

🔗 Active StorageでHDR動画のrotationを取得できない場合があったのを修正

動機/背景

修正: #50853

詳細

動画アナライザのrotationメタデータの取得は、side_dataの位置参照に依存していた。しかしside_dataの位置がすべての動画で同じであることは保証されていない。たとえば、iOSでポートレートモードで撮影したHDR動画では、"DOVI configuration record" side_dataが最初の位置にあり、それに続いてrotation値を含む"Display matrix" side_dataが置かれる。

この修正によって位置参照が削除され、"Display matrix" side_dataを明示的に探索してrotation値を取得できるようになる。
同PRより


つっつきボイス:「位置参照って言っているのはside_data[0]のことみたい」「位置をゼロで決め打ちしていると値を取り出せない場合があったので、Display Matrixという名前で取ってから"rotation"で取るようにしたのね」「これもバグ修正ですね」

# activestorage/lib/active_storage/analyzer/video_analyzer.rb#L55
   def angle
        if tags["rotate"]
          Integer(tags["rotate"])
-       elsif side_data && side_data[0] && side_data[0]["rotation"]
-         Integer(side_data[0]["rotation"])
+       elsif display_matrix && display_matrix["rotation"]
+         Integer(display_matrix["rotation"])
        end
      end

+     def display_matrix
+       side_data.detect { |data| data["side_data_type"] == "Display Matrix" }
+     end

🔗 NestedAttributesで、渡した引数の種類によってエラーメッセージが異なっていたのを修正

ネステッド属性関連付けライター(writer)に無効な引数を渡すと、常にArgumentErrorが発生するよう修正。

従来、このエラーはコレクション(複数形)の関連付けでしか発生せず、単数形の関連付けでは一般的なエラーを発生していた。

修正後は、コレクションと単数形の両方の関連付けでArgumentErrorが発生するようになった。

Joshua Young
同Changelogより


つっつきボイス:「Active RecordのNestedAttributesで、渡した引数が複数形か単数形でエラーの種類が違ってしまっていたので、どちらも同じエラーになるように修正したんですね」「options =の行を下に移動してから、引数がHashかどうかのチェックを追加している↓」「メソッド名が長いけど、これは1対1関連付け用なのね」

# activerecord/lib/active_record/nested_attributes.rb#L423
      def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
-       options = nested_attributes_options[association_name]
        if attributes.respond_to?(:permitted?)
          attributes = attributes.to_h
        end

+       unless attributes.is_a?(Hash)
+         raise ArgumentError, "Hash expected for `#{association_name}` attributes, got #{attributes.class.name}"
+       end
+
+       options = nested_attributes_options[association_name]
        attributes = attributes.with_indifferent_access
        existing_record = send(association_name)

「こっちのコレクション用メソッドはメッセージを複数形用に修正している↓」

# activerecord/lib/active_record/nested_attributes.rb#L487
      def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
        options = nested_attributes_options[association_name]
        if attributes_collection.respond_to?(:permitted?)
          attributes_collection = attributes_collection.to_h
        end

        unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
-         raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
+         raise ArgumentError, "Hash or Array expected for `#{association_name}` attributes, got #{attributes_collection.class.name}"
        end

Rails API: ActiveRecord::NestedAttributes(翻訳)

🔗Rails

🔗 Rails Foundationによるドキュメント改良プロジェクト(Rails公式ニュースより)


つっつきボイス:「Rails Foundationが、新しい取り組みとしてRailsのドキュメントをがっつり改良するプロジェクトを始めたそうです」「お〜、rails foundationタグがついているタグは、監査->執筆&チームレビュー->プルリク&コミュニティレビューという流れでドキュメントを改善していくんですね🎉」「見た感じではRailsガイドが改良の対象みたい↓」「今のところ3つか」

参考: Issues · rails/rails


参考: [RF DOCS] Add documentation for perform_all_later to Active Job Basics guide [ci-skip] by bhumi1102 · Pull Request #51004 · rails/rails
参考: [RF DOCS] Action Text Documention [ci-skip] by Ridhwana · Pull Request #50977 · rails/rails -- マージ
参考: [RF DOCS] Action Mailbox Documention [ci-skip] by bhumi1102 · Pull Request #50973 · rails/rails -- マージ

🔗 Turbo 8リリース

参考: Turbo: The speed of a single-page web application without having to write any JavaScript.


つっつきボイス:「つっつきの日(2/8)の前日に、jnchitoさんのXポストでTurbo 8リリースを知りました」「おぉ、HotwireやTurbo周りはどうなっていくのかな?」「以下のEvil Martiansの記事でも取り上げているモーフィングによるページ更新などが使えるようになるそうです」

Rails: フルスタックRailsの未来(1)Turbo Morph Drive(翻訳)

  • モーフィングによるページ更新(#1019
  • ビュートランジションAPIによるナビゲーション(#935
  • InstantClickの追加(#1101
  • TypeScriptの削除(#971) -- 👍より👎が圧倒的に多いですね
  • ナビゲーション中に使われていなかったスタイルシートの削除(#1128
  • data-turbo-track="dynamic"の導入(#1140
  • 送信中のフォーム要素にaria-busyを設定(#1110
  • html[data-turbo-visit-direction]によるvisit方向取得・操作(#1007
  • turbo:{before-,}morph-{element,attribute}の導入(#1097
  • ナビゲーション中にhtml[lang]を設定(#1035
  • プリロード中のturbo:before-fetch-{request,response}ディスパッチ(#1034
    Release v8.0.0 · hotwired/turboより

🔗Ruby

🔗 Rubyの位置引数(Ruby Weeklyより)


つっつきボイス:「Rubyのいわゆるpositional arguments(位置引数)を中心とした解説記事ですが、3.0のときの改訂↓の話に加えて、ナンパラなどの最新の書き方もカバーしています」

# 同記事より
def passthrough(*)
  illegal = *           # can't do this
  somehow_legal = [*]   # somehow, this works
  a, b, *c = [*]        # as does this
  other_method(*)
end

[1, 2, 3].map { _1 * _1 }

[1, 2, 3].map { it * it } // Ruby 3.4で導入予定

参考: Ruby 3.0における位置引数とキーワード引数の分離について


今回は以上です。

バックナンバー(2024年度第1四半期)

週刊Railsウォッチ: aws-sdk-rubyの全gemにRBSファイルが追加ほか(20240207後編)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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