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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

3.2.3 モジュール属性

mattr_reader、mattr_writer、mattr_accessorという3つのマクロは、クラス用に定義されるcattr_*マクロと同じです。実際、cattr_*マクロは単なるmattr_*マクロの別名です。クラス属性も参照してください。

たとえば、これらのマクロは以下のDependenciesモジュールで使用されています。

module ActiveSupport
  module Dependencies
    mattr_accessor :warnings_on_first_load
    mattr_accessor :history
    mattr_accessor :loaded
    mattr_accessor :mechanism
    mattr_accessor :load_paths
    mattr_accessor :load_once_paths
    mattr_accessor :autoloaded_constants
    mattr_accessor :explicitly_unloadable_constants
    mattr_accessor :logger
    mattr_accessor :log_activity
    mattr_accessor :constant_watch_stack
    mattr_accessor :constant_watch_stack_mutex
  end
end  

引用:RAILS GUIDE#モジュール属性

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

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

f:id:sktktk1230:20180709124745p:plain

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

1. activesupport > lib > active_support > core_ext > module > attribute_accessors.rb
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)
   syms.each do |sym|
     raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
     class_eval(<<-EOS, __FILE__, __LINE__ + 1)
       @@#{sym} = nil unless defined? @@#{sym}
       def self.#{sym}
         @@#{sym}
       end
     EOS

     if instance_reader && instance_accessor
       class_eval(<<-EOS, __FILE__, __LINE__ + 1)
         def #{sym}
           @@#{sym}
         end
       EOS
     end

     sym_default_value = (block_given? && default.nil?) ? yield : default
     class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?
   end
 end
 alias :cattr_reader :mattr_reader

まずここまでの処理を見てみます

syms.each do |sym|
  raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)

symsはsymbolsを短縮した名前で、変数に入る内容は、シンボルの配列ではないかと推測できます
unless 以下の正規表現 /\A[_A-Za-z]\w*\z/ を見てみると
\A は文字列の先頭にマッチ
[_A-Za-z] は指定された文字のどれかに一致
\w は英単語を構成する文字(a~z,A~Z,_,1~9)
* は0回以上の繰り返しにマッチ
\z は文字列の末尾にマッチ

上記の条件に当てはまるシンボルにマッチするかを検査しています
例えば、_hoge, Huga1 などがマッチします

これにマッチしない場合は NameError 例外を発生させるということになります

正規表現について詳しくは以下を参照

qiita.com

docs.ruby-lang.org

マッチした場合は、以下の処理が実行されます

class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  @@#{sym} = nil unless defined? @@#{sym}
  def self.#{sym}
    @@#{sym}
  end
EOS

class_evalにより引数のヒアドキュメントの内容をクラス内でそのまま実行することになります  

モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。

モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。

ただし、ローカル変数は module_eval/class_eval の外側のスコープと共有します。

文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。

class C
end
a = 1
C.class_eval %Q{
  def m                   # メソッドを動的に定義できる。
    return :m, #{a}
  end
}

p C.new.m        #=> [:m, 1]

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

ヒアドキュメントの処理をそれぞれ見ていきます
@@#{sym} = nil unless defined? @@#{sym} では変数symのクラス変数が自クラスに定義されているかを確認しています
定義されていない場合は、クラス変数にnilを代入しています

そして、こちらではクラス変数のアクセッサ(クラスメソッド)を定義しています

def self.#{sym}
  @@#{sym}
end

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

if instance_reader && instance_accessor
  class_eval(<<-EOS, __FILE__, __LINE__ + 1)
    def #{sym}
      @@#{sym}
    end
  EOS
end

instance_reader && instance_accessor こちらはメソッドの引数の値によって、判定されます。(デフォルトはどちらもtrueの為、実行されます)

  def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)

こちらの記述は先程のクラス変数のアクセッサとほぼ同一ですが、こちらはクラスメソッドではなく、インスタンスメソッドになります

class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  def #{sym}
    @@#{sym}
  end
EOS

クラスメソッド、インスタンスメソッドの定義の方法については以下参照

qiita.com

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

sym_default_value = (block_given? && default.nil?) ? yield : default
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?

ブロックが与えられているかつメソッドの引数defaultがnilの場合に、ブロックの返り値をsym_default_value変数に代入しています
それ以外の場合に、メソッドの仮引数のdefaultの値を代入しています

sym_default_valueがnilでないときに、クラス変数に値を代入しています

mattr_readerにはエイリアスも設定されており、cattr_readerも同じ処理を行うことができます
alias :cattr_reader :mattr_reader

to_symを使わずに文字列からシンボルを生成 | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

概要

日課のrails commit log流し読みを読んでいて、文字列からシンボルに変換する方法が to_sym 以外にあることがわかったので、備忘録も兼ねて書いてみました

対象のコミット

github.com

to_sym を使わずに、シンボルを作る

今までは "hoge".to_sym で文字列からシンボル作っていたのですが、

to_symメソッドまたはinternメソッドは、文字列に対応するシンボル(Symbolオブジェクト)を返します。

s = "hello"
p s.to_sym
s = "symbol with spaces"
p s.to_sym

引用:Rubyリファレンス#to_sym,intern

:"hoge" でもシンボルを作れるようです

f:id:sktktk1230:20180529104606p:plain

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

あるクラスで属性を定義すると、後にそのクラスのサブクラスが作成されるときに名前が衝突するリスクが生じます。これはライブラリにおいては特に重要な問題です。

Active Supportでは、attr_internal_reader、attr_internal_writer、attr_internal_accessorというマクロが定義されています。これらのマクロは、Rubyにビルトインされているattr_*と同様に振る舞いますが、内部のインスタンス変数の名前が衝突しにくいように配慮される点が異なります。

attr_internalマクロはattr_internal_accessorと同義です。

# ライブラリ
class ThirdPartyLibrary::Crawler
  attr_internal :log_level
end

# クライアントコード
class MyCrawler < ThirdPartyLibrary::Crawler
  attr_accessor :log_level
end

引用:RAILS GUIDES#内部属性

サブクラスに定義した名前が衝突してしまうケースを発生させない為の機能になります

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

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

f:id:sktktk1230:20180509155702p:plain

2. attr_internal関連の処理がまとまってそうなファイルが見つかったので、みてみます

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

alias_method :attr_internal, :attr_internal_accessor

省略

20行目でalias_methodを使用し、attr_internal_accessorattr_internal という別名を付け、こちらでも呼び出せるようにしています

alias_methodメソッドは、クラスやモジュールのメソッドに別名を付けます。引数には新しい別名new_name、元のメソッド名original_nameを順に指定します。戻り値はクラスやモジュール自身です。

別名を付けると、通常のメソッド呼び出しのように別名でメソッドを呼び出せるようになります。

class Cat
  def message() "meow" end

  alias_method :welcome, :message
end

cat = Cat.new
puts cat.welcome

引用:Rubyリファレンス#alias_method

attr_internal_accessor の処理を見てみます

省略

# Declares an attribute reader and writer backed by an internally-named instance
# variable.
def attr_internal_accessor(*attrs)
  attr_internal_reader(*attrs)
  attr_internal_writer(*attrs)
end

省略

内部でそれぞれ処理を呼んでいるだけのメソッドになります

# frozen_string_literal: true

class Module
  # Declares an attribute reader backed by an internally-named instance variable.
  def attr_internal_reader(*attrs)
    attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
  end

  # Declares an attribute writer backed by an internally-named instance variable.
  def attr_internal_writer(*attrs)
    attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
  end

省略

それぞれの処理を見てみると、受け取った引数(属性名の配列)を eachで1つずつ取り出し attr_internal_define に渡している処理になります
attr_internal_define という名前から推測すると内部属性の読み取り、書き取りを定義している処理で引数の :reader:writer で読み取り用、書き込み用の処理を動的に定義しているのではと思われます。

attr_internal_define を見てみます

省略

private

省略

  def attr_internal_define(attr_name, type)
    internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, "")
    # use native attr_* methods as they are faster on some Ruby implementations
    send("attr_#{type}", internal_name)
    attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
    alias_method attr_name, internal_name
    remove_method internal_name
  end
end

attr_internal_ivar_name(attr_name) の戻り値から @ を取り除いて、internal_name 変数に入れています。
次のsend("attr_#{type}", internal_name) ではtypeがreaderである場合はインスタンス変数の読み込み用メソッド、 writerである場合は、書き込み用メソッドを定義しています

