I was trying to solve a problem where we want to pass in a handful of parameters to a method that will then scope an ActiveRecord model in Rails.

I have hit this same problem in multiple guises. Most times when trying to allow a User to create their own report based on filterable attributes, all of which may be optional.

Before

created_at_start_date = Date.current
users = [User.find(1), User.find(5)]

posts = Post.all
posts = posts.by_created_at_start_date(created_at_start_date) if created_at_start_date.present? 
posts = posts.by_users(user) if users.any? 

puts posts.to_sql

After

PostsQuery.report_filtering(
  users: [User.find(1), User.find(5)],
  created_at_start_date: Date.current,
  created_at_end_date: Date.current, # ignored as there is no method
  updated_at_start_date: Date.current, # ignored as there is no method
  updated_at_end_date: Date.current, # ignored as there is no method
).to_sql

Magic

  def self.report_filtering(filters)
    filters.inject(self) do |klass, (method_name, args)|
      klass.try("by_#{method_name}", args) || klass.all
    end
  end

This code loops over the filter attributes that are passed in and dynamically chains the corresponding methods into a single call.

The generated code may look like this under the hood:

  def self.report_filtering(filters)
    by_users(filters[:users])
      .by_created_at_start_date(filters[:created_at_start_date])
      .all
	  .all
      .all
  end

Where the loop fails to call the corresponding method - for example by_created_at_end_date has not yet been defined - it will instead return all which allows us to continue chaining queries. Chaining .all does not effect the SQL that is generated. Rails is clever enough to ignore them.