Rails Developers Beer Bash 〜Railsのトレンドとこれから〜に行ってきました

概要

今回参加したイベントはこちらになります

techplay.jp

最近、Railsの勉強会に参加できてなかったので、最新情報とかいろいろとキャッチアップできればな〜と思い参加しました

参加したセッション

  1. 2週間で Rails 5.1 > Rails 6 タイムアタックチャレンジ
  2. Multi DBを実戦投入するときの落とし穴
  3. Railsパネルディスカッション

1. 2週間で Rails 5.1 > Rails 6 タイムアタックチャレンジ

speakerdeck.com

参加にあたって、特に気になっていたところ

  1. アップデートに関して気を付けることは何か?
  2. ボトルネックになりそうなことは何か?

1. アップデートに関して気を付けることは何か?

  • DEPRECATION WARNING はできるだけ消す
  • backportして
    • 先にリリースで安心感を得られる
  • 主要機能は、手動で動作確認
    • 決済機能など

2. ボトルネックになりそうなことは何か?

  • リリースノートは読んでおき、脳内インデックスを作っておくとスピードが増す
    • WARNING を消す際に必要な作業

感想

Railsアップグレードする場合は、

  • なるべく小さくリリースしていくのが大事
  • 主要機能はやはり手動で動作確認するの大事

なんだと思いました


2. Multi DBを実戦投入するときの落とし穴

www.slideshare.net

参加にあたって、特に気になっていたところ

  1. 投入する上でのハマりどころ

1. 投入する上でのハマりどころ

  • primary にトランザクション貼っているかreplica に貼っているかを意識する必要がある
  • 便利なmiddlewareのハマりどころ
    • HTTPメソッドにもとづいて read/writeを自動で決定するのが、不適切な場合がある
      • middlewareは使わず、 around_action で行う
  • 6.0.0 のdelay機能は曲者
    • 書き込みDBにアクセスしたあとGET/HEADも書き込み用DBにつなぐ
      • 書き込み用DBにつないでいる状態で、書き込み系クエリを投げるとエラーが発生する

感想

MultiDBは結構ハマりどころが多そうだったので、非常に学びがあった


3. Railsパネルディスカッション

参加にあたって、特に気になっていたところ

  1. Rails 6でうれしい機能は?
  2. Railsでマイクロサービスはどうやってやるのか?
  3. Railsと技術的負債の付き合い方は?

1. Rails 6でうれしい機能は?

2. Railsでマイクロサービスはどうやってやるのか?

  • Railから大きくハズレない
    • Railに乗っているところが巨大化するものは問題ない
  • 認証周りなど境界づけられたコンテキストごとにマイクロサービスして巨大化したコードベースを分割する

3. Railsと技術的負債の付き合い方は?

  • 依存ライブラリがRails 6に対応していないとアップグレードするのが、だいぶつらい
    • テストカバレッジが高いことがアップグレードを助けてくれる
  • バグを減らす日を設けて、継続的に負債を返却している
  • Dependabot で継続的にGemのバージョンアップをしている
  • モデルのレイヤをモデル層とデータ層に分割している

感想

各社の工夫の部分だったり、Railsで大きいサービスを作る場合の設計の指針はどうすればいいのか?など非常に学びがありました


LTのスライド

今日一日の感想

上記セッション以外にも懇親会 & LT大会 もあったのですが、翌日に作業があったため、途中で退室しました 💨
久しぶりのRailsの勉強会だったので、非常に楽しめました!
また機会があれば、参加したいです 💪

f:id:sktktk1230:20190926202810j:plain

銀座Rails#13のセッションを見てみました

概要

銀座Rails#13 @リンクアンドモチベーション - connpass

前から開催されていおり、気になっていた銀座Railsについに参加しようと思い応募してみましたが、 当日は、別の予定が入ってしまい泣く泣く参加できず

なので、オンラインで公開されているスライドやTwitterなどから気になっていたことなどをまとめてみます
未参加でイベントレポートを書くという特殊な試みです

当日開催されたセッション

  • 出張Railsウォッチ in 銀座Rails
  • fixture再考
  • RailsとJSのデータ橋渡しについて考えてみた
  • Railsのパフォーマンスをチューニングしてみる話

出張Railsウォッチ in 銀座Rails

speakerdeck.com

参加にあたって、特に気になっていたところ

  1. 今月のRailsの動きの最新情報
  2. 情報の仕入れ方の話とかあれば、それを知りたい

1. 今月のRailsの動きの最新情報

Action Text

  • What Yout See Is What You Get
    • 画面で見えているものと出力されるものが同じになるUIのこと
  • なぜ嬉しいのか?
    • アップロード周りの取り扱いが楽になる
    • ラピッドプロトタイピング開発で考えることが減る
  • 印象
    • 最低限の手間でサクッと画像・ファイルアップロードまで動くのは良い
    • 現代最低限求められる挙動を満たしている
    • 定義済みスタイルや機能は少ない

2. 情報の仕入れ方の話とかあれば、それを知りたい

ここらへんの話は特にありませんでした
話題になったGem/ライブラリ/スライドなどを紹介

感想

Action Textは、まだ触ってなかったので、使ってみた感触などが把握できてよかった


fixture再考

t.co

参加にあたって、特に気になっていたところ

  1. 通常とは違うfixtureの話であればそれの使い方

1. 通常とは違うfixtureの話であればそれの使い方

  • メリット
    • テストデータの生成が早い
      • vs FactoryBot
    • use_transactional_test との相性が良い
  • dev.toのテストデータを書き換えて、速度検証
    • 約13秒の実行時間の減少

f:id:sktktk1230:20190913155434p:plain

感想

fixtureはmasterデータ生成くらいでしか、使ってなかったので、使い所次第では有用なことが理解できて勉強になりました


RailsとJSのデータ橋渡しについて考えてみた

speakerdeck.com

参加にあたって、特に気になっていたところ

  1. SSRとAPIでのパフォーマンスなどの違い

1. DOMとAPIでのパフォーマンスなどの違い

  • API
    • 表示し始めが早い
    • Audits判定が早い
  • DOM
    • 表示終わりが早い

感想

  • それぞれの違いが明確になって、理解できた

Railsのパフォーマンスをチューニングしてみる話

TODO: - スライドを入れる

参加にあたって、特に気になっていたところ

  1. パフォーマンスチューニングする際の手順
  2. どこがボトルネックになりやすい?
  3. パフォーマンスチューニングするにあたって気をつけること

感想

TODO:


今日一日の感想

  • Rails関連の最新情報や普段使用している機能の深い考察など学びが非常に多かったので、次回こそは参加できるように予定を調整したい 💪

builderscon tokyo 2019に行ってきました

f:id:sktktk1230:20180726121247p:plain

2019/08/23のbuildersconに参加してきました
今回は、非同期処理や分散システムなどに興味があったので、そこをメインで見てきました

参加したセッション

メルペイ開発の裏側

t.co

参加にあたって、特に気になっていたところ

  1. データ整合性の担保をどうしているのか?
  2. ロールバックはどうしているのか?

1. データ整合性の担保をどうしているのか?

基本はリトライし、継続不可能なときのみ、処理をやめるようです

リトライと冪等性 - どんなエラーが出ても基本的にリトライする - 冪等性を担保して二重処理されないようにする - 継続不可能なときのみ処理をやめる

引用:P.22

リコンサイル

[名](スル)《一致させる、の意》複数帳簿間で残高照合を行うこと。また、金融関連では外国の銀行に保有する口座の取引明細と、自行の処理した取引明細と照合すること。 引用:リコンサイル

することで、データの整合性を担保しているそうです

データの整合性を確認すること - 会計データが完全な状態だということを保証する - 各マイクロサービスと会計システム間

引用:P.29

2. ロールバックはどうしているのか?

状態遷移モデルを採用し、どんな処理でも意図せず落ちることがある為、処理単位を記録するように状態を定義しているようです

ロールバックも状態遷移で定義 - 途中で継続不可能(tryが失敗)した場合も遷移先が異なるだけ - Cancelを行う状態を定義して一貫したモデルで扱う 引用:P.25

感想

トランザクション管理はやはり難しそうで、データ整合性を担保する為に、かなり安全に倒して設計されているんだなと思いました
また開発もサービス安定性、品質を重視されているので、リリースまで重厚なプロセスがあると感じました


RDBのトラブルの現場を追え!

speakerdeck.com

参加にあたって、特に気になっていたところ

  • どうやって障害理由を切り分け、そして対応してきたのか?

1. どうやって障害理由を切り分け、そして対応してきたのか?

壊れたとは何かをちゃんと特定すること たとえば、

  1. 突然パフォーマンスが悪化した
  2. データの不整合が発生している
  3. データベースが応答を返さない
  4. コネクションが溢れている
  5. 間違えてDROP TABLEしちゃった(バルス)

引用

感想

これは知らないDBの機能でした

  1. MySQLだと beginしても DROP TABLEするとauto commitが走って消える
  2. バルクインサートより、MySQLならLOAD、PostgreSQLならCOPYが早い
    MysqlSQLはこちら

    テキストファイルからテーブルをロードする場合は LOAD DATA INFILE を使用します。通常、これは INSERT ステートメントを使用する場合より、20 倍速くなります。セクション13.2.6「LOAD DATA INFILE 構文」を参照してください。

    引用:8.2.2.1 INSERT ステートメントの速度

    PostgreSQLはこちら

    PostgreSQL に大量のデータを高速に取り込む方法を紹介します。 COPY という専用のコマンドを使うと INSERT よりもずっと高速です。 また、COPY を使う際にひと工夫すると、さらに速くなります。

    引用:大量のデータを高速に投入するには

  3. B-Treeインデックスは全体の10%くらいのデータ量じゃないとインデックス効かない

Web API に秩序を与える Protocol Buffers 活用法

speakerdeck.com

参加にあたって、特に気になっていたところ

  • Web APIのSchema管理について

1. Web APIのSchema管理について

  • .proto ファイルを記述して、コードを自動生成する
  • 豊富なpluginでswagger.jsonの生成も可能

