Spree::Productにあるadd_search_scope（メソッド）についてコードを読んでみた。

# はじめに
rails初心者の私が、コードをしっかりと読む習慣をつけるために、読んだコードについてメモ代わりとして、投稿しています。
今、solidusを使用してアプリケーションを作成中で、taxonの取得について気になったので、コードを読んでみました。
間違いがある場合はコメントにてご指摘いただければ幸いです。

#今回の内容
solidusにてアプリケーションを作成中に、商品のページで関連商品を表示するとき、同じtaxonを持っている商品を取得するコードを探したところ、model/spree/product/scopes.rbにて使用出来そうなメソッドがありました。
こちらがそのメソッドです。　=> [self.in_taxons(taxons)]
このメソッドをコンソールで使用したところ、引数のtaxonsに所属するproductを重複することなく取得することができました。

#self.in_taxons(taxons)について
以下は、どのような工程で取得したのかをコードをおってみていきます。
まずは、self.in_taxons(taxons)が書かれたコードがこちら。

```ruby:model/spree/product/scopes.rb
add_search_scope :in_taxons do |*taxons|
  taxons = get_taxons(taxons)
  taxons.first ? prepare_taxon_conditions(taxons) : where(nil)
end
```

add_search_scopeメソッドがわからないので、探す。

```ruby:model/spree/product/scopes.rb
def self.add_search_scope(name, &block)
  singleton_class.send(:define_method, name.to_sym, &block)
  search_scopes << name.to_sym
end
```

上記を見ると、①singleton_class（特異クラス)に対して、define_methodでクラスインスタンスを定義しようとしている。
つまり、add_search_scope(name, &block)で引数となっているnameをメソッド名として、第二引数であるブロックをメソッドの内容として定義したクラスインスタンスを作成している。
②その後、search_scopesに代入している。おそらくこのクラスを継承したクラスでも使用できるようにするためだと思う。

①、②の内容で自分がわからなかった内容を記します。
①[singleton_classについて]、　[define_methodについて]、　[to_symについて]、 [sendについて]
②[search_scopeについて]、　[cattr_accessorについて]、　[attr_accessorについて]（参考として）

###①[singleton_classについて]
singleton_classは特異メソッドを格納しているクラスになる。
tanaka.hiを変数名として定義することで、特定のインスタンスのみで使用可能なメソッドを定義でき、そのメソッドが格納された場所が特異クラスとなる。

```ruby:コンソール
class User 
end  
tanaka = User.new
def tanaka.hi
  p "hello"
end  
tanaka.hi
=> "hello"
suzuki=User.new
suzuki.hi
=> NoMethodError: undefined method `hi'
```

詳しい内容： [特異メソッド、特異クラスとクラスメソッドの関係について](https://qiita.com/fujishin05/items/be60f4f788f2732921a4)

###①[define_methodについて]
クラスやモジュールにメソッドを定義する。

```ruby:コンソール
class Greeting
  { :cat => 'Meow', :dog => 'Bowwow' }.each do |name, message|
    define_method(name) {|num| message * num }
  end
end
puts Greeting.new.cat(2)
=> MoewMoew
```

###①[to_symについて]
文字列に対応したシンボルを返す。

```ruby:コンソール
h = “hello”
h.to_sym   # =>   :hello
```

###①[sendメソッドについて]
レシーバーの持っているメソッドを呼び出してくれるメソッド。引数によって役割が変化する。
詳しい内容： [sendメソッドのいろんな書き方](https://qiita.com/ngron/items/05d3a9624c2c3ec5dbb6#%E6%9D%A1%E4%BB%B6%E5%88%A5%E3%81%AB%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91%E3%82%8Bruby)

###②[search_scopesについて]
search_scopesを探すと以下のコードが同じクラス内にあった。cattr_accessorとして定義されていた。代入したname.to_sym(クラスインスタンス）を格納していくようになっている。

```ruby:model/spree/product/scopes.rb
cattr_accessor :search_scopes do
  []
