ransack gemの`ransackable_attributes`メソッドを見に行った時の備忘録

## 環境
- Rails 7.1.5.1
- Ruby 3.1.5

## この記事を書こうと思った理由

ransack gemを2.5.0から4系にアップデートを行った時に、下記のようなメッセージが出力されました

```
RuntimeError in Home#index

Ransack needs User attributes explicitly allowlisted as
searchable. Define a `ransackable_attributes` class method in your `User`
model, watching out for items you DON'T want searchable (for
example, `encrypted_password`, `password_reset_token`, `owner` or
other sensitive information). You can use the following as a base:
```

下記の記事を見ながら解決しようと思った時に、どんな仕組みになっているのかなぁ〜とgemのコードを見に行った時の備忘録です

https://qiita.com/jnchito/items/cb991633f00d6168edcf

## ransackable_attributesのコード

### ransackable_attributes(auth_object = nil)

まずこんな感じで定義されています

```ransack/lib/ransack/adapters/active_record/base.rb
# Ransackable_attributes, by default, returns all column names
# and any defined ransackers as an array of strings.
# For overriding with a whitelist array of strings.
#
def ransackable_attributes(auth_object = nil)
  @ransackable_attributes ||= deprecated_ransackable_list(:ransackable_attributes)
end
```

`||=`自己代入で`@ransackable_attributes`がなければ`deprecated_ransackable_list(:ransackable_attributes)`を呼びます

### deprecated_ransackable_list(method)

`ransackable_attributes(auth_object = nil)`で呼ばれている`deprecated_ransackable_list(:ransackable_attributes)`はこんな感じ

```ransack/lib/ransack/adapters/active_record/base.rb
def deprecated_ransackable_list(method)
  list_type = method.to_s.delete_prefix("ransackable_")

  if explicitly_defined?(method)
    warn_deprecated <<~ERROR
      Ransack's builtin `#{method}` method is deprecated and will result
      in an error in the future. If you want to authorize the full list
      of searchable #{list_type} for this model, use
      `authorizable_#{method}` instead of delegating to `super`.
    ERROR

    public_send("authorizable_#{method}")
  else
    raise <<~MESSAGE
      Ransack needs #{name} #{list_type} explicitly allowlisted as
      searchable. Define a `#{method}` class method in your `#{name}`
      model, watching out for items you DON'T want searchable (for
      example, `encrypted_password`, `password_reset_token`, `owner` or
      other sensitive information). You can use the following as a base:

      ```ruby
      class #{name} < ApplicationRecord

        # ...

        def self.#{method}(auth_object = nil)
          #{public_send("authorizable_#{method}").sort.inspect}
        end

        # ...

      end
      ```
    MESSAGE
  end
end
```

パッと見た感じ、ここでエラーメッセージやら警告が定義してあるなぁというのがわかります

`list_type = method.to_s.delete_prefix("ransackable_")`で`list_type`に`attributes`を代入します
`delete_prefix`は文字列の先頭から prefix を削除した文字列のコピーを返します

次に`explicitly_defined?(method)`でどんな仕組みで分岐されているのかを見に行きます

### explicitly_defined?(method)

```ransack/lib/ransack/adapters/active_record/base.rb
def explicitly_defined?(method)
  definer_ancestor = singleton_class.ancestors.find do |ancestor|
    ancestor.instance_methods(false).include?(method)
  end

  definer_ancestor != Ransack::Adapters::ActiveRecord::Base
end
```

ここが一番むずかったす

初見、一体これは何をしているんだ…という感じですが、ざっくりいうと

ユーザーが`attributes`をカスタムしているかどうか？を判定してくれるのメソッドです

ユーザーが`attributes`をカスタムしていれば`definer_ancestor != Ransack::Adapters::ActiveRecord::Base`でtrueが返り、していなければ`Ransack::Adapters::ActiveRecord::Base`で定義されいる元々の`attributes`とイコールになってしまうので`false`が返ります

※見たことないメソッドについての補足
#### singleton_class

`singleton_class`メソッドとはオブジェクトの[Rubyの特異クラス](https://qiita.com/HMMNRST/items/39c5a11ad1a2d4afbbc4)を取得してくれるメソッドです

#### ancestors

モジュールやクラスの継承チェーンを配列で返してくれる


### authorizable_ransackable_attributes

```ruby
public_send("authorizable_#{method}")
```

ユーザーによってカスタムされていた場合最後は`public_send`で`authorizable_ransackable_attributes`が呼び出されます

```Breadcrumbsransack/lib/ransack/adapters/active_record/base.rb
# Bare list of all potentially searchable attributes. Searchable attributes
# need to be explicitly allowlisted through the `ransackable_attributes`
# method in each model, but if you're allowing almost everything to be
# searched, this list can be used as a base for exclusions.
#
def authorizable_ransackable_attributes
  if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
    column_names + _ransackers.keys + _ransack_aliases.keys +
    attribute_aliases.keys
  else
    column_names + _ransackers.keys + _ransack_aliases.keys
  end.uniq
end
```

`if Ransack::SUPPORTS_ATTRIBUTE_ALIAS`でransackがサポートしているエイリアスかどうかを判定します

そしてようやく見覚えのあるスネークケースのリストが生成されます

## 感想

- 特異メソッドとか初めて聞きました。これもまた記事書くかも
- ちょっとした気持ちで見にいくと複雑で帰れなくなる

## 参考

https://github.com/activerecord-hackery/ransack

https://activerecord-hackery.github.io/ransack/