f:id:sktktk1230:20190830151249p:plain

感想

実装よりも振る舞いに集中するして開発できるのはかなり楽だなと思いました .proto file 自体もシンプルに見えるので、学習コストもあまりかからず導入できそうだなと感じました


Optimizing Ruby with JIT - 最速の言語を目指して

t.co

参加にあたって、特に気になっていたところ

  • RubyのJust-In-Timeコンパイラがいかにしてそのような言語の高速化を実現しているかのエッセンス

1. RubyのJust-In-Timeコンパイラがいかにしてそのような言語の高速化を実現しているかのエッセンス

  1. 機械語にしただけでは、早くならない
    • 処理を減らすことが最適化への道

感想

JIT コンパイラが何をやっているのか?を知らなかったので、ここが学べたのは、非常に大きいなと思いました


ウォレットアプリ「Kyash」の先 〜「Kyash Direct」のアーキテクチャ〜

t.co

参加にあたって、特に気になっていたところ

  • いろいろな設計を行っているが運用してみてどうだったのか?
    • DDD
    • Clean Architecture
    • Microservices
    • Orchestration/Choreography
    • Message Pub/Sub
    • Event Driven Architecture/Event Sourcing
    • CQRS
    • 分散Tracing

1. いろいろな設計を行っているが運用してみてどうだったのか?

運用はこれからなので、まだまだ試行錯誤が続きそう。。。

とのことでした

マイクロサービスの選択理由

  • モノリスにするには巨大で複雑すぎる
  • 部分的な変更が全体に影響を及ぼすのを避けたい
  • 部分ごとに負荷が大きく異なる
  • 外部サービスとの接続部分を切り離しておきたい
  • 機能追加のスピードは極力落としたくない
Pros.
  • 各サービス単体では、小さくてシンプル
  • 変更の影響を局所化できる
  • スケールしやすい
  • デプロイしやすい
  • リソース配分を最適化しやすい
Cons.
  • 設計・実装が難しい

感想

イベントドリブンアーキテクチャの設計を見たことがなかったので、ここまで難しいのかということと、かなり複雑なアーキテクチャで運用がつらそうと思ったが、エンジニアとしてはかなり刺激的だなと思いました

今日一日の感想

仕事でもすぐに生かしていけそうな知識からエンジニアとしての基礎力を上げるような内容まで幅広く非常に楽しめた一日でした

f:id:sktktk1230:20190830165635j:plain

※ 当日貰ったTシャツとバック

f:id:sktktk1230:20190830151909j:plain

f:id:sktktk1230:20190830152036j:plain

Railsのソースコード読んでみる | Active Support subclasses編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

subclasses を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

subclassesメソッドはレシーバのサブクラスを返します。

class C; end
C.subclasses # => []

class B < C; end
C.subclasses # => [B]

class A < B; end
C.subclasses # => [B]

class D < C; end
C.subclasses # => [B, D]

返されるクラスの順序は一定ではありません。

引用:RAILS GUIDES:subclasses

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def subclasses を探してみます

f:id:sktktk1230:20190806150812p:plain

2. 該当箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > class > subclasses.rb
class Class

省略

  # Returns an array with the direct children of +self+.
  #
  #   class Foo; end
  #   class Bar < Foo; end
  #   class Baz < Bar; end
  #
  #   Foo.subclasses # => [Bar]
  def subclasses
    subclasses, chain = [], descendants
    chain.each do |k|
      subclasses << k unless chain.any? { |c| c > k }
    end
    subclasses
  end
end

まず、一行目から見ていきます

  def subclasses
    subclasses, chain = [], descendants

多重代入を行い、2つの変数の初期化をしています

Rubyリファレンスの多重代入を見てみます

多重代入

例:

foo, bar, baz = 1, 2, 3
foo, = list()
foo, *rest = list2()

文法:

   式 [`,' [式 `,' ... ] [`*' [式]]] = 式 [, 式 ... ][`*' 式]
   `*' [式] = 式 [, 式 ... ][`*' 式]</p>

引用:Ruby 2.6.0 リファレンスマニュアル:多重代入

今回の場合だと、 変数 subclasses は空の配列で初期化、 変数 chaindescendants メソッドの戻り値で初期化されています

descendants というメソッド名から推測すると、 descendant は子孫という意味、複数形の s が付く為、 継承チェインのオブジェクトの配列が戻り値では無いかと思われます

次の処理を見ていきます
chain は継承チェイン上にあるオブジェクトの配列を each で1つずつ取り出しています

chain.each do |k|
  subclasses << k unless chain.any? { |c| c > k }
end

まずは、unless 修飾子を読んでみます

unless chain.any? { |c| c > k }

any? は真である要素があれば、trueを戻り値とし、無ければfalseを返します

参照:instance method Enumerable#any? (Ruby 2.6.0)

今回はブロックがある為、ブロックの条件を満たす場合に、trueを返し、満たさない場合は、falseになります
つまり、条件を1度も満たさない場合のみ、左辺の式を評価するということです

ブロック内の処理を見ると、配列 chain と eachで取り出した、 k を比較しています

参照:module Comparable (Ruby 2.6.0)

例えば、class Cを継承した B, D がある場合

class C; end
C.subclasses # => []

class B < C; end
C.subclasses # => [B]

class D < C; end
C.subclasses # => [B, D]

Cに対して、 descendants を実行すると以下になります
f:id:sktktk1230:20190806150936p:plain

つまり、変数 chain[D, B] です

改めて、eachの処理を見ます

chain.each do |k|
  subclasses << k unless chain.any? { |c| c > k }
end

ブロック引数 kD となります
chain.any? { |c| c > k } を見ると、

  1. cD の場合は、D > D で false
  2. cB の場合は、B > D で false

です
そのため、空配列の変数 subclasses に ブロック引数 k が追加されます

次のループでは、 ブロック引数 kB となります
chain.any? { |c| c > k } を見ると、

  1. cD の場合は、D > B で false
  2. cB の場合は、B > B で false

です
そのため、空配列の変数 subclasses に ブロック引数 k が追加されます

すると変数 subclasses[D, B] です

メソッドの処理全体を見ていくと

  def subclasses
    subclasses, chain = [], descendants
    chain.each do |k|
      subclasses << k unless chain.any? { |c| c > k }
    end
    subclasses
  end

変数 subclasses を戻り値としています

Railsのソースコード読んでみる | Active Support cattr_reader編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

cattr_reader を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

これらのメソッドは、クラス変数をnilに設定し (クラス変数が既にある場合を除く)、対応するクラスメソッドを生成してアクセスできるようにします。

class MysqlAdapter < AbstractAdapter
  # @@emulate_booleansにアクセスできるクラスメソッドを生成する
  cattr_accessor :emulate_booleans, default: true
end

引用:RAILS GUIDES:4.1.2 cattr_reader、cattr_writer、cattr_accessor

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def cattr_reader を探してみます

該当する定義がないので cattr_reader で探します

f:id:sktktk1230:20190806123806p:plain

検索結果内に aliasメソッドがあったので、実際の定義は mattr_reader のようです

alias :cattr_reader :mattr_reader

いつも通りコードリーディングしながら記事を書いていたんですが、以前 mattr_reader 読んでいたので、泣く泣くそれは破棄しました

mattr_reader のコードリーディング記事のリンク貼っておきます

blog.shitake4.tech

Rails のソースコード読んでみる | Active Support class_attribute 編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っている Ruby on Rails ですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

class_attribute を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDES の日本語ドキュメントを見てみると

4.1.1 class_attribute

class_attributeメソッドは、1つ以上の継承可能なクラスの属性を宣言します。そのクラス属性は、その下のどの階層でも上書き可能です。

class A
  class_attribute :x
end

class B < A; end

class C < B; end

A.x = :a
B.x # => :a
C.x # => :a

B.x = :b
A.x # => :a
C.x # => :b

C.x = :c
A.x # => :a
B.x # => :b

引用:RAILS GUIDES:class_attribute

ソースコードを読んでみる

1. rails プロジェクトの activesupport にある機能ですので、activesupport ディレクトリの lib 配下で def class_attibute を探してみます

f:id:sktktk1230:20190529145022p:plain

2. 該当箇所が 1 箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > class > attribute.rb
# frozen_string_literal: true

require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/redefine_method"
require "active_support/core_ext/array/extract_options"

class Class

省略

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )
    attrs.each do |name|
      singleton_class.silence_redefinition_of_method(name)
      define_singleton_method(name) { default }

      singleton_class.silence_redefinition_of_method("#{name}?")
      define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate

      ivar = "@#{name}".to_sym

      singleton_class.silence_redefinition_of_method("#{name}=")
      define_singleton_method("#{name}=") do |val|
        redefine_singleton_method(name) { val }

        if singleton_class?
          class_eval do
            redefine_method(name) do
              if instance_variable_defined? ivar
                instance_variable_get ivar
              else
                singleton_class.send name
              end
            end
          end
        end
        val
      end

      if instance_reader
        redefine_method(name) do
          if instance_variable_defined?(ivar)
            instance_variable_get ivar
          else
            self.class.public_send name
          end
        end

        redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
      end

      if instance_writer
        redefine_method("#{name}=") do |val|
          instance_variable_set ivar, val
        end
      end
    end
  end

まず class_attribute の引数からみてみます

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )

attrs は可変長引数で instance_accessor などの引数はキーワード引数です
可変長引数は引数を配列として受け取れ、キーワード引数はハッシュとして引数で宣言できます

参照:Rubyの引数いろいろ - Qiita

それぞれ以下のように挙動します

class A
  def self.hoge(*attrs, huga: 'a')
    p '==attrs=='
    p attrs
    p '==huga=='
    p huga
  end
end

=> A.hoge(1,2,3, huga: 'b')
"==attrs=="
[1, 2, 3]
"==huga=="
"b"

それではメソッドの中身を見ていきます