end
```

###②[cattr_accessorについて]

クラスの変数を与えることができる。その影響は継承先でも使用可能となる。

```ruby:コンソール
class Parent
  cattr_accessor :foo
end
class Child < Parent
end
class Grandchild < Child
end
Parent.foo = 100
Child.foo #=> 100
Child.foo = 200
Child.foo #=> 200
Parent.foo #=> 200     // 値は継承ツリーの上位に反映される
Grandchild.foo #=> 200 // 値は継承ツリーの下位に反映される
```

詳しい内容： [Rails: クラスレベルの3つのアクセサを比較する（翻訳）](https://techracho.bpsinc.jp/hachi8833/2017_10_19/46488)

###②[attr_accessorについて]（参考として）
クラスの変数を与えることができる。その影響は継承先では使用出来ない。

```ruby:コンソール
class Parent
 class << self
   attr_accessor :foo
 end
end
class Child < Parent 
end
Parent.foo = 100
Child.foo #=> nil  // 値は継承されない
Child.foo = 200
Child.foo #=> 200
Parent.foo #=> 100 // 子クラスで値を変更しても親クラスの値には影響しない
```

詳しい内容： [Rails: クラスレベルの3つのアクセサを比較する（翻訳）](https://techracho.bpsinc.jp/hachi8833/2017_10_19/46488)

#再びin_taxonsに戻る
上記のことを踏まえて再度in_taxonsのコードをみてみる。

```ruby:model/spree/product/scopes.rb
add_search_scope :in_taxons do |*taxons|
  taxons = get_taxons(taxons)
  taxons.first ? prepare_taxon_conditions(taxons) : where(nil)
end
```

get_taxonsでtaxonsに代入している。ので、get_taxonsを探す。

```ruby:model/spree/product/scopes.rb
def get_taxons(*ids_or_records_or_names)
  taxons = Spree::Taxon.table_name
  ids_or_records_or_names.flatten.map { |t|
    case t
    when Integer then Spree::Taxon.find_by(id: t)
    when ActiveRecord::Base then t
    when String
      Spree::Taxon.find_by(name: t) ||
        Spree::Taxon.where("#{taxons}.permalink LIKE ? OR #{taxons}.permalink = ?", "%/#{t}/", "#{t}/").first
          end
        }.compact.flatten.uniq
end
```

①taxonのテーブル名をtaxonsに代入した後、引数にmapメソッドを実行し、配列に格納する。今回は、インスタンスを引数にしていたため、そのまま出力されている。
格納した値は、全てnilを削除し、ネストを解除し、重複を無くしている。その値が入った配列を戻り値として返している。

以下、わからないコードを書いています。
①[table_nameについて]

###①[table_nameについて]
ActiveRecord::Base.table_name=メソッドでテーブル名を取得できる。
以下のように指定もできる。

 ```ruby:example.rb
class Product < ApplicationRecord
  self.table_name = "PRODUCT"
end
```
#再度in_taxonsに戻る
戻り値をtaxonsに代入して、taxons.firstがtureの場合、prepare_taxon_conditions(taxons)メソッドが実行されている。

```ruby:model/spree/product/scopes.rb
add_search_scope :in_taxons do |*taxons|
  taxons = get_taxons(taxons)
  taxons.first ? prepare_taxon_conditions(taxons) : where(nil)
end
```
prepare_taxon_conditions(taxons)では、重複やネストなどを解除したtaxonsの配列からmapメソッドで、各idを抜き出して、joinを使用して、取得したtaxonのidたちに絞りproductを取得している。

```ruby:model/spree/product/scopes.rb
def prepare_taxon_conditions(taxons)
  ids = taxons.map { |taxon| taxon.self_and_descendants.pluck(:id) }.flatten.uniq
  joins(:taxons).where("#{Spree::Taxon.table_name}.id" => ids)
end
```

in_taxons(taxons）からproductを取得しているまでのコードについて読んでみました。