次の行ではtypeがwriterである場合は、新しく定義するアクセッサ−をattr_name 変数へ、内部で使用するアクセッサーを internal_name へ入れています
そして、alias_method で新しく定義するアクセッサーへと変更しています
そしてメソッドの説明であったメソッド名がサブクラスで衝突するのを避ける為

あるクラスで属性を定義すると、後にそのクラスのサブクラスが作成されるときに名前が衝突するリスクが生じます。これはライブラリにおいては特に重要な問題です。

に、 remove_methodsend("attr_#{type}", internal_name) で定義したアクセッサーを削除しています

読んでみて

ライブラリを作る時にアクセッサーを定義した場合には、なるべく使うようにしないとバグを生み出す可能性がかなり高くなりそうだと感じました

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESを見てみると

3.2 属性

3.2.1 alias_attribute

モデルの属性には、リーダー (reader)、ライター (writer)、述語 (predicate) があります。上に対応する3つのメソッドを持つ、モデルの属性の別名 (alias) を一度に作成することができます。他の別名作成メソッドと同様、1つ目の引数には新しい名前、2つ目の引数には元の名前を指定します (変数に代入するときと同じ順序、と覚えておく手もあります)。

class User < ActiveRecord::Base
  # emailカラムを"login"という名前でも参照したい
  # そうすることで認証のコードがわかりやすくなる
  alias_attribute :login, :email
end

引用:RAILS GUIDES

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

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

f:id:sktktk1230:20180423192006p:plain

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

1. activesupport > lib > active_support > core_ext > moudle > aliasing.rb
# frozen_string_literal: true

class Module
  # Allows you to make aliases for attributes, which includes
  # getter, setter, and a predicate.
  #
  #   class Content < ActiveRecord::Base
  #     # has a title attribute
  #   end
  #
  #   class Email < Content
  #     alias_attribute :subject, :title
  #   end
  #
  #   e = Email.find(1)
  #   e.title    # => "Superstars"
  #   e.subject  # => "Superstars"
  #   e.subject? # => true
  #   e.subject = "Megastars"
  #   e.title    # => "Megastars"
  def alias_attribute(new_name, old_name)
    # The following reader methods use an explicit `self` receiver in order to
    # support aliases that start with an uppercase letter. Otherwise, they would
    # be resolved as constants instead.
    module_eval <<-STR, __FILE__, __LINE__ + 1
      def #{new_name}; self.#{old_name}; end          # def subject; self.title; end
      def #{new_name}?; self.#{old_name}?; end        # def subject?; self.title?; end
      def #{new_name}=(v); self.#{old_name} = v; end  # def subject=(v); self.title = v; end
    STR
  end
end

module_eval メソッドに対して ヒアドキュメントで文字列(def〜STRの上の行まで)、__FILE____LINE__ + 1を渡しています

※ヒアドキュメントとは?
Rubyのヒアドキュメント 4パターンのまとめ -- ぺけみさお

第2引数、第3引数に記述されている __FILE__, __LINE__ +1 がわからなかったので、調べてみました

__FILE__

現在のソースファイル名

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


__LINE__

現在のソースファイル中の行番号

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

引用:#疑似変数

疑似変数と呼ばれる特殊な変数のようです。

実は nil,true,false も疑似変数でそれぞれのクラス(NilClass, TrueClass, FalseClass)の唯一のインスタンスが格納されているようです

module_evalを調べてみると

モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。

モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。

ただし、ローカル変数は module_eval/class_eval の外側のスコープと共有します。

文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。

[PARAM] expr:

評価される文字列。

[PARAM] fname:

文字列を指定します。ファイル fname に文字列 expr が書かれているかのように実行されます。 スタックトレースの表示などを差し替えることができます。

[PARAM] lineno:

文字列を指定します。行番号 lineno から文字列 expr が書かれているかのように実行されます。 スタックトレースの表示などを差し替えることができます。

例:

class C
end
a = 1
C.class_eval %Q{
  def m                   # メソッドを動的に定義できる。
    return :m, #{a}
  end
}

p C.new.m        #=> [:m, 1]

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

つまり、moduleに動的にでセッターメソッド、ゲッターメソッドと真偽値を返すメソッドを定義しているということになります

qiita.com

読んでみて

__FILE____LINE__ はコード読んでいるとちょくちょく出てきてたので、そこは気にせず、読んでましたが、ちゃんと調べてみると新たな発見があったので、 ソースコードをしっかりと読み込むことは大切だと感じました

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

述語in?は、あるオブジェクトが他のオブジェクトに含まれているかどうかをテストします。渡された引数がinclude?に応答しない場合はArgumentError例外が発生します。

in?の例を示します。

1.in?([1,2])        # => true
"lo".in?("hello")   # => true
25.in?(30..50)      # => false
1.in?(1)            # => ArgumentError  

引用:RAILS GUIDES:in?

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

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

f:id:sktktk1230:20180403185950p:plain

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

1. activesupport > lib > active_support > core_ext > object > inclusion.rb
class Object
  # Returns true if this object is included in the argument. Argument must be
  # any object which responds to +#include?+. Usage:
  #
  #   characters = ["Konata", "Kagami", "Tsukasa"]
  #   "Konata".in?(characters) # => true
  #
  # This will throw an +ArgumentError+ if the argument doesn't respond
  # to +#include?+.
  def in?(another_object)
    another_object.include?(self)
  rescue NoMethodError
    raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
  end

省略

end

引数(another_object)に対してinclude?を実行しています

include?を調べてみます

array.include?(obj)

include?メソッドは、配列の要素に引数objが含まれていればtrue、なければfalseを返します。要素と引数objが同じかどうかの比較には==メソッドが使われます。

animals = ["dog", "cat", "mouse"]
puts animals.include?("cat")
puts animals.include?("elephant")
true
false

引用:Rubyリファレンス#include?(Array)

str.include?(other_str)

include?メソッドは、文字列の中に引数other_strの文字列が含まれるかどうかを調べます。含まれるときはtrue、含まれないときはfalseを返します。

s = "Supercalifragilisticexpialidocious"
puts s.include?("exp")
true

引用:Rubyリファレンス#include?(String)

mod.include?(other_mod)

include?メソッドは、クラスやモジュールが引数のモジュールother_modをインクルードしているかどうかを調べます。インクルードしていればtrueを、そうでなければfalseを返します。

親クラスがインクルードしているモジュールを指定してもtrueになります。

p String.include?(Comparable)
p String.include?(Enumerable)
p String.include?(Kernel)
true
true  (Ruby 1.9ではfalse)
true

引用:Rubyリファレンス#include?(Module)

rubyのinclude?の実装は3つあるみたいです(String, Array, Module)
another_object.include?(self) は引数のオブジェクトがレシーバに対してinclude?かどうかをチェックしています

そして、メソッドが実装されていない場合には、 ArgumentError を返すという処理になっていました

rescue NoMethodError
  raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
end

読んでみて

include? はよく使用していますが、in?はあまり馴染みがなかったので、挙動を学べたので、うまく使い分けなど出来るといいなと思います

わからないこと調べてみた | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいてわからなかったこと調べてみました

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

  1. PRの中の処理に書かれていたrack_app < Rails::Engine とは何だろう?

4. PRを読んでみる

対象のPR

github.com

1. どんな修正内容?

Make engine check more explicit
actionpack/lib/action_dispatch/routing/endpoint.rbの修正です。
Routing::Endpoint#engine?メソッドでappがengineかどうかをチェックするのに、routesメソッドが定義されているかどうかで判定していたのを、appかRails::Engineの子クラスかどうかで判定するよう修正しています。
engineではないがroutesメソッドが定義されているケースがある(e.g. Grape APIのendpoint)為。
引用:rails commit log流し読み(2018/03/25)

2. 対象のPRの内容

f:id:sktktk1230:20180327113224p:plain

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

1. rack_app < Rails::Engine とは何だろう?

記述箇所は該当PRの下記部分になります

def engine?
  rack_app.is_a?(Class) && rack_app < Rails::Engine
end

特定のクラスを継承しているかチェックしている処理とのことでした

※ 調べてみたら@semind氏のQiitaに記事がありました

qiita.com

実際の挙動を確認してみました
ChildクラスがParentクラスを継承している場合

f:id:sktktk1230:20180327113240p:plain

継承関係にあったので、戻り値がtrueになっています
つまり、 def engine? の処理は

rack_app.is_a?(Class) && rack_app < Rails::Engine