attrs.each do |name|
  singleton_class.silence_redefinition_of_method(name)
  define_singleton_method(name) { default }

  singleton_class.silence_redefinition_of_method("#{name}?")
  define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate

  ivar = "@#{name}".to_sym

attrs は先程の可変長引数で配列になります。配列の要素を each で1つずつ取り出して処理しています

each の中の処理を見ていきます
singleton_class.silence_redefinition_of_method(name)
singleton_class メソッドでレシーバの特異クラスを戻り値とします

参照:instance method Object#singleton_class (Ruby 2.6.0)

silence_redefinition_of_methodactive_support > core_ext > module > redefine_method.rb に定義されているメソッドです
処理内容と経緯については @y-yagi さんのブログに記載がありましたので、こちらを引用させて頂きます

普通にメソッドを再定義しようとすると既にメソッドがある為Rubyのwarningが発生します。それを避ける為、今まではremove_possible_methodメソッドを先に呼び出してメソッドを未定義にするようにしていました(redefine_methodメソッド内でもそのような挙動になっています)。が、メソッドを再定義する、という事をする為にメソッドを未定義にする、というのは目的がわかりにくいのでは、という事で新たにremove_possible_methodを使用せずに再定義するメソッドが追加されました。

実装は下記の通りです(Ruby 2.3以降の場合)。

# Marks the named method as intended to be redefined, if it exists.
# Suppresses the Ruby method redefinition warning. Prefer
# #redefine_method where possible.
def silence_redefinition_of_method(method)
  if method_defined?(method) || private_method_defined?(method)
    # This suppresses the "method redefined" warning; the self-alias
    # looks odd, but means we don't need to generate a unique name
    alias_method method, method
  end
end

alias_methodを使用して対応しているんですねえ。

引用:rails commit log流し読み(2017/09/01)

処理自体はメソッドの再定義をRubyのエラーを表示させず、実行できるものです

次の行を見てみます

  define_singleton_method(name) { default }

define_singleton_method は、レシーバにシングルトンのメソッドを追加できるメソッドです
使い方は以下の通りです

define_singleton_method(symbol, method) → symbol
define_singleton_method(symbol) { block } → symbol

引用:Rubyドキュメント2.6

define_singleton_method の引数とブロック内の default にそれぞれ何が入るかは name は、可変長引数 attrs の各要素で、ブロック内の default は引数のdefaultが入ります

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )
    attrs.each do |name|
      singleton_class.silence_redefinition_of_method(name)
      define_singleton_method(name) { default }

次の行をみます
先程見てきた処理とほとんど一緒です

  singleton_class.silence_redefinition_of_method("#{name}?")

違いは name? にしているところです
rubyで ? が付くメソッドは、booleanを返すメソッドです

次の行をみます

  define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate

instance_predicate はメソッドの引数の値でデフォルト値が true のものです
define_singleton_method("#{name}?") は、先ほどと一緒でシングルトンメソッドを定義している処理です
ブロック内の処理 { !!public_send(name) } を見ていきます

public_send がどんな処理をするか見てみると、引数のパブリックメソッドを呼び出す処理です

オブジェクトの public メソッド name を args を引数にして呼び出し、メソッ ドの実行結果を返します。

1.public_send(:+, 2)  # => 3

引用:Ruby 2.6.0 リファレンスマニュアル

public_sendの前にある !! はdouble bangという方法で 戻り値を booleanにする方法です

参照:Rubyの否定演算子2つ重ね「!!」(double-bang)でtrue/falseを返す

ちなみにrubocopではチェック対象になります

Examples:

# bad
!!something

# good
!something.nil?

引用:Class: RuboCop::Cop::Style::DoubleNegation

次の行をみます
インスタンス変数名をシンボルにしています
例えば、name が hoge な場合だと、 :@hoge となります

ivar = "@#{name}".to_sym

次の行をみます

singleton_class.silence_redefinition_of_method("#{name}=")

今までと同じようにこんどはsetterのメソッド定義しています

まずは、次の処理の全体像をみます

  define_singleton_method("#{name}=") do |val|
    redefine_singleton_method(name) { val }

    if singleton_class?
      class_eval do
        redefine_method(name) do
          if instance_variable_defined? ivar
            instance_variable_get ivar
          else
            singleton_class.send name
          end
        end
      end
    end
    val
  end

define_singleton_method("#{name}=") は今までと同じでシングルトンメソッドの定義です

次の行をみます

redefine_singleton_method(name) { val }

redefine_singleton_method は既に定義されているメソッドを上書きするRailsのメソッドです

参照:Module

次のif文内の全体像をみます

if singleton_class?
  class_eval do
    redefine_method(name) do
      if instance_variable_defined? ivar
        instance_variable_get ivar
      else
        singleton_class.send name
      end
    end
  end
end

最初の行は、レシーバがシングルトンのクラスかどうか判定しています

if singleton_class?

次の行をみます

class_eval do

省略

end

class_eval のブロック内の処理をレシーバに動的に実装します

モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。 モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。

引用:Ruby 2.6.0 リファレンスマニュアル#class_eval

class_eval のブロック内を見ていきます

redefine_method(name) do
  if instance_variable_defined? ivar
    instance_variable_get ivar
  else
    singleton_class.send name
  end
end

redefine_methodactivesupport > lib > active_support > core_ext > class > redefine_method.rb の17行目に定義されているメソッドで定義済みのメソッドを書き換える処理を行います

instance_variable_defined? は引数のインスタンス変数名が定義されていれば、真を返すメソッドです

インスタンス変数 var が定義されていたら真を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#instance_variable_defined?

ivarclass_attribute メソッドの引数 *attrs の要素の1つになります

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )
    attrs.each do |name|
    
    省略
    
      ivar = "@#{name}".to_sym

if instance_variable_defined? ivar が真の場合は、instance_variable_get ivar でレシーバのインスタンス変数を取り出します

オブジェクトのインスタンス変数の値を取得して返します。 インスタンス変数が定義されていなければ nil を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#instance_variable_get

偽の場合は、singleton_class.send name でレシーバから特異クラスを返し、特異クラスに対して send namename メソッドを呼び出します

レシーバの特異クラスを返します。 まだ特異クラスがなければ、新しく作成します。 レシーバが nil か true か false なら、それぞれ NilClass, TrueClass, FalseClass を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#singleton_class

次にこちらをみます
今まで見てきた実装とほぼ同じです
ことなる部分は、 if instance_variable_defined?(ivar) が偽の場合に、 self.class.public_send name でレシーバのクラスメソッドを呼び出す処理になっている部分です

instance method Object#public_send (Ruby 2.6.0)

  if instance_reader
    redefine_method(name) do
      if instance_variable_defined?(ivar)
        instance_variable_get ivar
      else
        self.class.public_send name
      end
    end

    redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
  end

if instance_predicate が真の場合は、レシーバのpublicなメソッドとして定義されている name を定義しています
例えば、 namehoge である場合は、 hoge? というメソッドが定義され、 戻り値が true, false になります

次をみていきます
instance_writer が真の場合に、redefine_method でセッターを定義しています

  if instance_writer
    redefine_method("#{name}=") do |val|
      instance_variable_set ivar, val
    end
  end

参照:instance method Object#instance_variable_set (Ruby 2.6.0)

Action Mailer Previewsをproduction環境で使えるようにする

f:id:sktktk1230:20180726121250p:plain

Railsに搭載されているAction Mailerという機能でメールを送信することができるのですが、このメールの文面のプレビュー機能がAction Mailer Previewsです
このプレビュー機能をproduction環境でも使えるようにして、非エンジニアの人にも簡単にメールの文面を確認してもらいたいと思って、対応してみたので、そのやり方をまとめてみました

環境

  • Ruby 2.4.2
  • Ruby on Rails 4.2.8

手順

1. production環境でmailer_previewが閲覧できるように config/application.rb に設定の追加をします

config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
# production環境でもpreviewが可能にする
config.action_mailer.show_previews = true

2. ドメイン名/rails/mailers から管理画面のパス配下の ドメイン名/admin/rails/mailers になるようにします

config/initializers 配下にファイルを作成し、routes.rbへの追加処理を記述します

Rails.application.config.after_initialize do |app|
  app.routes.prepend do
    get "admin/rails/mailers"         => "rails/mailers#index", internal: true
    get "admin/rails/mailers/*path"   => "rails/mailers#preview", internal: true
    get "rails/mailers"         => redirect('admin/rails/mailers')
    get "rails/mailers/*path"   => redirect('admin/rails/mailers#preview')
  end
end

こちらの記述はrails/railsを参照しました github.com

3. lib/mailer_previews/previews 配下にpreviewのクラスを作成します

y_yagiさんのブログが非常にわかりやすいです

参照:RailsのAction Mailer Previewsについて | 日々雑記

注意点

これでproduction環境で domain/admin/rails/mailers にアクセスするとmailerを閲覧することが可能になりましたが、認証機能はないので、ユーザーも /admin/rails/mailers にアクセスすると閲覧出来てしまいます
そのため、webサーバ(nginx)等で /admin を社内からのみアクセスできないようになど対策する必要があります

Railsのソースコード読んでみる | Active Support delegate_missing_to編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

delegate_missing_to を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

UserオブジェクトにないものをProfileにあるものにすべて委譲したいとしましょう。

delegate_missing_toマクロを使えばこれを簡単に実装できます。

class User < ApplicationRecord
  has_one :profile
 
  delegate_missing_to :profile
end

オブジェクト内にある呼び出し可能なもの(インスタンス変数、メソッド、定数など)なら何でも対象にできます。対象のうち、publicなメソッドだけが委譲されます。

引用:RAILS GUIDES:delegate_missing_to

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def delegate_missing_to を探してみます

f:id:sktktk1230:20181206152315p:plain

2. 該当箇所が1箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > module > delegation.rb
# frozen_string_literal: true

require "set"

class Module

省略

