Rails CHANGELOG "2019-10-07".."2019-10-13"

この期間の CHANGELOG.md へのコミットは3件。

ActionPack, ActiveModel, ActiveStorageに関してそれぞれ1件。

ActionPack

ActionDispatch::Request クラスのインスタンス変数 @remote_ip を更新できない不具合を修正した

コミット: Updated `ActionDispatch::Request.remote_ip=` · rails/rails@bf14a8e

修正は remote_ip= メソッドの @remote_ip = nil の一行だけ。

def remote_ip
  @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
end

def remote_ip=(remote_ip)
  @remote_ip = nil
  set_header "action_dispatch.remote_ip", remote_ip
end

引用: rails/request.rb at bf14a8e23526447b9893ed66090f898da68eb7f1 · rails/rails

@remote_ip = nil をせずに remote_ip メソッドを呼んでいたため、set済みの @remote_ip のまま書き変わらない不具合があった。

ActiveModel

freezeされた ActiveModel のオブジェクトに対して attribute の書き変えをできないようにした

コミット: Raise FrozenError for frozen objects when trying to write to a · rails/rails@54b1574

概要はCHANGELOG.mdにある例の通り。

class Animal
  include ActiveModel::Attributes  
  attribute :age 
end

animal = Animal.new
animal.freeze 
animal.age = 25 # => FrozenError, "can't modify a frozen Animal"

引用: rails/CHANGELOG.md at 54b157442171d28c7e9553537aef7a6ae571aad7 · rails/rails

修正は ActiveModel:: Attributes::ClassMethods モジュールの define_method_attribute= メソッド。

def define_method_attribute=(name)
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
    generated_attribute_methods, name, writer: true,
  ) do |temp_method_name, attr_name_expr|
    generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{temp_method_name}(value)
        raise FrozenError, "can't modify frozen #{self.class.name}" if frozen?
        name = #{attr_name_expr}
        write_attribute(name, value)
      end
    RUBY
  end
end

引用: rails/activemodel/lib/active_model/attributes.rb at 54b157442171d28c7e9553537aef7a6ae571aad7 · rails/rails

なるほどこういう実装になっていたのか〜。

ActiveStorage

ActiveStorage::Blob#url でパーマネントリンクを作れるようになった

コミット: Permanent URLs for public storage blobs · rails/rails@feab703

config/storage.yml に新しいキー public: true | false を設定することで、 true の場合はパーマネントリンクを作ることができる。未設定の場合は false で、今まで通り期限付きのリンクになる。

ActiveStorage::Blob#url を見てみる。ここでは service.url(ActiveStorage::Service#url) を呼んでる。

def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
  filename = ActiveStorage::Filename.wrap(filename || self.filename)

  service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
    disposition: forced_disposition_for_service_url || disposition, **options
end

引用: rails/blob.rb at feab7031b57040afa2b2f5f78f9251dbad6cbdf8 · rails/rails

ActiveStorage::Service#url を見てみる。if public?public_url or private_url に振り分けてる。

def url(key, **options)
  instrument :url, key: key do |payload|
    generated_url =
      if public?
        public_url(key, **options)
      else
        private_url(key, **options)
      end

    payload[:url] = generated_url

    generated_url
  end
end

引用: rails/service.rb at feab7031b57040afa2b2f5f78f9251dbad6cbdf8 · rails/rails

ActiveStorage::Service クラスの public_urlprivate_url は未実装(raise NotImplementedError)となっており、このクラスを継承している各ストレージサービスのクラスで public_urlprivate_url を実装している。具体的な実装は各クラスを参照。

子クラスで実装を分けたい(かつ未実装だったらエラー吐きたい)ときに、親クラスで raise NotImplementedError にしとくの便利。よくある(定石的な)実装なのかな。これまでまともにOSSのコードリーディングしてなかった&設計関連の本もほぼ読んでないからこの辺の設計論?みたいなもの、自論として確立できてないんだよなぁ。ともかく勉強になった。