rack_app がクラスであり、かつ Rails::Engine を継承しているかを判定している処理でした

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

ブロックが継続する間$VERBOSEの値を変更し、その後リセットします。

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }

suppressメソッドを使用すると例外の発生を止めることもできます。このメソッドは、例外クラスを表す任意の数値を受け取ります。suppressは、あるブロックの実行時に例外が発生し、その例外が(kind_of?による判定で)いずれかの引数に一致する場合、それをキャプチャして例外を発生せずに戻ります。一致しない場合、例外はキャプチャされません。

# ユーザーがロックされていればインクリメントは失われるが、重要ではない
suppress(ActiveRecord::StaleObjectError) do
  current_user.increment! :visits
end

引用:RAILS GUIDES#2.13 警告・例外の抑制

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

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

f:id:sktktk1230:20180323135822p:plain

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

1. activesupport > lib > active_support > core_ext > kernel > reporting.rb
# frozen_string_literal: true

module Kernel
  module_function

  # Sets $VERBOSE to +nil+ for the duration of the block and back to its original
  # value afterwards.
  #
  #   silence_warnings do
  #     value = noisy_call # no warning voiced
  #   end
  #
  #   noisy_call # warning voiced
  def silence_warnings
    with_warnings(nil) { yield }
  end

省略

end

with_warnings メソッドにブロックを渡してます。 そのブロック内で yield しているので、元は silence_warnings に渡したブロックです
たとえば、

silence_warnings { puts 'ブロック実行' }

このように書いた場合は、 {}の内容がwith_warnings へそのままブロックとして渡すという感じになります

※ ブロックの詳しい説明は@kidach1氏のこちらの記事を参考にしてください qiita.com

silence_warnings の処理を見てみます

省略

# Sets $VERBOSE for the duration of the block and back to its original
# value afterwards.
def with_warnings(flag)
  old_verbose, $VERBOSE = $VERBOSE, flag
  yield
ensure
  $VERBOSE = old_verbose
end

省略

仮引数flag には nil が入ります

次に変数の初期化処理  old_verbose, $VERBOSE = $VERBOSE, flag で、$VERBOSE がどんなものなのかわからなかったので、調べてみます

冗長メッセージフラグです。Rubyインタプリタへの コマンドラインオプション -v でセットされます。
警告レベルは三段階あり、それぞれ以下の通りです。

  • nil
    • 警告を出力しない
  • false
    • 重要な警告のみ出力 (デフォルト)
  • true
    • すべての警告を出力する

$VERBOSE に nil, false 以外を代入すると値は true になります。
$VERBOSE の値はコマンドラインオプション -W でも設定できます。 -W0 オプションで nil、 -W1 オプションで false、 -W2, -W オプションで true が設定されます。 -v オプションや -w オプションを指定した場合は true が設定されます。
$VERBOSE はグローバルスコープです。
引用:Ruby2.5.0リファレンスマニュアル#variable $-v

with_warningsメソッド内では

  1. ブロック実行前に $VERBOSE へ nilを入れることで警告を出力しないモードへと変更
  2. yield でブロックを実行
  3. ブロック実行後、もともと設定されていた$VERBOSE へ設定し直すという処理になります

読んでみて

普段は使っていない機能だったので、どんな挙動するのか手探りで読んでました。使い所がまだ理解できていないので、このような機能があることを覚えておき、使えるようにしておくのが、良さそうだと思いました

わからないこと調べてみた | rails commit log流し読み(2018/03/15)

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいてわからなかったこと調べてみました

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

  1. PRの中の処理に書かれていたZlib::Deflate.deflate とは何だろう?
  2. PRの中の処理に書かれていたMarshal.dump とは何だろう?

4. PRを読んでみる

対象のPR

github.com

1. どんな修正内容?

activesupport/lib/active_support/cache.rb の修正です。
ActiveSupport::Cache::Entry で保存するオブジェクトを複数回marshalしていたのを、値をキャッシュして複数回marshalしないよう修正しています。
引用:rails commit log流し読み(2018/03/15)

2. 複数回実施しているところは?

initialize 内の処理でshould_compress?がtrueだった場合、should_compress?compress で2回 Marshal.dump(@value) が実行されている為、should_compress? 実行時にインスタンス変数に格納し、 compress 実行時にはそのインスタンス変数内のデータを再度利用することで、複数回実施しないようにしているようです

変更箇所
  1. def should_compress? f:id:sktktk1230:20180316135525p:plain
  2. def compress f:id:sktktk1230:20180316135528p:plain
  3. def marshaled_value f:id:sktktk1230:20180316135532p:plain

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

1. Zlib::Deflate.deflate とは何だろう?

string を圧縮します。level の有効な値は Zlib::NO_COMPRESSION, Zlib::BEST_SPEED, Zlib::BEST_COMPRESSION, Zlib::DEFAULT_COMPRESSION 及び 0 から 9 の整数です。

ちなみに、このメソッドは以下のコードとほぼ同じです:

require 'zlib'

def deflate(string, level)
  z = Zlib::Deflate.new(level)
  dst = z.deflate(string, Zlib::FINISH)
  z.close
  dst
end

引用:Ruby 2.5.0 リファレンスマニュアル class Zlib::Deflate

文字列を圧縮する際に使うメソッドのようです

実際の挙動を確認してみました
f:id:sktktk1230:20180316135617p:plain

ちなみに、zlib自体はRubyの機能ではなく

gzipのメイン開発者が、"Deflate"アルゴリズム処理部分だけを緩いライセンスで実装したライブラリ。Linux/UNIXだけでなく、Windowsなど幅広いプラットフォームに移植されている。
引用:技術/歴史/zip,gzip,zlib,bzip2

ということのようです

その後、gzipの主要開発者であるJean-Loup Gailly と Mark Adler らは"Deflate"圧縮アルゴリズム処理だけをライブラリとして実装し、"zlib"として商用利用も可能な緩いライセンスで公開する。
引用:技術/歴史/zip,gzip,zlib,bzip2

2. Marshal.dump とは何だろう?

obj を指定された出力先に再帰的に出力します。

ファイルに書き出せないオブジェクトをファイルに書き出そうとすると 例外 TypeError が発生します。 ファイルに書き出せないオブジェクトは以下の通りです。

  • 名前のついてない Class/Module オブジェクト。(この場 合は、例外 ArgumentError が発生します。無名クラスについて は、Module.new を参照。)
  • システムがオブジェクトの状態を保持するもの。具体的には以下のイン スタンス。Dir, File::Stat, IO とそのサブクラス File, Socket など。
  • MatchData, Data, Method, UnboundMethod, Proc, Thread, ThreadGroup, Continuation のインスタンス。
  • 特異メソッドを定義したオブジェクト
  • また、これらのオブジェクトを間接的に指すオブジェクトなども書き出せ ません。例えば、デフォルト値を求めるブロックを持った Hash は Proc を間接的に指していることになります。