def delegate_missing_to(target)
    target = target.to_s
    target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def respond_to_missing?(name, include_private = false)
        # It may look like an oversight, but we deliberately do not pass
        # +include_private+, because they do not get delegated.

        #{target}.respond_to?(name) || super
      end

      def method_missing(method, *args, &block)
        if #{target}.respond_to?(method)
          #{target}.public_send(method, *args, &block)
        else
          begin
            super
          rescue NoMethodError
            if #{target}.nil?
              raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
            else
              raise
            end
          end
        end
      end
    RUBY
  end
end

まずはここまで見ていきます

def delegate_missing_to(target)
  target = target.to_s
  target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

引数targetを文字列へ変換しています
そしてtargetは DELEGATION_RESERVED_METHOD_NAMES に含まれているのかチェックしています

定数 DELEGATION_RESERVED_METHOD_NAMES がどのようなものか調べる為、記述箇所付近を見てみます

class Module
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
  # option is not used.
  class DelegationError < NoMethodError; end

  RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
  else elsif END end ensure false for if in module next nil not or redo rescue retry
  return self super then true undef unless until when while yield)
  DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
  DELEGATION_RESERVED_METHOD_NAMES = Set.new(
    RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
  ).freeze

定義箇所はこちらです

DELEGATION_RESERVED_METHOD_NAMES = Set.new(
  RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze

class Set (Ruby 2.6.0)は集合を表すクラスになります
Set.new で引数のオブジェクトを要素として集合を作ります

Set.new に配列を与えると

Setの挙動
Setの挙動

このような集合を作成します

RUBY_RESERVED_KEYWORDS, DELEGATION_RESERVED_KEYWORDS で定義されている文字列を見てみると

RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
else elsif END end ensure false for if in module next nil not or redo rescue retry
return self super then true undef unless until when while yield)
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)

こちらの文字列に含まれているかをチェックしているようです

ここまでを踏まえてもう一度、こちらを見てみると

target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

targetが含まれている場合は、self.target で自身のメソッドを呼び出します

そして次を見ていきます

module_eval <<-RUBY, __FILE__, __LINE__ + 1
  def respond_to_missing?(name, include_private = false)
    # It may look like an oversight, but we deliberately do not pass
    # +include_private+, because they do not get delegated.

    #{target}.respond_to?(name) || super
  end

  def method_missing(method, *args, &block)
    if #{target}.respond_to?(method)
      #{target}.public_send(method, *args, &block)
    else
      begin
        super
      rescue NoMethodError
        if #{target}.nil?
          raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
        else
          raise
        end
      end
    end
  end
RUBY

instance method Module#class_eval (Ruby 2.6.0)を使用して動的にメソッドを定義しています
ヒアドキュメント <<-RUBY 〜 RUBY に記載されている FILE , LINE は疑似変数というものです

FILE
現在のソースファイル名

フルパスとは限らないため、フルパスが必要な場合は File.expand_path(FILE) とする必要があります。

LINE
現在のソースファイル中の行番号
引用:Ruby 2.5.0 リファレンスマニュアル#疑似変数

動的に定義するメソッドを1つずつ見ていきます

def respond_to_missing?(name, include_private = false)
  # It may look like an oversight, but we deliberately do not pass
  # +include_private+, because they do not get delegated.

  #{target}.respond_to?(name) || super
end

実際に動作する部分はこちらで

#{target}.respond_to?(name) || super

targetにnameメソッドが存在するか、もしなければ、継承チェーン内の親の instance method Object#respond_to_missing? (Ruby 2.6.0)を実行します

次にもうひとつ定義されるメソッドを見てみます

def method_missing(method, *args, &block)
  if #{target}.respond_to?(method)
    #{target}.public_send(method, *args, &block)
  else
    begin
      super
    rescue NoMethodError
      if #{target}.nil?
        raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
      else
        raise
      end
    end
  end
end

instance method BasicObject#method_missing (Ruby 2.6.0)はメタプログラミングで利用されるテクニックで、オーバーライドすることで、存在しないメソッドを呼び出したときに、エラーとなる前に特定の処理を実行することができます

targetのpublicなmethodに存在しているか確認しています instance method Object#public_send (Ruby 2.6.0)
存在している場合は、そのメソッドを呼び出すという処理です

if #{target}.respond_to?(method)
  #{target}.public_send(method, *args, &block)

else句を見てみると、 NoMethodError が発生した場合、 targetがnilであれば、DelegationError とするようです

else
  begin
    super
  rescue NoMethodError
    if #{target}.nil?
      raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
    else
      raise
    end
  end
end

メタプログラミングについてはこちらの書籍がわかりやすかったので、もし読んでない方は読んでおくといいかもしれないです

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

Railsのソースコード読んでみる | Active Support delegate編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

delegate を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

elegateマクロを使用すると、メソッドを簡単に委譲できます。


あるアプリケーションのUserモデルにログイン情報があり、それに関連する名前などの情報はProfileモデルにあるとします。

class User < ApplicationRecord
  has_one :profile
end

この構成では、user.profile.nameのようにプロファイル越しにユーザー名を取得することになります。これらの属性に直接アクセスできたらもっと便利になることでしょう。

class User < ApplicationRecord
  has_one :profile
 
  def name
    profile.name
  end
end

delegateを使用すればできるようになります。

class User < ApplicationRecord
  has_one :profile
 
  delegate :name, to: :profile
end

引用:RAILS GUIDES:delegate

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def delegate を探してみます

f:id:sktktk1230:20181106163127p:plain

2. 該当箇所が10個ほどあったので、それぞれみてみます

1. activesupport > lib > active_support > core_ext > module > delegation.rb
# frozen_string_literal: true

require "set"

class Module
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
  # option is not used.

省略

def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
    unless to
      raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
    end

    if prefix == true && /^[^a-z_]/.match?(to)
      raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
    end

    method_prefix = \
      if prefix
        "#{prefix == true ? to : prefix}_"
      else
        ""
      end

    location = caller_locations(1, 1).first
    file, line = location.path, location.lineno

    to = to.to_s
    to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)

    method_names = methods.map do |method|
      # Attribute writer methods only accept one argument. Makes sure []=
      # methods still accept two arguments.
      definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"

      # The following generated method calls the target exactly once, storing
      # the returned value in a dummy variable.
      #
      # Reason is twofold: On one hand doing less calls is in general better.
      # On the other hand it could be that the target has side-effects,
      # whereas conceptually, from the user point of view, the delegator should
      # be doing one call.
      if allow_nil
        method_def = [
          "def #{method_prefix}#{method}(#{definition})",
          "_ = #{to}",
          "if !_.nil? || nil.respond_to?(:#{method})",
          "  _.#{method}(#{definition})",
          "end",
        "end"
        ].join ";"
      else
        exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")

        method_def = [
          "def #{method_prefix}#{method}(#{definition})",
          " _ = #{to}",
          "  _.#{method}(#{definition})",
          "rescue NoMethodError => e",
          "  if _.nil? && e.name == :#{method}",
          "    #{exception}",
          "  else",
          "    raise",
          "  end",
          "end"
        ].join ";"
      end

      module_eval(method_def, file, line)
    end

    private(*method_names) if private
    method_names
  end

まずはじめにここまで見てみます

unless to
  raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end

toはdelegateメソッドの引数の為、引数toが偽の場合はArgumentErrorになるようにチェックしています

次を見ていきます

if prefix == true && /^[^a-z_]/.match?(to)
  raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end

if文内の正規表現 /^[^a-z_]/ は、preifxがtrueかつ引数toの先頭の文字列が英字と_以外だった場合にマッチします
こちらの条件に当たる場合は、エラーになります

※正規表現について

次にこちらを見ていきます

method_prefix = \
  if prefix
    "#{prefix == true ? to : prefix}_"
  else
    ""
  end

まず、こちらの書き方ですとif文内の戻り値が変数に格納されるという動きをします

変数 = if 〜 else 〜 end

これを踏まえて見ていきます
まず、prefixが真の場合です

"#{prefix == true ? to : prefix}_"

prefixがtrueの場合は、引数toに _ がついた文字列が戻り値になります
prefixがtrue以外の場合は、prefixに _ がついた文字列が戻り値となります

prefixが偽の場合は、空文字が戻り値となります

次を見ていきます

location = caller_locations(1, 1).first
file, line = location.path, location.lineno

caller_locationsがわからなかったので調べてみます

現在のフレームを Thread::Backtrace::Location の配列で返します。引 数で指定した値が範囲外の場合は nil を返します。
[PARAM] start:
開始フレームの位置を数値で指定します。
[PARAM] length:
取得するフレームの個数を指定します。
[PARAM] range:
取得したいフレームの範囲を示す Range オブジェクトを指定します。
[SEE_ALSO] Thread::Backtrace::Location, Kernel.#caller

引用:Ruby2.5.0リファレンスマニュアル#caller_locations

まだ、いまいち理解が進まないので、更に調べます

Kernel#caller_locations

スタックに積まれたメソッドの呼び出し元を配列として返してくれる。

  1 def fiz
  2   baz
  3 end
  4 
  5 def baz
  6   p caller_locations
  7 end
  8 
  9 fiz
% ruby caller_locations.rb
["caller_locations.rb:2:in `fiz'", "caller_locations.rb:9:in `
'"]

引用:Ruby 2.0の機能解説メモ(Kernel)

メソッドの呼び出し元を調べる為に使うメソッドのようです
今回はdelegateメソッド呼び出し元を調べているという感じです

location = caller_locations(1, 1).first
file, line = location.path, location.lineno

呼び出し元の情報を取得して、fileと行数をそれぞれ変数に格納しています

次を見てみます

to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)

引数toを文字列としています
次を見ると

if DELEGATION_RESERVED_METHOD_NAMES.include?(to)

toがDELEGATION_RESERVED_METHOD_NAMES に含まれているかチェックしています
定数の定義場所を見ているとこちらになります

class Module

省略

  RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
  else elsif END end ensure false for if in module next nil not or redo rescue retry
  return self super then true undef unless until when while yield)
  DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
  DELEGATION_RESERVED_METHOD_NAMES = Set.new(
    RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
  ).freeze

