ransack scopeの検索キーワードとして 'T' が使えない

# 問題

ransackでは、ActiveRecordのscopeを用いて検索することができます。しかし、以下のように検索キーワードとして `'T'` は使えません。エラーメッセージにある通り、scopeに渡されているはずのキーワードが渡されていません。

![Sep-24-2023 23-49-35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472392/4e3f26ba-85f5-3ce6-680d-540e33f2515d.gif)


## 環境

- Rails 7.1.7
- Ruby 3.2.2
- Ransack 4.0.0

# 原因

このような問題が起きるのは、`'T'` などといった真理値っぽいものは、ActiveRecordのscopeに渡される前にサニタイズされるからです。

検索キーワードとして、真っぽいものが入っていたら `true`に、偽っぽいものが入っていたら`false`に変換しています(`sanitized_scope_args`メソッドの5-8行目)。

```rb
def sanitized_scope_args(args)
  if args.is_a?(Array)
    args = args.map(&method(:sanitized_scope_args))
  end

  if Constants::TRUE_VALUES.include? args
    true
  elsif Constants::FALSE_VALUES.include? args
    false
  else
    args
  end
end
```

https://github.com/activerecord-hackery/ransack/blob/16ce9110cbaee6682aca6e06ccb9d66a0c7a46c3/lib/ransack/search.rb#L143-L155

```rb
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
```

https://github.com/activerecord-hackery/ransack/blob/16ce9110cbaee6682aca6e06ccb9d66a0c7a46c3/lib/ransack/constants.rb#L26-L27


ここで変換されたものが`chain_scope`メソッドに渡されます(`add_scope`メソッドの12行目)。検索キーワードとして `'T'` を渡していれば、`sanitized_args`として`true`が渡されます。したがって、`chain_scope`メソッド内の3行目が実行されます。つまり、ActiveRecord scopeの引数として何も渡さないということです。そのため、上記gifにあるようなエラーメッセージが表示されます。

```rb
def add_scope(key, args)
  sanitized_args = if Ransack.options[:sanitize_scope_args] && !@context.ransackable_scope_skip_sanitize_args?(key, @context.object)
    sanitized_scope_args(args)
  else
    args
  end

  if @context.scope_arity(key) == 1
    @scope_args[key] = args.is_a?(Array) ? args[0] : args
  else
    @scope_args[key] = args.is_a?(Array) ? sanitized_args : args
  end
  @context.chain_scope(key, sanitized_args)
end
```

https://github.com/activerecord-hackery/ransack/blob/16ce9110cbaee6682aca6e06ccb9d66a0c7a46c3/lib/ransack/search.rb#L128-L141

```rb
def chain_scope(scope, args)
  return unless @klass.method(scope) && args != false
  @object = if scope_arity(scope) < 1 && args == true
    @object.public_send(scope)
  else
    @object.public_send(scope, *args)
  end
end
```

https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/context.rb#L76-L83


# 解決策

真理値っぽいものが`true`や`false`にサニタイズされることが原因です。このサニタイズのスキップは、ransack でサポートされています。`ransackable_scopes_skip_sanitize_args`メソッドを以下のようにオーバーライドします。そうすれば、`partial_match` scopeの引数はサニタイズされません。詳しくは[ドキュメント](https://activerecord-hackery.github.io/ransack/going-further/other-notes/#using-scopesclass-methods)を参照ください。


```rb
class Stock < ApplicationRecord
  scope :partial_match, ->(keyword) do
    return all if keyword.blank?

    pattern = "%#{sanitize_sql(keyword)}%"
    where('name LIKE :pattern', pattern:)
  end

  private

  class << self
    def ransackable_scopes_skip_sanitize_args
      %i[partial_match]
    end
  end  
end
```

# まとめ

「ransack scopeを用いた検索では、キーワードとして 'T' が使えない」という問題の原因は、検索キーワード中の真理値っぽいものがサニタイズされていることでした。この問題は、そのサニタイズをスキップすることで解決しました。

![Sep-24-2023 23-49-35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472392/4e3f26ba-85f5-3ce6-680d-540e33f2515d.gif)


# なぜ真理値っぽいものをサニタイズしているのか？

boolean型のフィールドに対してransack scopeをいい感じに使えるようにするために、真理値っぽいものをサニタイズしているのかもしれません(@aki77 さんに教えてもらいました)。

例えば、チェックボックスの`value`は`'1'`や`'T'`などいろんな値が使われています。`'1'`や`'T'`を一意な値に変換して、scope内で使いやすいようにしているのだとしたら、サニタイズするのも少しは納得します。ただ、サニタイズという言い方は微妙ですが。。。



# 参考URL

- https://github.com/activerecord-hackery/ransack/issues/1232
- https://activerecord-hackery.github.io/ransack/going-further/other-notes/#using-scopesclass-methods