Is there a way to dynamically add a scope to an active record class?

957 views Asked by At

I am trying to dynamically add a scope to an active record object. Some of the things I have been trying via metaprogramming are not working. Below is sort of what I want to achieve.

class UtilityClass
   def add_scope_to_class klass, name, lambda
     # should add something like
     # scope :published, -> { where(published: true) }
     code = lambda { filter_clause(value, &lambda) }
     klass.class_exec(&code)
   end
end

class Article < ActiveRecord::Base
end

UtilityClass.add_scope_to_class

Article.published # returns published articles

I have tried a couple of variations of this with plain ruby objects, but I thought asking the question with this context might get some different ideas/answers.

Thanks.

Updated

So far I have managed to come up with the following but it's not working

class ActiveSearch
  attr_accessor :collection
  attr_reader :params

  def initialize params={}
    @params = params
  end

  def results
    @collection = Person.where(nil)
    params.compact.each do |key, value|
      @collection = @collection.send("#{key}_filter", value)
    end
    @collection
  end

  def self.filter name, filter_proc
    code = lambda { |filter_name| scope("#{filter_name.to_s}_filter".to_s, filter_proc) }
    Person.class_exec(name, &code)
  end
end

class PersonSearch < ActiveSearch
  filter :first_name, ->(name){ where(first_name: name) }
end

I have hard coded a couple of things. The main idea is there.

2

There are 2 answers

2
Ryan-Neal Mes On

After playing around, I found the solution is a bit simpler than I originally thought.

class ActiveSearch
  attr_accessor :collection
  attr_reader :params

  def initialize params={}
    @params = params
  end

  def results
    @collection = Person.where(nil)
    params.compact.each do |key, value|
      @collection = @collection.send("#{key}_filter", value)
    end
    @collection
  end

  def self.filter name, filter_proc
    Person.define_singleton_method "#{name.to_s}_filter".to_sym, &filter_proc # does most of the heavy lifting
  end
end

class PersonSearch < ActiveSearch
  filter :first_name, lambda { |name| where(first_name: name) }
  filter :last_name, lambda { |name| where(last_name: name) }
end


PersonSearch.new({first_name: 'ryan', last_name: 'test'}).results

The above will combine all the appropriate methods and execute the query.

A couple of tweaks are needed, but it's pretty much doing what I want. The active search class still needs to be cleaned up and made generic, but I think it gets the idea across.

3
Chris Heald On

I think you might just be reinventing scopes.

class Person < ActiveRecord::Base
end

Person.send :scope, :first_name, -> {|name| where(first_name: name) }
Person.send :scope, :last_name,  -> {|name| where(last_name: name)  }

Person.first_name("chris").last_name("heald").first

Person.send :scope, :name, ->{|f, l| first_name(f).last_name(l) }
Person.name("chris", "heald").first