こちらに該当する文字列であった場合は、レシーバのメソッドが呼ばれるという動きになります

次の処理を見ていきます

method_names = methods.map do |method|
      # Attribute writer methods only accept one argument. Makes sure []=
      # methods still accept two arguments.
      definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"

      # The following generated method calls the target exactly once, storing
      # the returned value in a dummy variable.
      #
      # Reason is twofold: On one hand doing less calls is in general better.
      # On the other hand it could be that the target has side-effects,
      # whereas conceptually, from the user point of view, the delegator should
      # be doing one call.
      if allow_nil
        method_def = [
          "def #{method_prefix}#{method}(#{definition})",
          "_ = #{to}",
          "if !_.nil? || nil.respond_to?(:#{method})",
          "  _.#{method}(#{definition})",
          "end",
        "end"
        ].join ";"
      else
        exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")

        method_def = [
          "def #{method_prefix}#{method}(#{definition})",
          " _ = #{to}",
          "  _.#{method}(#{definition})",
          "rescue NoMethodError => e",
          "  if _.nil? && e.name == :#{method}",
          "    #{exception}",
          "  else",
          "    raise",
          "  end",
          "end"
        ].join ";"
      end

      module_eval(method_def, file, line)
    end

methodsはdelegateメソッドの引数です

まず最初の処理を見ていきます

# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"

method名に]が含まれていないメソッドが確認しています

次を見ていきます

# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
if allow_nil
  method_def = [
    "def #{method_prefix}#{method}(#{definition})",
    "_ = #{to}",
    "if !_.nil? || nil.respond_to?(:#{method})",
    "  _.#{method}(#{definition})",
    "end",
  "end"
  ].join ";"
else

allow_nilが真の場合は、method_def変数に配列内の文字列を ; で連結し、文字列として格納しています
method_def変数の文字列は動的にメソッド定義されるときに使われます
そのため、どのような処理のメソッドが定義されるのか見てみます

まず配列の最初はメソッドの定義部分です

"def #{method_prefix}#{method}(#{definition})",

例えば、prefixがhogeの場合は、 def hoge_huga(arg) みたいになります

次は、toをローカル変数 _ に格納しています

"_ = #{to}",

その次は、ローカル変数 _ がnilではない または、nilがmethodを実装している場合に真となります

"if !_.nil? || nil.respond_to?(:#{method})",

※ respond_to?について
instance method Object#respond_to? (Ruby 2.6.0)

真だった場合には、変数 _ にメソッド定義部分で使用した変数 methodと仮引数で使用したdefinitionを使い呼び出しています

"  _.#{method}(#{definition})"

例えば、prefixがhoge, methodがhugaの場合は、このようなメソッド def hoge_huga(arg) を定義することになるのですが、 内部での呼び出しは、このような形になります

 to.huga(arg)

次に、こちらを見ていきます

else
  exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")

  method_def = [
    "def #{method_prefix}#{method}(#{definition})",
    " _ = #{to}",
    "  _.#{method}(#{definition})",
    "rescue NoMethodError => e",
    "  if _.nil? && e.name == :#{method}",
    "    #{exception}",
    "  else",
    "    raise",
    "  end",
    "end"
  ].join ";"
end

こちらはさきほどとの違いはエラー処理が追加されているところです
exception変数にはエラーメッセージが配列で格納されています
さきほどと同じようにmethod_defには動的にメソッド定義する処理が記載されています

NoMethodErrorが発生し、delegateメソッドの引数toがnilかつエラーが発生したメソッド名がmethodと同じであれば、delegationErrorが発生するという処理が先程と異なっています

"rescue NoMethodError => e",
"  if _.nil? && e.name == :#{method}",
"    #{exception}",
"  else",
"    raise",
"  end",

これでmethod_def変数の戻り値の部分は見終わりました

次を見ていくと、ファイルと行番号を指定して、メソッド定義を動的に行っています

module_eval(method_def, file, line)

※ module_evalについて
instance method Module#class_eval (Ruby 2.6.0)

ブロック内は見終わったので、次を見ていきます

private(*method_names) if private
method_names

delegateメソッドの引数privateが真の場合は、method_namesで定義したメソッドをprivateに設定します
※ privateについて
instance method Module#private (Ruby 2.6.0)

Railsのソースコード読んでみる | Active Support parents編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照ください
shitake4.hatenablog.com

読んだ箇所

parents を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

parentsメソッドは、レシーバに対してparentを呼び出し、Objectに到着するまでパスをさかのぼります。連鎖したモジュールは、階層の下から上の順に配列として返されます。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z
 
X::Y::Z.parents # => [X::Y, X, Object]
M.parents       # => [X::Y, X, Object]

引用:RAILS GUIDES:parents

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def parents を探してみます

f:id:sktktk1230:20180920165902p:plain

2. 該当箇所が1箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > module > introspection.rb
# frozen_string_literal: true

require "active_support/inflector"

class Module

省略

  # Returns all the parents of this module according to its name, ordered from
  # nested outwards. The receiver is not contained within the result.
  #
  #   module M
  #     module N
  #     end
  #   end
  #   X = M::N
  #
  #   M.parents    # => [Object]
  #   M::N.parents # => [M, Object]
  #   X.parents    # => [M, Object]
  def parents
    parents = []
    if parent_name
      parts = parent_name.split("::")
      until parts.empty?
        parents << ActiveSupport::Inflector.constantize(parts * "::")
        parts.pop
      end
    end
    parents << Object unless parents.include? Object
    parents
  end
end

まず、parents 変数を空の配列で初期化しています

次に、if文内をみてみます

if parent_name
  parts = parent_name.split("::")
  until parts.empty?
    parents << ActiveSupport::Inflector.constantize(parts * "::")
    parts.pop
  end
end

分岐条件はparent_nameメソッドの戻り値が真の場合に処理が実行されるようです

※ parent_nameメソッドのコードリーディングについてはparentメソッドを読んだときに書きましたので、こちらを参照くださいRailsのソースコード読んでみる | Active Support parent編 - そういうこともある

parent_nameの戻り値は、親のモジュールの定数を戻り値とします
たとえば、このような戻り値となります

X::Y::Z.parent_name # => "X::Y"
M.parent_name       # => "X::Y"

真になるパターンは親のモジュールがある場合です
偽になるパターンは無名またはトップレベルの場合になります
parent_nameメソッドは無名またはトップレベルの場合に、戻り値がnilになるためです

さらに読み進めていきます
parts = parent_name.split("::") はparent_nameの戻り値の文字列を'::'を区切り文字として分割し、parts変数へ格納するという処理になります
parent_nameの戻り値が、"X::Y"だとするとsplit後の戻り値はこちらになります

"X::Y".split("::")
=> ['X', 'Y']

Ruby 2.5.0 リファレンスマニュアル#split

次にunless内の処理をみていきます

until parts.empty?
  parents << ActiveSupport::Inflector.constantize(parts * "::")
  parts.pop
end

until parts.empty? でparts配列が空になるまでuntil … endを繰り返し処理を行っています

Ruby 2.5.0 リファレンスマニュアル#until
Ruby 2.5.0 リファレンスマニュアル#empty

until内の処理をみてみます

parents << ActiveSupport::Inflector.constantize(parts * "::")
parts.pop

constantizeメソッドの引数 parts * "::" ですが、 配列partsの文字列要素の間に"::"を連結し、文字列を生成しています
Arry#* を調べてみると

self * sep -> String[permalink][rdoc]

指定された sep を間にはさんで連結した文字列を生成して返します。Array#join(sep) と同じ動作をします。

[PARAM] sep:

文字列を指定します。 文字列以外のオブジェクトを指定した場合は to_str メソッドによる暗黙の型変換を試みます。

p [1,2,3] * ","
# => "1,2,3"

引用:Ruby 2.5.0 リファレンスマニュアル#Array*

ActiveSupport::Inflector.constantizeは引数の文字列を定数に変換します
※ constantizeメソッドのコードリーディングについては、こちらを参照くださいRailsのソースコード読んでみる | Active Support constantize編 - そういうこともある

そしてその戻り値をメソッドの最初行で初期化した変数に入れています
parents << ActiveSupport::Inflector.constantize(parts * "::")

そして parts.pop で配列の末尾の要素を取り除いています

Ruby 2.5.0 リファレンスマニュアル#pop

ここまでuntil、if文の処理が読み終わりました

最後にparents変数にObjectが含まれているか確認し、なかった場合はObjectを追加しています
そして戻り値はレシーバの親の定数が含まれた配列を戻り値としています

parents << Object unless parents.include? Object
parents

Railsのソースコード読んでみる | Active Support constantize編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

constantize を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると

説明

引数の文字列で指定した名前で定数を探す


使い方

文字列.constantize()

'Module'.constantize     # => Module
'Test::Unit'.constantize # => Test::Unit

引用:Railsドキュメント:constantize

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def constantize を探してみます

f:id:sktktk1230:20180905141912p:plain

2. 該当箇所が2箇所ほどあったので、それぞれみてみます

1. activesupport > lib > active_support > core_ext > string > inflections.rb
# frozen_string_literal: true

require "active_support/inflector/methods"
require "active_support/inflector/transliterate"

# String inflections define new methods on the String class to transform names for different purposes.
# For instance, you can figure out the name of a table from the name of a class.
#
#   'ScaleScore'.tableize # => "scale_scores"
#
class String

省略

  # +constantize+ tries to find a declared constant with the name specified
  # in the string. It raises a NameError when the name is not in CamelCase
  # or is not initialized.  See ActiveSupport::Inflector.constantize
  #
  #   'Module'.constantize  # => Module
  #   'Class'.constantize   # => Class
  #   'blargle'.constantize # => NameError: wrong constant name blargle
  def constantize
    ActiveSupport::Inflector.constantize(self)
  end

省略

String クラスをオープンクラスして constantizeメソッドを実装しています

処理は ActiveSupport::Inflector.constantize へ移譲しているので、こちらをみてみます