p Marshal.dump(Hash.new {})
=> -:1:in `dump': cannot dump hash with default proc (TypeError)

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

実際の挙動を確認してみました
f:id:sktktk1230:20180316135628p:plain

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

instance_variable_namesメソッドは配列を返します。配列のインスタンス名には"@"記号が含まれます。

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end

C.new(0, 1).instance_variable_names # => ["@x", "@y"]  

引用:RAILS GUIDES:instance_variable_names

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

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

f:id:sktktk1230:20180228174817p:plain

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

1. activesupport > lib > active_support > core_ext > object > instance_variables.rb
# frozen_string_literal: true

class Object

省略

  # Returns an array of instance variable names as strings including "@".
  #
  #   class C
  #     def initialize(x, y)
  #       @x, @y = x, y
  #     end
  #   end
  #
  #   C.new(0, 1).instance_variable_names # => ["@y", "@x"]
  def instance_variable_names
    instance_variables.map(&:to_s)
  end
end

まず、 instance_variables.map() を見てます

instance_variables を調べてみると

instance_variablesメソッドは、レシーバのオブジェクトが持っているインスタンス変数の名前を配列に入れて返します。

Ruby 1.9 Ruby 1.8では配列中の変数名は文字列ですが、Ruby 1.9ではシンボルになります。

class Book
  def initialize(title, price)
    @title = title; @price = price
  end
end
book = Book.new("Programming Ruby", 2000)
p book.instance_variables

["@title", "@price"] (Ruby 1.8の場合)

[:@title, :@price] (Ruby 1.9の場合)

引用:Rubyリファレンス:instance_variables

インスタンス変数の名前を配列で取得しています。そしてその配列に対して map を実行しています

mapは

mapメソッドは、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。collectメソッドの別名です。

numbers = ["68", "65", "6C", "6C", "6F"]
p numbers.map {|item| item.to_i(16) }

[104, 101, 108, 108, 111]

引用:Rubyリファレンス:map

ブロックの戻り値を集めて配列にするメソッドです

次に .map(&:to_s) を見てみます

&: は配列の各要素に対して to_s を実行しているということです
たとえば、

[1, 2, 3].map(&:to_s)
=> ["1", "2", "3"]

ということです
※詳しい解説は@kasei-san氏のこちらの記事がわかりやすいかと思います
qiita.com

つまり、def instance_variable_names はレシーバのインスタンス変数名を配列として取り出すメソッドになります

読んでみて

今回は、前回書いた instance_values を読んでいたので、すんなりと読めました shitake4.hatenablog.com

Rubyでメソッドチェーンを書く為に覚えておきたいメソッド | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいてわからなかったこと調べてみました

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

PRの中の処理に書かれていた.tap ってなんだろう?

対象のPR

github.com

記述内容

if value.blank? && options[:prompt]
  tag_options = { value: "" }.tap do |prompt_opts|
    prompt_opts[:disabled] = true if options[:disabled] == ""
    prompt_opts[:selected] = true if options[:selected] == ""
  end
  option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
 end

4. 調べてみた

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

obj.tap {|myself| block }

tapメソッドは、ブロック変数にレシーバ自身を入れてブロックを実行します。戻り値はレシーバ自身です。メソッドチェーンの中にtapメソッドをはさみ込み、ソースコードを簡潔にする目的で使われます。

次の例は、文字列中の文字のコードを10進数と16進数で表示するものです。

arr = "hello".bytes.to_a
p arr
arr = arr.collect {|byte| byte.to_s(16) }
p arr

この例は、tapメソッドを使えば次のように書けます。

"hello".bytes.to_a.tap {|arr| p arr }
  .collect {|byte| byte.to_s(16) }.tap {|arr| p arr }

[104, 101, 108, 108, 111]

["68", "65", "6c", "6c", "6f"]

引用:Rubyリファレンス:tap

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

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

tag_options = { value: "" }.tap do |prompt_opts|
    prompt_opts[:disabled] = true if options[:disabled] == ""
    prompt_opts[:selected] = true if options[:selected] == ""
  end

ブロック引数の prompt_optstap のレシーバである { value: ""} となります
options[:disabled] == "" が真の場合は、 { value: ""}disabled: true が追加されます
options[:selected] == "" が真の場合は、 { value: ""}selected: true が追加されます

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

instance_valuesメソッドはハッシュを返します。インスタンス変数名から"@"を除いたものがハッシュのキーに、インスタンス変数の値がハッシュの値にマップされます。キーは文字列です。

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end
C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}

引用:RAILS GUIDES:instance_values

インスタンス変数を簡単に取り出して使うメソッドです

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

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

f:id:sktktk1230:20180223175611p:plain

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

1. activesupport > lib > active_support > core_ext > object > instance_values.rb
# frozen_string_literal: true

class Object
  # Returns a hash with string keys that maps instance variable names without "@" to their
  # corresponding values.
  #
  #   class C
  #     def initialize(x, y)
  #       @x, @y = x, y
  #     end
  #   end
  #
  #   C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
  def instance_values
    Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }]
  end

まず、 Hash[] を調べてみます

Hashクラスのクラスメソッドは、新しいハッシュ(Hashクラスのインスタンス)を返します。の中に[キー1, 値1, キー2, 値2, ...]のようにオブジェクトを並べると、それが新しいハッシュのキーと値になります。

[]内のオブジェクトの数が奇数のときは、例外ArgumentErrorが発生します。

movie = Hash[:title, "Alien", :director, "Ridley Scott", :year, 1979]
puts movie[:title]
puts movie[:year]

Alien

1979

引用:Rubyリファレンス:[]

ハッシュクラスのインスタンスを生成するクラスメソッドになります

次に、どんな値を元にハッシュ生成しているのか見るため instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] } を順を追って見てみたいと思います

まず、instance_variables.map {} です

instance_variables を調べてみると

instance_variablesメソッドは、レシーバのオブジェクトが持っているインスタンス変数の名前を配列に入れて返します。

Ruby 1.9 Ruby 1.8では配列中の変数名は文字列ですが、Ruby 1.9ではシンボルになります。

class Book
  def initialize(title, price)
    @title = title; @price = price
  end
end
book = Book.new("Programming Ruby", 2000)
p book.instance_variables

["@title", "@price"] (Ruby 1.8の場合)

[:@title, :@price] (Ruby 1.9の場合)

引用:Rubyリファレンス:instance_variables

インスタンス変数の名前を配列で取得しています。そしてその配列に対して map を実行しています

mapは

mapメソッドは、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。collectメソッドの別名です。

numbers = ["68", "65", "6C", "6C", "6F"]
p numbers.map {|item| item.to_i(16) }

[104, 101, 108, 108, 111]

引用:Rubyリファレンス:map

ブロックの戻り値を集めて配列にするメソッドです

次にmapに渡しているブロック部分 { |name| [name[1..-1], instance_variable_get(name)] } を見てみます

name[1..-1] はインスタンス変数名から部分文字列を取得している処理です

レシーバがStringクラスだった場合は、次のとおりです

文字列の中から部分文字列を取り出すメソッドです。s[2]、s[3,5]、s[2..7]、s[/[0-9]/] のように、いろいろな形で利用できます。配列要素の取り出しのように記述しますが、実際にはメソッド呼び出しです。[]の中はメソッドの引数です。


省略


引数に範囲を指定すると、その範囲に対応する部分文字列を返します。範囲外の位置を指定すると、nilが返ります。

s = "hello, world"
puts s[7..10]   # 7文字目から10文字目まで
puts s[7...10]  # 7文字目から10文字目まで、10文字目は含まない

worl

wor

開始位置と終了位置がマイナスの場合は、文字列の末尾から数えます(-1が末尾から1番目、-2が末尾から2番目、...)。

s = "hello, world"
puts s[-5..-1]  # 末尾から5文字目..末尾から1文字目まで

world

引用:Rubyリファレンス:[] (String)

name[1..-1] は2文字目から末尾1文字目までを取得しています
たとえば、 "@title"であれば title が取得できます

レシーバがシンボルだった場合にも同様です

rangeで指定したインデックスの範囲に含まれる部分文字列を返します。


(self.to_s[range] と同じです。)


[PARAM] range:

取得したい文字列の範囲を示す Range オブジェクトを指定します。

:foo[0..1] # => "fo"

[SEE_ALSO] String#[], String#slice

引用:Ruby2.5.0リファレンス:Symbol

次に、instance_variable_get(name) を見てみます

instance_variable_getメソッドは、レシーバが持っているインスタンス変数の値を返します。引数nameにはインスタンス変数の名前を:@titleや"@title"のようにシンボルか文字列で渡します。

定義されていない変数名を渡すとnilが返ります。:titleのようにインスタンス変数と見なされない名前を渡すと例外NameErrorが発生します。

class Book
  def initialize(title)
    @title = title
  end
end
book = Book.new("Programming Ruby")
p book.instance_variable_get(:@title)
p book.instance_variable_get(:@price)

"Programming Ruby"

nil

引用:Ruby2.5.0リファレンス:Symbol

インスタンス変数名を引数に渡すとその値が取れるという動きです

ブロックの中で行っている処理は、 配列の先頭にインスタンス変数名、次にインスタンス変数の値をセットしている処理です

たとえば、 { |name| [name[1..-1], instance_variable_get(name)] } の name"@title" の場合であれば、 ["title", "@titleに入ってた値"] ということになります

ここまでをまとめて考えてみると、Hash[] の引数に入る値は、

[
  ["インスタンス変数名1", "インスタンス変数の値1"],
  ["インスタンス変数名2", "インスタンス変数の値2"],
  ["インスタンス変数名3", "インスタンス変数の値3"]
]

になります

Hash[] を調べてみると

新しいハッシュを生成します。 引数は必ず偶数個指定しなければなりません。奇数番目がキー、偶数番目が値になります。

このメソッドでは生成するハッシュにデフォルト値を指定することはできません。 Hash.newを使うか、Hash#default=で後から指定してください。

[PARAM] key_and_value:

生成するハッシュのキーと値の組です。必ず偶数個(0を含む)指定しなければいけません。

[EXCEPTION] ArgumentError:

奇数個の引数を与えたときに発生します。

以下は配列からハッシュを生成する方法の例です。


省略


(2) キーと値のペアの配列からハッシュへ

alist = [[1,"a"], [2,"b"], [3,["c"]]]
p Hash[*alist.flatten(1)]  # => {1=>"a", 2=>"b", 3=>["c"]}

引用:Ruby2.5.0リファレンス:Hash.[]

さきほどの配列をHash[] すると

{
  "インスタンス変数名1" => "インスタンス変数の値1",
  "インスタンス変数名2" => "インスタンス変数の値2",
  "インスタンス変数名3" => "インスタンス変数の値3"
}

となります

読んでみて

インスタンス変数も利用がしやすいようにこんなメソッドもあるんだというのが学びでした

メソッドが定義されているか確認したい場合のdefined? | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいてわからなかったこと調べてみました

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

PRの中の処理に書かれていたdefined? ってなんだろう?

対象のPR

github.com

記述内容

def config_target_version
  defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f
end

4. 調べてみた

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

文法:
defined?

式が定義されていなければ、偽を返します。定義されていれば式の種別 を表す文字列を返します。

定義されていないメソッド、undef されたメソッド、Module#remove_method により削除されたメソッドのいずれに対しても defined? は偽を返します。

特別な用法として以下があります。

defined? yield

yield の呼び出しが可能なら真(文字列 "yield")を返します。 Kernel.#block_given? と同様にメソッドがブロック付きで呼ばれたか を判断する方法になります。

defined? super

super の実行が可能なら真(文字列 "super")を返します。

defined? a = 1
p a # => nil

引用:Rubyリファレンス2.5.0:defined?

式が定義されているかを確認するメソッドのようです
式が定義されていない場合は、nilを戻り値とします
定義されている場合の戻り値は以下となります

以下は、defined? が返す値の一覧です。

  • "super"
  • "method"
  • "yield"
  • "self"
  • "nil"
  • "true"
  • "false"
  • "assignment"
  • "local-variable"
  • "local-variable(in-block)"
  • "global-variable"
  • "instance-variable"
  • "constant"
  • "class variable"
  • "expression"

引用:Rubyリファレンス2.5.0:defined?

5. ちょっと思ったこと

こんな書き方

def config_target_version
  @config_target_version || Rails::VERSION::STRING.to_f
end

もありなのかなと思ったのですが、 defined? は式が定義されている場合のみ戻り値があるので、 少し挙動が変わってしまうので、同じ動きはしないので、これはダメそうでした

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

2.11 JSON support

Active Supportが提供するto_jsonメソッドの実装は、通常json gemがRubyオブジェクトに対して提供しているto_jsonよりも優れています。その理由は、HashやOrderedHash、Process::Statusなどのクラスでは、正しいJSON表現を提供するために特別な処理が必要になるためです。
引用:RAILS GUIDES:Active Support コア拡張機能:JSON

to_json についてはこちらの記事を参照ください

shitake4.hatenablog.com

ここからは上記記事の続きになります

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

as_jsonはどんな処理をしているのか見てみます

1. activesupport > lib > active_support > core_ext > object > json.rb
1. class Object
class Object
  def as_json(options = nil) #:nodoc:
    if respond_to?(:to_hash)
      to_hash.as_json(options)
    else
      instance_values.as_json(options)
    end
  end
end

レシーバがrespond_to?(:to_hash) できる場合であれば、レシーバをハッシュにし、as_jsonしています
そうでなければ、 instance_valuesを呼んでいます

instance_variablesメソッドは、レシーバのオブジェクトが持っているインスタンス変数の名前を配列に入れて返します。
引用:Rubyリファレンス」instance_values

レシーバのインスタンス変数を持った配列に対して as_json しています

オブジェクトに対する as_json はハッシュか配列に変換して実行するということになります

2. class Hash
class Hash
  def as_json(options = nil) #:nodoc:
    # create a subset of the hash by applying :only or :except
    subset = if options
      if attrs = options[:only]
        slice(*Array(attrs))
      elsif attrs = options[:except]
        except(*Array(attrs))
      else
        self
      end
    else
      self
    end

    Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
  end
end
subset = if options

省略

else
  self
end

でoptionsが偽の場合はレシーバがsubsetに代入されます
真の場合は、if文内の最終評価値が、入ります

if options内をみてみます

if attrs = options[:only]
  slice(*Array(attrs))
elsif attrs = options[:except]
  except(*Array(attrs))
else
  self
end

if attrs = options[:only]でattrsに代入し、attrsが真であれ場合 slice(*Array(attrs)) が実行されます

Hash.slice はactive support の拡張機能になります

Slice a hash to include only the given keys. Returns a hash containing the given keys.

{ a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# => {:a=>1, :b=>2}

This is useful for limiting an options hash to valid keys before passing to a method:

def search(criteria = {})
  criteria.assert_valid_keys(:mass, :velocity, :time)
end
search(options.slice(:mass, :velocity, :time))

If you have an array of keys you want to limit to, you should splat them:

valid_keys = [:mass, :velocity, :time]
search(options.slice(*valid_keys))

引用:apidock:Ruby on Rails:slice

レシーバから引数に指定した値と一致するキーを含んだハッシュを戻り値とします

*Array(attrs) で可変長引数として配列で初期化し、引数に渡しています

この部分の処理はオプション only で指定したキーのみをレシーバから取り出す処理になります

次に elsif attrs = options[:except] を見てみます

さきほどと似ている記述です
exceptもactive supoprtの拡張機能であり、sliceとは違い引数に指定した値をレシーバから除外し、ハッシュを戻り値とする処理です

Returns a hash that includes everything but the given keys.

hash = { a: true, b: false, c: nil}
hash.except(:c) # => { a: true, b: false}
hash # => { a: true, b: false, c: nil}

This is useful for limiting a set of parameters to everything but a few known toggles:

@person.update(params[:person].except(:admin))

引用:apidock:Ruby on Rails:except

上記2パターンに当てはまらない場合は、レシーバが返ります

次に、こちらをみてみます

    Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]

処理を順番に見てみます
Hash[] はハッシュインスタンスの作成処理です

Hashクラスのクラスメソッドは、新しいハッシュ(Hashクラスのインスタンス)を返します。の中に[キー1, 値1, キー2, 値2, ...]のようにオブジェクトを並べると、それが新しいハッシュのキーと値になります。
[]内のオブジェクトの数が奇数のときは、例外ArgumentErrorが発生します。
引用:Rubyリファレンス:Hash

subset.map { |k, v| } はsubset(さきほどのif文内で戻り値ハッシュ) に対して map でkey, valueを取り出しています

続いて [k.to_s, options ? v.as_json(options.dup) : v.as_json] を見てみます

配列の先頭にkeyをto_sしたものをいれています
配列の2番目に三項演算子 options ? v.as_json(options.dup) : v.as_json の戻り値が入ります

つまり、ここでの処理はハッシュsubsetのキー、バリューそれぞれに対して as_json し、再度ハッシュ生成しています

3. class Array
class Array
  def as_json(options = nil) #:nodoc:
    map { |v| options ? v.as_json(options.dup) : v.as_json }
  end
end

レシーバである配列の各要素に対してas_json しています

4. class Struct
class Struct #:nodoc:
  def as_json(options = nil)
    Hash[members.zip(values)].as_json(options)
  end
end

ここまでHash[members.zip(values)]見てみます
まずHash[] はハッシュの初期化処理です

membersメソッドを調べてみると、

構造体のメンバの名前(文字列)の配列を返します。

Foo = Struct.new(:foo, :bar)
p Foo.new.members  # => ["foo", "bar"]

[注意] 本メソッドの記述は Struct の下位クラスのインスタンスに対して呼び 出す事を想定しています。Struct.new は Struct の下位クラスを作成する点に 注意してください。

引用:Ruby2.5.0:Struct

zipメソッドを調べてみると

zipメソッドは、配列の要素を引数の配列other_arrayの要素と組み合わせ、配列の配列を作成して返します。transposeメソッドで[array, other_array, ...].transposeとしたときと同じく、行と列を入れ替えます。ただし、transposeメソッドと違って足りない要素はnilで埋められ、余分な要素は捨てられます。

arr1 = [1, 2, 3]
arr2 = [4, 5]
arr3 = [6, 7, 8, 9]
p arr1.zip(arr2, arr3)
 [[1, 4, 6], [2, 5, 7], [3, nil, 8]]

引用:Rubyリファレンス:zip

valuesメソッドを調べてみると

構造体のメンバの値を配列にいれて返します。

例えば以下のようにして passwd のエントリを出力できます。
require 'etc'
print Etc.getpwuid.values.join(":"), "\n"

引用:Ruby2.5.0:Struct

ここまでをまとめると、構造体からメンバー名とメンバーの値を取り出しハッシュへと変換しています
そしてそのハッシュに対し as_json しています

5. class TrueClass
class TrueClass
  def as_json(options = nil) #:nodoc:
    self
  end
end
6. class FalseClass
class FalseClass
  def as_json(options = nil) #:nodoc:
    self
  end
end
7. class NilClass
class NilClass
  def as_json(options = nil) #:nodoc:
    self
  end
end
8. class String
class String
  def as_json(options = nil) #:nodoc:
    self
  end
end
9. class Symbol
class Symbol
  def as_json(options = nil) #:nodoc:
    to_s
  end
end
10. class Numeric
class Numeric
  def as_json(options = nil) #:nodoc:
    self
  end
end
11. class Float
class Float
  # Encoding Infinity or NaN to JSON should return "null". The default returns
  # "Infinity" or "NaN" which are not valid JSON.
  def as_json(options = nil) #:nodoc:
    finite? ? self : nil
  end
end

finite? を調べてみると

数値が ∞, -∞, あるいは NaN でない場合に true を返します。 そうでない場合に false を返します。
引用:Ruby2.5.0リファレンスマニュアル:finite?

数値として問題がない場合にレシーバを返すようです

12. class BigDecimal
class BigDecimal
  # A BigDecimal would be naturally represented as a JSON number. Most libraries,
  # however, parse non-integer JSON numbers directly as floats. Clients using
  # those libraries would get in general a wrong number and no way to recover
  # other than manually inspecting the string with the JSON code itself.
  #
  # That's why a JSON string is returned. The JSON literal is not numeric, but
  # if the other end knows by contract that the data is supposed to be a
  # BigDecimal, it still has the chance to post-process the string and get the
  # real value.
  def as_json(options = nil) #:nodoc:
    finite? ? to_s : nil
  end
end

こちらは数値として問題ないか確認後文字列へ変換しています

13. class Regexp
def as_json(options = nil) #:nodoc:
  to_s
end

レシーバをそのまま文字列へ変換しています

14. module Enumerable
def as_json(options = nil) #:nodoc:
  to_a.as_json(options)
end

module Enumerable を調べてみると

繰り返しを行なうクラスのための Mix-in。このモジュールの メソッドは全て each を用いて定義されているので、インクルード するクラスには each が定義されていなければなりません。
引用:Ruby 2.5.0:Enumerable

mapやeach_with_indexなどがインスタンスメソッドとして定義されているクラスです
rubyでハッシュや配列を使う場合に、便利なメソッドが定義されています

to_a.as_json(options) ですので、レシーバを配列に変換し、as_json しています

15. class IO
class IO
  def as_json(options = nil) #:nodoc:
    to_s
  end
end

レシーバをそのまま文字列へ変換しています

16. class Range
class Range
  def as_json(options = nil) #:nodoc:
    to_s
  end
end

レシーバをそのまま文字列へ変換しています

17. class Time
class Time
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
    end
  end
end

まず ActiveSupport::JSON::Encoding.use_standard_json_time_format を見てみます

module ActiveSupport

  省略

  module JSON

    省略

    module Encoding #:nodoc:
      class JSONGemEncoder #:nodoc:

        省略

      end

      class << self
        # If true, use ISO 8601 format for dates and times. Otherwise, fall back
        # to the Active Support legacy format.
        attr_accessor :use_standard_json_time_format

        省略

        # Sets the precision of encoded time values.
        # Defaults to 3 (equivalent to millisecond precision)
        attr_accessor :time_precision

        省略

      end

      self.use_standard_json_time_format = true

      省略

      self.time_precision = 3

アクセッサに use_standard_json_time_format が定義されており、デフォルトでtrueに設定されています
その為、 Timeクラスの as_json 内では基本的に xmlschema(ActiveSupport::JSON::Encoding.time_precision) が実行されそうです

ActiveSupport::JSON::Encoding.time_precision も同じようにアクセッサが用意されております
デフォルト値は3です

time_precisionを調べてみると

@y_yagi氏のrails commit log流し読み(2014/07/10)で仕様変更の経緯が解説されていました y-yagi.hatenablog.com

time_precisionはミリ秒の桁数のようです ActiveSupport::JSON::Encoding.time_precision = 4に設定してみると

f:id:sktktk1230:20180206172934p:plain

ミリ秒の桁数が4桁になりました

xmlschema を調べてみると

XML Scheme (date) による書式の文字列を返します。
引用:Ruby2.5.0リファレンス:Date#xmlschema

文字列を返すメソッドのようです

次に if ActiveSupport::JSON::Encoding.use_standard_json_time_format が偽の場合を見てみます

else
  %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end

まず%()を調べてみると

ダブルクオートで囲う場合と同等。
引用:Rubyで%記法(パーセント記法)を使う

ダブルクオートと同じようです

strftimeは引数のフォーマット文字列に従って、レシーバを変換し戻り値とします

時刻を format 文字列に従って文字列に変換した結果を返します。
引用:Ruby2.5.0リファレンス:Time#strftime

今回の引数の値の記号の意味はこちらになります

  • %Y…西暦を表す数
  • %m…月を表す数字(01-12)
  • %d…日(01-31)
  • %H…24時間制の時(00-23)
  • %M…分(00-59)
  • %S…秒(00-60) (60はうるう秒)

次にformatted_offset(false) を見てみます

active_support > core_ext > time > conversions.rb
class Time

  省略

  # Returns a formatted string of the offset from UTC, or an alternative
  # string if the time zone is already UTC.
  #
  #   Time.local(2000).formatted_offset        # => "-06:00"
  #   Time.local(2000).formatted_offset(false) # => "-0600"
  def formatted_offset(colon = true, alternate_utc_string = nil)
    utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
  end

utc? && alternate_utc_string はUTCなのか、またはalternate_urc_stringを設定しているかを判定しています
設定している場合は戻り値が設定値になります

f:id:sktktk1230:20180206173019p:plain

utc? && alternate_utc_string の判定が偽の場合を見てみます

ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon) は世界標準時間からの差を出力します

※UTCについてはこちらの記事がわかりやすいかと思います
www.724685.com

 

18. class Date
class Date
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      strftime("%Y-%m-%d")
    else
      strftime("%Y/%m/%d")
    end
  end
end

if ActiveSupport::JSON::Encoding.use_standard_json_time_format は 17. class Timeを参照してください
レシーバを文字列変換する処理をしています
strftimeは同様に 17. class Time を参照してください

19. class DateTime
class DateTime
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      strftime("%Y/%m/%d %H:%M:%S %z")
    end
  end
end
  1. class Timeと処理が似ています
    use_standard_json_time_format が真の場合は、 time_precision で指定した桁数でミリ秒で文字列へ変換し戻り値とします
    偽の場合も同様に文字列に変換しています
20. class URI::Generic
class URI::Generic #:nodoc:
  def as_json(options = nil)
    to_s
  end
end
21. class Pathname
class Pathname #:nodoc:
  def as_json(options = nil)
    to_s
  end
end
22. class Process::Status
class Process::Status #:nodoc:
  def as_json(options = nil)
    { exitstatus: exitstatus, pid: pid }
  end
end

Process::Status クラスがどのようなものか調べてみると

プロセスの終了ステータスを表すクラスです。 メソッド Process.#wait2 などの返り値として使われます。
引用:Ruby2.5.0:class Process::Status

exitstatus

exited? が真の場合プロセスが返した終了ステータスの整数を、そ うでない場合は nil を返します。
引用:Ruby2.5.0:class Process::Status

pid

終了したプロセスのプロセス ID を返します。
引用:Ruby2.5.0:class Process::Status

それぞれの値をハッシュにし戻り値としています

23. class Exception
class Exception
  def as_json(options = nil)
    to_s
  end
end

読んでみて

JSON化するために様々なクラスをオープンクラスしていたので、Railsを使わずにJSONの処理をするときには気をつけないと思っていない挙動することがありそうだと思いました

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

2.11 JSON support

Active Supportが提供するto_jsonメソッドの実装は、通常json gemがRubyオブジェクトに対して提供しているto_jsonよりも優れています。その理由は、HashやOrderedHash、Process::Statusなどのクラスでは、正しいJSON表現を提供するために特別な処理が必要になるためです。
引用:RAILS GUIDES:Active Support コア拡張機能:JSON

JSON を見てみます

どんな使い方だっけ?

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

Returns a JSON string representing the hash. Without any options, the returned JSON string will include all the hash keys. For example:

  { :name => "Konata Izumi", 'age' => 16, 1 => 2 }.to_json
  # => {"name": "Konata Izumi", "1": 2, "age": 16}  

引用:API dock:to_json

レシーバに対して to_jsonすると、JSON文字列を返すという処理です

thinkit.co.jp

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

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

f:id:sktktk1230:20180201183238p:plain

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

1. activesupport > lib > active_support > core_ext > object > json.rb
1. module ActiveSupport::ToJsonWithActiveSupportEncoder
省略

module ActiveSupport
  module ToJsonWithActiveSupportEncoder # :nodoc:
    def to_json(options = nil)
      if options.is_a?(::JSON::State)
        # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
        super(options)
      else
        # to_json is being invoked directly, use ActiveSupport's encoder
        ActiveSupport::JSON.encode(self, options)
      end
    end
  end
end

メソッド to_json のみ実装されたモジュールです さらに読み進めてみます

省略

[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass|
  klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)
end

reverse_each の挙動を調べてみます

reverse_eachメソッドは、配列の要素の数だけブロックを繰り返し実行します。繰り返しごとにブロック引数には各要素が末尾から逆順に入ります。戻り値はレシーバ自身です。
animals = ["dog", "cat", "mouse"]
animals.reverse_each {|anim| puts anim }
mouse  
cat  
dog

引用:Rubyリファレンス:reverse_each

Enumerableから順番に klass へ各要素が入ります

次にブロックの中

klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)

を見てみます

prependがわからないので、調べてみると

指定したモジュールを self の継承チェインの先頭に「追加する」ことで self の定数、メソッド、モジュール変数を「上書き」します。
継承チェイン上で、self のモジュール/クラスよりも「手前」に 追加されるため、結果として self で定義されたメソッドは override されます。
modules で指定したモジュールは後ろから順に処理されるため、 modules の先頭が最も優先されます。
また、継承によってこの「上書き」を処理するため、prependの引数として 渡したモジュールのインスタンスメソッドでsuperを呼ぶことで self のモジュール/クラスのメソッドを呼び出すことができます。
実際の処理は modules の各要素の prepend_features を後ろから順に呼びだすだけです。 Module#prepend_features が継承チェインの改変を実行し、結果として上のような 処理が実現されます。そのため、prepend_features を override することで prepend の処理を追加/変更できます。
引用:Ruby2.5.0:prepend

配列のクラス [Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable] にモジュールを追加して to_json をオーバーライドしてます
再度、ActiveSupport::ToJsonWithActiveSupportEncoderを見てみると

def to_json(options = nil)
  if options.is_a?(::JSON::State)
    # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
    super(options)
  else
    # to_json is being invoked directly, use ActiveSupport's encoder
    ActiveSupport::JSON.encode(self, options)
  end
end

optionに ::JSON::State を指定した場合には、super(options) により継承元の to_json を呼んでいます
それ以外は ActiveSupport::JSON.encode(self, options) となります

encodeの処理を見てみます

2. activesupport > lib > active_support > json > encoding.rb
module JSON
  # Dumps objects in JSON (JavaScript Object Notation).
  # See http://www.json.org for more info.
  #
  #   ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
  #   # => "{\"team\":\"rails\",\"players\":\"36\"}"
  def self.encode(value, options = nil)
    Encoding.json_encoder.new(options).encode(value)
  end

引数valueに入るのは to_json した際のレシーバです

次に Encoding.json_encoderまでみてみます

module ActiveSupport

省略

  module JSON

省略

    module Encoding #:nodoc:

省略

    self.json_encoder = JSONGemEncoder
    self.time_precision = 3
    end

module Encoding内でjson_encoderにJSONGemEncoderクラスをセットしています

JSONGemEncoderクラスのinitializerでは引数のオプションをインスタンス変数に入れています

module Encoding #:nodoc:
  class JSONGemEncoder #:nodoc:
    attr_reader :options

    def initialize(options = nil)
      @options = options || {}
    end

ここまでみると Encoding.json_encoder.new(options) の戻り値は JSONGemEncoderクラスのインスタンスです

省略

class JSONGemEncoder #:nodoc:

省略

# Encode the given object into a JSON string
def encode(value)
  stringify jsonify value.as_json(options.dup)
end

上記は括弧が省略されていますが、 stringify(jsonify(value.as_json(options.dup))) と同義です
value.as_jsonでvalueは to_json した際のレシーバですので、ハッシュなどになります

jsonifyをみてみます

# Convert an object into a "JSON-ready" representation composed of
# primitives like Hash, Array, String, Numeric,
# and +true+/+false+/+nil+.
# Recursively calls #as_json to the object to recursively build a
# fully JSON-ready object.
#
# This allows developers to implement #as_json without having to
# worry about what base types of objects they are allowed to return
# or having to remember to call #as_json recursively.
#
# Note: the +options+ hash passed to +object.to_json+ is only passed
# to +object.as_json+, not any of this method's recursive +#as_json+
# calls.
def jsonify(value)
  case value
  when String
    EscapedString.new(value)
  when Numeric, NilClass, TrueClass, FalseClass
    value.as_json
  when Hash
    Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
  when Array
    value.map { |v| jsonify(v) }
  else
    jsonify value.as_json
  end
end

Stringクラスの場合には

# This class wraps all the strings we see and does the extra escaping
class EscapedString < String #:nodoc:
  def to_json(*)
    if Encoding.escape_html_entities_in_json
      super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
    else
      super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
    end
  end

  def to_s
    self
  end
end

上記EscapedStringクラスに値を入れ直して、newしています

Numeric, NilClass, TrueClass, FalseClassの場合には、as_jsonするだけになります

Hashクラスの場合もみてみます

Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]

まずvalue.map { |k, v| [jsonify(k), jsonify(v)] } ではvalue(ハッシュ)のkey,valueをそれぞれjsonifyし、2次元配列を作っています
※ 2次元配列はこのようなデータです [['hoge', 'huga'],['piyo', 'poyo']]

メソッドの内部で再度同じメソッドを呼ぶことが不思議に思えるかもしれませんが、再帰といいます

※ 再帰とはどのようなものかはこちらの記事がわかりやすいかと思います
再帰呼び出し

mapはブロックの戻り値を配列化します

ブロックの内部ではハッシュのキー、バリューを jsonifyし配列を作成しています

Hash[] の処理を調べてみます

新しいハッシュを生成します。 引数は必ず偶数個指定しなければなりません。奇数番目がキー、偶数番目が値になります。

このメソッドでは生成するハッシュにデフォルト値を指定することはできません。 Hash.newを使うか、Hash#default=で後から指定してください。

[PARAM] key_and_value:

生成するハッシュのキーと値の組です。必ず偶数個(0を含む)指定しなければいけません。

[EXCEPTION] ArgumentError:

奇数個の引数を与えたときに発生します。

以下は配列からハッシュを生成する方法の例です。


省略


4) キーや値が配列の場合

alist = [[1,["a"]], [2,["b"]], [3,["c"]], [[4,5], ["a", "b"]]]
hash = Hash[alist] # => {1=>["a"], 2=>["b"], 3=>["c"], [4, 5]=>["a", "b"]}

引用:Ruby2.5.0:Hash

hashの各キー、バリューをjsonifyし、再度ハッシュを作り直すという処理になります

Arrayクラスの場合を見てみます
value.map { |v| jsonify(v) } 配列の各要素に対して jsonify しています

上記以外の場合は

jsonify value.as_json

引数を as_json しているので、上記クラスのどれかになるまで処理が繰り返されます

ここまでが to_json の処理になります

json.rb ではさきほど見てきた処理以外にもオープンクラスして as_json メソッドを定義している箇所があります
こちらについては別記事にまとめようと思います

読んでみて

json化する処理はよく使ったりするので、調べてみて内部で何をしているのか把握することは大切だと感じました

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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

with_optionsメソッドは、連続した複数のメソッド呼び出しに対して共通して与えられるオプションを解釈するための手段を提供します。
デフォルトのオプションがハッシュで与えられると、with_optionsはブロックに対するプロキシオブジェクトを生成します。
そのブロック内では、プロキシに対して呼び出されたメソッドにオプションを追加したうえで、そのメソッドをレシーバに転送します。
たとえば、以下のように同じオプションを繰り返さないで済むようになります。

class Account < ActiveRecord::Base  
  has_many :customers, dependent: :destroy  
  has_many :products,  dependent: :destroy
  has_many :invoices,  dependent: :destroy
  has_many :expenses,  dependent: :destroy
 end  

上は以下のようにできます。

class Account < ActiveRecord::Base
  with_options dependent: :destroy do |assoc|
    assoc.has_many :customers
    assoc.has_many :products
    assoc.has_many :invoices
    assoc.has_many :expenses
  end
end

引用:RAILS GUIDES:Active Support コア拡張機能#with_options

読んでみましたが、イマイチ使い方がわからなかったので、さらに調べてみます

with_optionsの引数に渡したHashのオプションの値がブロック内に適用されます。上の例ではhash_manyのdependentオプションを共通化しましたが、他のパターンでも適用できます。例えば、モデルの例では他にもvalidatesメソッドのオプションを共通化など使えます。
引用:Railsのwith_options

上記例であれば、ハッシュである dependent: :destroy を共通化して各 has_many :xxx の引数に渡すという感じです

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

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

f:id:sktktk1230:20180126135718p:plain

2. 該当箇所をみてみます

1. activesupport > lib > active_support > core_ext > object > with_options.rb
# frozen_string_literal: true

require "active_support/option_merger"

class Object

省略

def with_options(options, &block)
  option_merger = ActiveSupport::OptionMerger.new(self, options)
  block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
end

まず引数を見てみます
さきほどの例だと optionsdependent: :destroy となります
&block

do |assoc|
  assoc.has_many :customers
  assoc.has_many :products
  assoc.has_many :invoices
  assoc.has_many :expenses
end

までとなります

それではwith_optionsのメソッドのロジックをみていきます
一行目 option_merger = ActiveSupport::OptionMerger.new(self, options)ActiveSupport::OptionMerger が何をしているのかわからないので、 こちらのコードを読んでみます

activesupport > lib > active_support > option_merger.rb
# frozen_string_literal: true

require "active_support/core_ext/hash/deep_merge"

module ActiveSupport
  class OptionMerger #:nodoc:
    instance_methods.each do |method|
      undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
    end

    def initialize(context, options)
      @context, @options = context, options
    end

    private
      def method_missing(method, *arguments, &block)
        if arguments.first.is_a?(Proc)
          proc = arguments.pop
          arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
        else
          arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
        end

        @context.__send__(method, *arguments, &block)
      end
  end
end

newした際の引数self, options がインスタンス変数 context, optionsにそれぞれ格納されます

次に block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) です

条件式 ? 真(true)の場合に実行される : 偽(false)の場合に実行される という書き方は三項演算子というものです

arity を調べると

メソッドが受け付ける引数の数を返します。
ただし、メソッドが可変長引数を受け付ける場合、負の整数
引用:Ruby2.5.0リファレンスマニュアル:arity

zero? を調べると

自身がゼロの時、trueを返します。そうでない場合は false を返します。 引用:Ruby2.5.0リファレンスマニュアル:arity

つまり、条件式 block.arity.zero? はブロックが受け付ける引数の数がゼロかを判定しています

条件式が真の場合に実行される option_merger.instance_eval(&block) を見てみます
instance_evalを調べてみると

instance_evalメソッドは、渡されたブロックをレシーバのインスタンスの元で実行します。ブロックの戻り値がメソッドの戻り値になります。
ブロック内では、インスタンスメソッド内でコードを実行するときと同じことができます。ブロック内でのselfはレシーバのオブジェクトを指します。なお、ブロックの外側のローカル変数はブロック内でも使えます。
引用:Rubyリファレンス:instance_eval

つまりoption_merger.instance_eval(&block) とはインスタンスoption_mergerでブロックの内容が実行されるということです
このようなのブロックの場合

do
  has_many :customers
  has_many :products
  has_many :invoices
  has_many :expenses
end

で定義されていた has_many メソッドは option_mergerで定義されていないので、 最終的に method_missing となります

※ method_missingについて参考: qiita.com

呼び出されるmethod_missingはoption_mergerのプライベートメソッドでオーバーライドしている method_missingとなります

private
  def method_missing(method, *arguments, &block)
    if arguments.first.is_a?(Proc)
      proc = arguments.pop
      arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
    else
      arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
    end

    @context.__send__(method, *arguments, &block)
  end

method_missing の引数に入る値はさきほどのブロックの例だと method = hash_many , *arguments = [:customers] です

if から else までみてみます

if arguments.first.is_a?(Proc)
  proc = arguments.pop
  arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
else

arguments配列の先頭がProcだった場合という意味になります
なぜこの条件式があるのかわからなかったので、調べてみます
RubyMineでdef method_missing end を選択し、選択箇所のコミットを見てみます

f:id:sktktk1230:20180126135748p:plain

option_mergerでlambdaもマージできるようにしたということだけがわかりましたが、どんなコードになるのか想像できなかったので、テストケースをみてみます
同じコミットにテストケースが1つ追加されていました

def test_nested_method_with_options_using_lambda
  local_lambda = lambda { { lambda: true } }
  with_options(@options) do |o|
    assert_equal @options.merge(local_lambda.call),
      o.method_with_options(local_lambda).call
  end
end

ハッシュを返すlambdaを定義しています
@optionsmethod_with_options は同じクラス内に定義されていたので、見てみると

class OptionMergerTest < ActiveSupport::TestCase
  def setup
    @options = { hello: "world" }
  end

省略

  private
    def method_with_options(options = {})
      options
    end
end

ブロック内で定義されているメソッドのoptionに当たる部分がlambdaだった場合に、正しく動くかをテストしてます
テストケースを見てみると、このコミットでの変更はメソッドのoptionsがハッシュを返すlambdaだった場合も正しく動くようにするという修正であることがわかりました

次に proc = arguments.pop を見ます

popメソッドは、配列の末尾の要素を削除し、その要素を返します。レシーバ自身を変更するメソッドです。配列が空のときはnilを返します。
引用:Rubyリファレンス:pop

else から end までみてみます

else
  arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
end

三項演算子の条件式部分の (arguments.last.respond_to?(:to_hash) をみます

argumentsに入るのはブロックで定義したメソッドの引数部分となります
optionがあるメソッドの定義は基本的に

def hoge(value, option)
end

のように引数の最後に記述することが多いので、引数の最後の値がハッシュを返すかを確認しています
to_hash はHashやArrayなどで定義されています

三項演算子の判定結果が真の場合 @options.deep_merge(arguments.pop) を見てみます

@options は with_optionsメソッドで第一引数に入れたハッシュでした
そのハッシュに対してdeep_mergeしているということです

deep_mergeについて調べてみると

先の例で説明したとおり、キーがレシーバと引数で重複している場合、引数の側の値が優先されます。
Active SupportではHash#deep_mergeが定義されています。ディープマージでは、レシーバと引数の両方に同じキーが出現し、さらにどちらも値がハッシュである場合に、その下位のハッシュを マージ したものが、最終的なハッシュで値として使用されます。
引用:RAILS GUIDES:Active Support コア拡張機能#deep_merge

arguments.pop は、さきほどの例def hoge(value, option) でいうと arguments = [value, option] となっているのを .pop することで option を取り出し arguments = [value] へと破壊的変更をすることです

判定結果が偽の場合 @options.dup も見ます

cloneメソッドとdupメソッドは、レシーバのオブジェクトのコピーを作成して返します。オブジェクトのコピーとは、同じ内容を持つ別のオブジェクトです。具体的には、元のオブジェクトと同じクラスの新しいオブジェクトで、元のオブジェクトのインスタンス変数を新しいオブジェクトにコピーしたものです。
引用:Rubyリファレンス:clone, dup

with_optionsメソッドで第一引数に入れたハッシュをコピーして返却するだけです

arguments << はそれぞれの判定結果の戻り値ハッシュをargumentsに追加しています

最後に @context.__send__(method, *arguments, &block) をみます

@context はwith_optionsのレシーバです
__send__ を調べてみます

sendメソッドは、sendの別名です。レシーバの持っているメソッドを呼び出します。
引用:Rubyリファレンス:send

with_optionsのレシーバに対してブロックで定義したメソッドを実行しています

読んでみて

method_missingを利用したプログラミングを初めて見ることができたので、非常に勉強になりました