ransackable_scopesを使った検索でdate_fieldを使うとエラーになる

Railsアプリでの検索機能を実装できるRansackというGemがあります。

Ransackでは引数を受け取るscopeを定義してそれを検索で使うことができます。ransackable_scopesという機能を使います。
ransack - Using Scopes/Class Methods

このransackable_scopesActionView::Helpers::FormHelper#date_fieldを併用するとエラーになる事象があったので、それに関するメモです。

Issueを作ったので、ほぼそれのコピペです。

NoMethodError error when using ransackable_scopes and date_field · Issue #1252 · activerecord-hackery/ransack

バージョン

  • ransack 2.4.2
  • Rails 6.1.4
  • Ruby 2.6.3

再現手順

article.rb
class Article < ApplicationRecord
  scope :created_since, -> (date) {
    where('created_at >= ?', date)
  }

  def self.ransackable_scopes(auth_object = nil)
    %i[created_since]
  end
end
search_controller.rb
class SearchController < ApplicationController
  def index
    @q = Article.ransack(params[:q])
  end
end
search/index.html.slim
= search_form_for @q, url: search_index_path do |form|
  = form.date_field :created_since
  = form.submit 'Search'

検索を実行する(検索フォームをサブミット)と下記のエラーとなります。

undefined method `strftime' for "2021-09-25":String
Did you mean?  strip

ちなみにransackable_scopesを介さない検索であれば、上記のようなエラーは発生しません。

解決策

シンプルにsearch_fieldを使えば解決します。

= form.search_field :created_since, type: 'date'

Ransackの中の処理

ransackable_scopesを介さない検索であればエラーにならないところが謎だったので、Ransackの処理を追ってみました。

ransackable_scopesではない普通の検索であれば、Condition#valueからValue#castが呼ばれて、検索時の value をよしなにキャストしてくれます。

lib/ransack/nodes/condition.rb
def value
  if predicate.wants_array
    values.map { |v| v.cast(default_type) }
  else
    values.first.cast(default_type)
  end
end
lib/ransack/nodes/value.rb
def cast(type)
  case type
  when :date
    cast_to_date(value)
  when :datetime, :timestamp, :time
    cast_to_time(value)
  when :boolean
    cast_to_boolean(value)
  when :integer
    cast_to_integer(value)
  when :float
    cast_to_float(value)
  when :decimal
    cast_to_decimal(value)
  when :money
    cast_to_money(value)
  else
    cast_to_string(value)
  end
end

ransackable_scopesの場合は上記処理を通過しないので、キャストされずに文字列のままstrftimeを呼び出そうとしてエラーになっているようです。

action_view/helpers/tags/date_field.rb
module ActionView
  module Helpers
    module Tags # :nodoc:
      class DateField < DatetimeField # :nodoc:
        private
          def format_date(value)
            value&.strftime("%Y-%m-%d") # 👈 エラーとなる箇所
          end
      end
    end
  end
end

search_fieldを使うと上記処理は通らないのでエラーになりません。