2. activesupport > lib > active_support > inflector > methods.rb
省略

    # Tries to find a constant with the name specified in the argument string.
    #
    #   constantize('Module')   # => Module
    #   constantize('Foo::Bar') # => Foo::Bar
    #
    # The name is assumed to be the one of a top-level constant, no matter
    # whether it starts with "::" or not. No lexical context is taken into
    # account:
    #
    #   C = 'outside'
    #   module M
    #     C = 'inside'
    #     C                # => 'inside'
    #     constantize('C') # => 'outside', same as ::C
    #   end
    #
    # NameError is raised when the name is not in CamelCase or the constant is
    # unknown.
    def constantize(camel_cased_word)
      names = camel_cased_word.split("::".freeze)

      # Trigger a built-in NameError exception including the ill-formed constant in the message.
      Object.const_get(camel_cased_word) if names.empty?

      # Remove the first blank element in case of '::ClassName' notation.
      names.shift if names.size > 1 && names.first.empty?

      names.inject(Object) do |constant, name|
        if constant == Object
          constant.const_get(name)
        else
          candidate = constant.const_get(name)
          next candidate if constant.const_defined?(name, false)
          next candidate unless Object.const_defined?(name)

          # Go down the ancestors to check if it is owned directly. The check
          # stops when we reach Object or the end of ancestors tree.
          constant = constant.ancestors.inject(constant) do |const, ancestor|
            break const    if ancestor == Object
            break ancestor if ancestor.const_defined?(name, false)
            const
          end

          # owner is in Object, so raise
          constant.const_get(name, false)
        end
      end
    end

省略

freeze で文字列 :: を変更不可のオブジェクトとし、引数camel_cased_word:: でsplitしています

※ splitの仕様はこちら
instance method String#split (Ruby 2.6.0)

次にこちらをみてみます

# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?

引数をsplitした配列の要素が0のときに実行される処理です
※ Array#empty?の仕様はこちら
instance method Array#empty? (Ruby 2.6.0)

Object.const_get を調べてみます

name で指定される名前の定数の値を取り出します。


Module#const_defined? と違って Object を特別扱いすることはありません。


[PARAM] name:

定数名。String か Symbol で指定します。 完全修飾名を指定しなかった場合はモジュールに定義されている name で指定される名前の定数の値を取り出します。

[PARAM] inherit:

false を指定するとスーパークラスや include したモジュールで 定義された定数は対象にはなりません。

[EXCEPTION] NameError:

定数が定義されていないときに発生します。

module Bar
  BAR = 1
end
class Object
  include Bar
end
# Object では include されたモジュールに定義された定数を見付ける
p Object.const_get(:BAR)   # => 1

class Baz
  include Bar
end
# Object以外でも同様
p Baz.const_get(:BAR)      # => 1
# 定義されていない定数
p Baz.const_get(:NOT_DEFINED) #=> raise NameError
# 第二引数に false を指定すると自分自身に定義された定数から探す
p Baz.const_get(:BAR, false) #=> raise NameError
# 完全修飾名を指定すると include や自分自身へ定義されていない場合でも参照できる
p Class.const_get("Bar::BAR") # => 1

引用:Ruby 2.5.0 リファレンスマニュアル#const_get

nameで指定されている名前の定数を取り出す処理で、完全修飾名であれば、自分へ定義されていない場合でも参照できる処理になります
コメントを読んでみると、引数camel_cased_word が間違った名称だった場合に、NameErrorを発生させる為に使用しているようです

プルリクエストはこちらです
Fix #10932. Treat "" and "::" as invalid on constantize by killthekitten · Pull Request #10943 · rails/rails · GitHub

次にこちらを読みます

# Remove the first blank element in case of '::ClassName' notation.
names.shift if names.size > 1 && names.first.empty?

こちらの処理はさきほどのコミットで追加されています
::ClassNameをsplitするケースだと配列の先頭が空になる為、先頭の要素を取り除く処理を実行しています
f:id:sktktk1230:20180905142049p:plain

※ Array#shiftの仕様はこちら
instance method Array#shift (Ruby 2.6.0)

次にこちらをみていきます

names.inject(Object) do |constant, name|
    if constant == Object
        constant.const_get(name)
    else
        candidate = constant.const_get(name)
        next candidate if constant.const_defined?(name, false)
        next candidate unless Object.const_defined?(name)

        # Go down the ancestors to check if it is owned directly. The check
        # stops when we reach Object or the end of ancestors tree.
        constant = constant.ancestors.inject(constant) do |const, ancestor|
        break const    if ancestor == Object
        break ancestor if ancestor.const_defined?(name, false)
        const
        end

        # owner is in Object, so raise
        constant.const_get(name, false)
    end
end

まず inject の仕様を調べます

リストのたたみこみ演算を行います。


最初に初期値 init と self の最初の要素を引数にブロックを実行します。 2 回目以降のループでは、前のブロックの実行結果と self の次の要素を引数に順次ブロックを実行します。 そうして最後の要素まで繰り返し、最後のブロックの実行結果を返します。


要素が存在しない場合は init を返します。


初期値 init を省略した場合は、 最初に先頭の要素と 2 番目の要素をブロックに渡します。 また要素が 1 つしかなければブロックを実行せずに最初の要素を返します。 要素がなければブロックを実行せずに nil を返します。


[PARAM] init:

最初の result の値です。任意のオブジェクトが渡せます。

[PARAM] sym:

ブロックの代わりに使われるメソッド名を表す Symbol オブジェクトを指定します。 実行結果に対して sym という名前のメソッドが呼ばれます。

例:

# 合計を計算する。
p [2, 3, 4, 5].inject {|result, item| result + item }        #=> 14

# 自乗和を計算する。初期値をセットする必要がある。
p [2, 3, 4, 5].inject(0) {|result, item| result + item**2 }  #=> 54
この式は以下のように書いても同じ結果が得られます。

result = 0
[1, 2, 3, 4, 5].each {|v| result += v }
p result   # => 15

p [1, 2, 3, 4, 5].inject(:+)                    #=> 15
p ["b", "c", "d"].inject("abbccddde", :squeeze) #=> "abcde"

引用:Ruby 2.5.0 リファレンスマニュアル#inject

injectで初期化した値が、ブロックの最初の要素 constant となり、配列 names がブロックの2番目の要素 name に入るという感じになります
names.inject(Object) do |constant, name|

ブロックの内部の処理をみてみます

if constant == Object
    constant.const_get(name)
else

ブロックの引数 constant がObjectクラスである場合、.const_get で nameで指定されている名前の定数を取り出す処理を行っています
次のループではconstant.const_get(name) の戻り値がconstantとして扱われます

次にelse以降をみていきます

else
    candidate = constant.const_get(name)
    next candidate if constant.const_defined?(name, false)

candidate変数にnameで指定されている名前の変数を取り出しております
次の行をみてみます

const_defined? の処理を調べてみると

モジュールに name で指定される名前の定数が定義されている時真 を返します。


スーパークラスや include したモジュールで定義された定数を検索対象 にするかどうかは第二引数で制御することができます。


[PARAM] name:

String, Symbol で指定される定数名。

[PARAM] inherit:

false を指定するとスーパークラスや include したモジュールで 定義された定数は対象にはなりません。


module Kernel
  FOO = 1
end

# Object は include したモジュールの定数に対しても
# true を返す
p Object.const_defined?(:FOO)   # => true

module Bar
  BAR = 1
end
class Object
  include Bar
end
# ユーザ定義のモジュールに対しても同様
p Object.const_defined?(:BAR)   # => true

class Baz
  include Bar
end
# Object 以外でも同様になった
# 第二引数のデフォルト値が true であるため
p Baz.const_defined?(:BAR)      # => true

# 第二引数を false にした場合
p Baz.const_defined?(:BAR, false)   # => false

引用:Ruby 2.5.0 リファレンスマニュアル#const_defined?

constantに定義されている名前か確認している処理のようです。更に第二引数がfalseの為、スーパークラスや includeしたモジュールに定義された定数は対象外としています
constantに定義されている場合は、constantを戻り値とし、次のループとなっています

次の行をみてみます

next candidate unless Object.const_defined?(name)

Objectクラスに定義された定数名でない場合には、candidateをそのまま戻り値として次のループとなっています

次をみてみるとinject内でさらにinjectしている処理があります

省略

# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject(constant) do |const, ancestor|
    break const    if ancestor == Object
    break ancestor if ancestor.const_defined?(name, false)
    const
end

省略

継承チェーンの親要素までみて確認する処理のようです
ここの処理に行き着くconstantの条件は

  1. constantはObjectクラスではない
  2. constantオブジェクト自身に定義されている定数ではない(includeしているクラス, スーパークラスの定数は対象外)
  3. constantオブジェクトはObjectクラスに定義されている定数(Objectにincludeしているクラス, スーパークラスの定数も対象)

となっています

まず、ancestors メソッドを調べてみます

クラス、モジュールのスーパークラスとインクルードしているモジュール を優先順位順に配列に格納して返します。

module Foo
end
class Bar
  include Foo
end
class Baz < Bar
  p ancestors
  p included_modules
  p superclass
end
# => [Baz, Bar, Foo, Object, Kernel, BasicObject]
# => [Foo, Kernel]
# => Bar

引用:Ruby 2.5.0 リファレンスマニュアル#ancestors

constant.ancestors ではconstant変数のオブジェクトの継承チェーンを取り出しています

次にブロックの中をみてみます

break const    if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
const

継承チェーン内にObjectが含まれている場合は、constを戻り値とし、ループを終了しています 次の行をを見ると継承チェーンのオブジェクトancestorに定義された定数であった場合は、そのオブジェクトを戻り値とし、ループを終了します どの条件にもひっかからない場合は、更に継承チェーンをさかのぼって調べます

次をみていきます

# owner is in Object, so raise
constant.const_get(name, false)

さきほどの処理で取り出したconstantから定数を取り出しています

Railsのソースコード読んでみる | Active Support parent編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

parent を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると

parentメソッドは、名前がネストしたモジュールに対して実行でき、対応する定数を持つモジュールを返します。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z
 
X::Y::Z.parent_name # => "X::Y"
M.parent_name       # => "X::Y"

引用:RAILS GUIDES:parent

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def parent を探してみます

f:id:sktktk1230:20180905100624p:plain

2. 該当箇所が1箇所だったので、みてみます

1. activesupport > lib > active_support > core_ext > module > introspection.rb
省略

  # Returns the module which contains this one according to its name.
  #
  #   module M
  #     module N
  #     end
  #   end
  #   X = M::N
  #
  #   M::N.parent # => M
  #   X.parent    # => M
  #
  # The parent of top-level and anonymous modules is Object.
  #
  #   M.parent          # => Object
  #   Module.new.parent # => Object
  def parent
    parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
  end

省略

三項演算子 xxx ? yyy : zzz を使っています
parent_name の戻り値によって、
真の場合は ActiveSupport::Inflector.constantize(parent_name)
偽の場合は Object
となるようです

parent_name の実装をみてみます

# frozen_string_literal: true

require "active_support/inflector"

class Module
  # Returns the name of the module containing this one.
  #
  #   M::N.parent_name # => "M"
  def parent_name
    if defined?(@parent_name)
      @parent_name
    else
      parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
      @parent_name = parent_name unless frozen?
      parent_name
    end
  end

省略

Moduleクラスをオープンクラスし、parent_name メソッドを実装しています

まずは最初のif文をみてみると

if defined?(@parent_name)
  @parent_name
else

変数@parent_nameが定義されている場合はそのまま変数を戻り値とします
変数@parent_nameが定義されていない場合の処理は以下になります

else
  parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
  @parent_name = parent_name unless frozen?
  parent_name
end

こちらからみていきます
name =~ /::[^:]+\Z/

Moduleクラスのnameメソッドの仕様を調べてみると

モジュールやクラスの名前を文字列で返します。

このメソッドが返す「モジュール / クラスの名前」とは、 より正確には「クラスパス」を指します。 クラスパスとは、ネストしているモジュールすべてを 「::」を使って表示した名前のことです。 クラスパスの例としては「CGI::Session」「Net::HTTP」が挙げられます。

[RETURN]

名前のないモジュール / クラスに対しては nil を返します。

module A
  module B
  end

  p B.name  #=> "A::B"

  class C
  end
end

p A.name    #=> "A"
p A::B.name #=> "A::B"
p A::C.name #=> "A::C"

# 名前のないモジュール / クラス
p Module.new.name   #=> nil
p Class.new.name    #=> nil

引用:Ruby 2.5.0 リファレンスマニュアル#name

クラスパスを戻り値をするメソッドのようです

=~ も調べてみます

正規表現 other とのマッチを行います。 マッチが成功すればマッチした位置のインデックスを、そうでなければ nil を返します。

other が正規表現でも文字列でもない場合は other =~ self を行います。

このメソッドが実行されると、組み込み変数 $~, $1, ... にマッチに関する情報が設定されます。

[PARAM] other:

正規表現もしくは =~ メソッドを持つオブジェクト

[EXCEPTION] TypeError:

other が文字列の場合に発生します。

例

p "string" =~ /str/   # => 0
p "string" =~ /not/   # => nil
p "abcfoo" =~ /foo/   # => 3

引用:Ruby 2.5.0 リファレンスマニュアル#=~

正規表現にパターンマッチした情報を$1などに格納する処理でマッチした場合は、Integerクラスが戻り値となるようです

正規表現をみてみると ::[^:]+\Z/ ::ではじまり[^:]+ :にマッチしない1回以上の繰り返しを末尾とする文字にマッチするようです

三項演算子の真の場合の処理 $`.freeze をみてみます

現在のスコープで最後に成功した正規表現のパターンマッチでマッチした 部分より後ろの文字列です。 最後のマッチが失敗していた場合には nil となります。

Regexp.last_match.post_match と同じです。

この変数はローカルスコープかつスレッドローカル、読み取り専用です。 Ruby起動時の初期値は nil です。

例
str = '<p><a href="http://example.com">example.com</a></p>'
if %r[<a href="(.*?)">(.*?)</a>] =~ str
  p $'
end
#=> "</p>"

引用:Ruby 2.5.0 リファレンスマニュアル#variable $'

パターンマッチした部分より、後ろの文字列を戻り値とし、それを.freeze で変更不可としています

ここまでの処理をサンプルのクラスで実行し、動きをみてみました
f:id:sktktk1230:20180905100707p:plain

次に @parent_name = parent_name unless frozen? をみます

frozen? でレシーバが変更不可状態がチェックしています
理由がわからなかった為、コミットログから探してみます

f:id:sktktk1230:20180905100725p:plain

こちらのコミットが該当のコミットとイシューのようです

プルリクエスト
github.com

イシュー
github.com

イシューをみてみると
f:id:sktktk1230:20180905100750p:plain

オブジェクトをfreezeした状態でインスタンス変数に変更を加えてしまうとエラーとなる為、回避しているようです

frozen? をしている理由がわかったので、 unless の条件文内部の処理をみてみます
@parent_name = parent_name こちらの処理でインスタンス変数@parent_nameににいれることでキャッシュとして扱えるようにしています
2回以降の parent_name メソッドを呼ぶと 一度目の処理でインスタンス変数は初期化済みの為、if defined?(@parent_name) がtrueとなるので、else以降の処理は呼ばれず、インスタンス変数を返す処理になります

parent_nameメソッドの処理は読めたので、 parent メソッドに戻ります

省略

  # Returns the module which contains this one according to its name.
  #
  #   module M
  #     module N
  #     end
  #   end
  #   X = M::N
  #
  #   M::N.parent # => M
  #   X.parent    # => M
  #
  # The parent of top-level and anonymous modules is Object.
  #
  #   M.parent          # => Object
  #   Module.new.parent # => Object
  def parent
    parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
  end

省略

parent_nameの戻り値が真だった場合をみてみます

ActiveSupport::Inflector.constantizeを調べると

Tries to find a constant with the name specified in the argument string.

constantize('Module')   # => Module
constantize('Foo::Bar') # => Foo::Bar

The name is assumed to be the one of a top-level constant, no matter whether it starts with “::” or not. No lexical context is taken into account:

C = 'outside'
module M
  C = 'inside'
  C                # => 'inside'
  constantize('C') # => 'outside', same as ::C
end

引用:Ruby on Rails 5.2.1#ActiveSupport::Inflector#constantize

引数で指定した名前の定数を探すメソッドのようです

<追記>
constantizeもコードリーディングしてみました
blog.shitake4.tech

なので、ActiveSupport::Inflector.constantize(parent_name) は parent_nameの戻り値の定数を返します
偽の場合は、オブジェクトを戻り値としています

Railsのコミットでわからないものを調べてみた | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

日課で@y_yagiさんのrails commit log流し読みを読んでいるのですが、 コミットの内容を読んでみて、どうしてその修正でバグが直るのかわからないものがありました
そこで、その修正内容をしっかりと理解する為に調査したりしたので、どうやって理解するに至ったかなどを書いていきたいと思います
今回読んでいたエントリはこちらです

issue

github.com

該当PR

github.com

修正内容を抜粋

f:id:sktktk1230:20180822134705p:plain

2. PRを理解するためにやってみたこと

1. まず、PRのコメントを読んでみる

Fix merging relation that order including `?`

The `Relation::Merger` has a problem that order values would be merged
as nested array.

That was caused an issue #33664 since if array value is passed to
`order` and first element in the array includes `?`, the array is
regarded as a prepared statement and bind variables.

https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order

Just merging that as splat args like other values would fix the issue.

Fixes #33664.

ネストしたArrayで最初の値に'?'が含まれている場合、prepared statementとみなされてエラーとなるというバグとのことでした

issueには再現コードも書かれていました

github.com

どういったバグになるのかが何となく理解できたところで、コードを追ってみます

2. 変数の前に*をつけるとどんな動きなのか調べてみる

修正の対象になった箇所はこちらです

        def merge_multi_values
          if other.reordering_value
            # override any order specified in the original relation
            relation.reorder!(*other.order_values)
          elsif other.order_values.any?
            # merge in order_values from relation
            relation.order!(*other.order_values)
          end

          extensions = other.extensions - relation.extensions
          relation.extending!(*extensions) if extensions.any?
        end

Ruby * 等でGoogleで調べてみると、こちらの記事がヒットしたので、読んでみました alpaca.tc

変数の前に* を書くと、配列に変換してくれるようです
なんとなく修正内容を理解したところで、コードを読んでみます

3. relation.reorder! を読んでみる

rails/activerecord/lib/active_record/relation/query_methods.rb

# Same as #reorder but operates on relation in-place instead of copying.
def reorder!(*args) # :nodoc:
  preprocess_order_args(args)

  self.reordering_value = true
  self.order_values = args
  self
end

reorder! の引数をそのまま preprocess_order_argsに渡しているので、こちらの処理を追ってみます

4. preprocess_order_args を読む

rails/activerecord/lib/active_record/relation/query_methods.rb

def preprocess_order_args(order_args)
  order_args.map! do |arg|
    klass.sanitize_sql_for_order(arg)
  end
  order_args.flatten!

  @klass.enforce_raw_sql_whitelist(
    order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
    whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
  )

省略

end

引数を.map! して 配列の要素を sanitize_sql_for_order に渡しているようなので、こちらを読んでみます

5. sanitize_sql_for_order を読む

PRのコメントに書いてあったエラーの原因のメソッドまでいきつきました

That was caused an issue #33664 since if array value is passed to order and first element in the array includes ?, the array is regarded as a prepared statement and bind variables.

https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order

こちらのドキュメントを読んで、挙動を確認してみます

sanitize_sql_for_order(condition)

Accepts an array, or string of SQL conditions and sanitizes them into a valid SQL fragment for an ORDER clause.

sanitize_sql_for_order(condition)

sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
# => "field(id, 1,3,2)"

sanitize_sql_for_order("id ASC")
# => "id ASC"

引用:Ruby on Rails 5.2.1#sanitize_sql_for_order

そしてコードを読んでみると、

rails/activerecord/lib/active_record/sanitization.rb

def sanitize_sql_for_order(condition)
  if condition.is_a?(Array) && condition.first.to_s.include?("?")
    enforce_raw_sql_whitelist([condition.first],
      whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
    )

    # Ensure we aren't dealing with a subclass of String that might
    # override methods we use (eg. Arel::Nodes::SqlLiteral).
    if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
      condition = [String.new(condition.first), *condition[1..-1]]
    end

    Arel.sql(sanitize_sql_array(condition))
  else
    condition
  end
end

conditionが配列かつ要素の最初の値に'?'が含まれている場合に、prepared statementと判断し、処理をやってくれるみたいです

なので、ここまで読んでみて推測すると、正しい挙動では引数conditionは こちらの条件

 if condition.is_a?(Array) && condition.first.to_s.include?("?")

には該当せず、

else
  condition
end

こちらになるのが正しい挙動のようです

ここまで読んでみて仮設を立てるとpreprocess_order_args(order_args) の引数が

  • バグのケースだと [['?', '!']] が引数となり、
  • 正しいケースだと ['?', '!'] となるのではないかなと思いました

しかしなぜこちらのパターン[['?', '!']] になるのかわからず困っていたのですが、

会社の先輩に相談したところこちらの記事を教えていただきました qiita.com

変数の前に* を書くと、配列に変換してくれるではなく、 可変長引数を受け取るメソッドに、配列を渡す際に変数に* をつけないともう一回り配列で包まれてしまうというこちらの挙動ではないかとのことでした

なので、こちらの修正を行うことで f:id:sktktk1230:20180822134705p:plain

other.order_values の戻り値の配列がさらにもうひと回り配列で包まれることが無くなるということだと思われます
本当に戻り値が配列なのか確認するために、さらにコードを読んでいきます

6. other.order_values の戻り値を確認してみる

order_valuesの定義箇所を探してみようと思い def other.order_values でgrepしてみてもヒットしませんでした

activerecord/lib/active_record/relation/merger.rb ファイルの中を見てみると、lock_value, create_with_valueなどのメソッドがあり、こちらも定義箇所が見つからなかったので、define_method などでsuffixに_valueを設定し、動的にメソッド定義しているのではないかと予想し探し方を変えてみました

Merger クラス内で other を初期化している箇所を探してみると(RubyMineで⌘+B)

こちらにいきつきました

activerecord/lib/active_record/relation/merger.rb

省略

    class Merger # :nodoc:
      attr_reader :relation, :values, :other

      def initialize(relation, other)
        @relation = relation
        @values   = other.values
        @other    = other
      end

省略

Mergeクラスを初期化する際に引数でotherを入れているので、初期化処理の箇所がないか調べてみました f:id:sktktk1230:20180823105415p:plain

こちらを見てみるとotherはRelationクラスの可能性がありそうです

rails/activerecord/lib/active_record/relation/spawn_methods.rb

省略

def merge!(other) # :nodoc:
      if other.is_a?(Hash)
        Relation::HashMerger.new(self, other).merge
      elsif other.is_a?(Relation)
        Relation::Merger.new(self, other).merge
      elsif other.respond_to?(:to_proc)
        instance_exec(&other)
      else
        raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
      end
    end

省略

こちらのファイルを見てみると、

activerecord/lib/active_record/relation.rb

# frozen_string_literal: true

module ActiveRecord
  # = Active Record \Relation
  class Relation
    MULTI_VALUE_METHODS  = [:includes, :eager_load, :preload, :select, :group,
                            :order, :joins, :left_outer_joins, :references,
                            :extending, :unscope]

    SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
                            :reverse_order, :distinct, :create_with, :skip_query_cache]

    CLAUSE_METHODS = [:where, :having, :from]
    INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]

    VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS

    include Enumerable
    include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation

定数の中にorder とあるので、この定数を利用してメソッド定義しているのではないかと予想しました MULTI_VALUE_METHODS で検索すると こちらでしか利用してなさそうなので、 ruby VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS activerecord/lib/active_record配下で、 VALUE_METHODSを使用しているところを探してみます

f:id:sktktk1230:20180823110148p:plain

こちらで定義していました

rails/activerecord/lib/active_record/relation/query_methods.rb

省略

    Relation::VALUE_METHODS.each do |name|
      method_name = \
        case name
        when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
        when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
        when *Relation::CLAUSE_METHODS then "#{name}_clause"
        end
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{method_name}                   # def includes_values
          get_value(#{name.inspect})         #   get_value(:includes)
        end                                  # end

        def #{method_name}=(value)           # def includes_values=(value)
          set_value(#{name.inspect}, value)  #   set_value(:includes, value)
        end                                  # end
      CODE
    end

省略

動的にメソッドを定義し、メソッド内部では get_value を呼び出しているので、こちらの定義箇所を読んでみます

    # Returns a relation value with a given name
    def get_value(name) # :nodoc:
      @values.fetch(name, DEFAULT_VALUES[name])
    end

fetchの第二引数を指定すると、キーが存在しない場合、デフォルト値を返すようになっているので、DEFAULT_VALUES を探してみます

DEFAULT_VALUES ハッシュにはorderが定義されていないのですが

      DEFAULT_VALUES = {
        create_with: FROZEN_EMPTY_HASH,
        where: Relation::WhereClause.empty,
        having: Relation::WhereClause.empty,
        from: Relation::FromClause.empty
      }

      Relation::MULTI_VALUE_METHODS.each do |value|
        DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
      end

こちらで追加していました

      Relation::MULTI_VALUE_METHODS.each do |value|
        DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
      end

配列をループしている内部の処理は、左辺が未定義または偽の場合に右辺の値を代入するという意味になります
Rubyの||=というイディオムは左辺が未定義または偽なら代入の意味 -- ぺけみさお

なので配列の要素のこちらをひとつずつ取り出しながら

    MULTI_VALUE_METHODS  = [:includes, :eager_load, :preload, :select, :group,
                            :order, :joins, :left_outer_joins, :references,
                            :extending, :unscope]

未定義の場合は、FROZEN_EMPTY_ARRAYを追加していくという処理です
DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY

FROZEN_EMPTY_ARRAYは空の配列になります

    FROZEN_EMPTY_ARRAY = [].freeze

なので、other.order_values の戻り値は配列になるということでした

今回のPRの修正、reoader! メソッドの引数に * をつけることで配列で包まれることなく、配列を渡せるようになったというのが修正内容でした

Railsで許可するハッシュキーを設定する | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいての学びを書いてみます

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

PRの中の処理に書かれていた.assert_valid_keys ってどんな処理か

対象のPR

github.com

記述内容

def _define_before_model_callback(klass, callback)
  klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
    options.assert_valid_keys :if, :unless, :prepend
    set_callback(:"#{callback}", :before, *args, **options, &block)
  end
end

4. 調べてみた

assert_valid_keys が分からなかった為、どんなメソッドなのか調べてみました

Validates all keys in a hash match *valid_keys, raising ArgumentError on a mismatch.

Note that keys are treated differently than HashWithIndifferentAccess, meaning that string and symbol keys will not match.

{ name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
{ name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
{ name: 'Rob', age: '28' }.assert_valid_keys(:name, :age)   # => passes, raises nothing

引用:Ruby on Rails 5.2.0

assert_valid_keys の引数に入れた値がレシーバのkeyに存在するかチェックするメソッドです
存在しない場合は、ArgumentErrorをraiseする仕様です
シンボルと文字列は区別します またoptionsハッシュにassert_valid_keys の引数に設定した値のうち1つしか含んでいない場合

f:id:sktktk1230:20180724134134p:plain

となります

5. さきほどのソースコードを読んでみる

さきほどの記述の assert_valid_keys の部分を見てみると

options.assert_valid_keys :if, :unless, :prepend

optionsに設定した値にif, unless, prepend 以外のキーが存在しないかチェックしており、それ以外がある場合は、ArgumentErrorをraiseします

Hash#fetchでブロックを記述した場合の挙動 | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいての学びを書いてみます

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

  1. PRの中の処理に書かれていたHash#fetchにブロックを渡すとどうなるか
def log_to_stdout?
  options.fetch(:log_to_stdout) do
    options[:daemon].blank? && environment == "development"
  end
end

4. PRを読んでみる

対象のPR

github.com

1. どんな修正内容?

railties/lib/rails/commands/server/server_command.rbの修正です。
rails serverでlogをstdoutに出力するかどうかをrails serverコマンドの引数で指定出来るようにしています。
rails serverコマンドに--no-log-to-stdoutを引数に指定した場合、developmentでもlogがstdoutに出力されないようになっています。
引用:rails commit log流し読み(2018/07/09)

5. PR読んでてわからない部分調べてみた

1. Hash#fetch にブロックを渡すとどんな挙動になる?

fetch(key, default = nil) {|key| ... } -> object[permalink][rdoc]
key に関連づけられた値を返します。該当するキーが登録されてい ない時には、引数 default が与えられていればその値を、ブロッ クが与えられていればそのブロックを評価した値を返します。
fetchはハッシュ自身にデフォルト値が設定されていても単に無視します(挙動に変化がありません)。
引用:Ruby 2.5.0 リファレンスマニュアル Hash#fetch

ハッシュにkeyが存在しない場合は、ブロックの評価値が戻り値となるようです

実際の挙動を確認してみました

f:id:sktktk1230:20180710113633p:plain

ブロックの評価値trueが戻り値となりました

ここまでを踏まえ、さきほどのPRの内容を見てみると、

def log_to_stdout?
  options.fetch(:log_to_stdout) do
    options[:daemon].blank? && environment == "development"
  end
end

optionsに log_to_stdout が設定されていない場合、options[:daemon]がblankかつ環境がdevelopmentだとログ出力をするという処理になります