I have Filterable
module that creates scopes usesd by various models,
module Filterable
extend ActiveSupport::Concern
included do
# References
scope :filter_eq_reference, ->(val){ where(reference: val) }
scope :filter_eq_description, ->(val){ where(description: val) }
scope :filter_eq_ref1, ->(val){ where(ref1: val) }
scope :filter_eq_ref2, ->(val){ where(ref2: val) }
scope :filter_eq_ref3, ->(val){ where(ref3: val) }
scope :filter_eq_ref4, ->(val){ where(ref4: val) }
scope :filter_eq_ref5, ->(val){ where(ref5: val) }
scope :filter_eq_ref6, ->(val){ where(ref6: val) }
# Weights
scope :filter_eq_weight, ->(val){ where(weight: val) }
scope :filter_lt_weight, ->(val){ where('weight < ?', val) }
scope :filter_lte_weight,->(val){ where('weight <= ?', val) }
scope :filter_gt_weight, ->(val){ where('weight > ?', val) }
scope :filter_gte_weight,->(val){ where('weight >= ?', val) }
# ...
end
class_methods do
end
end
I want to refactor it for several reasons #1. it's getting large #2. All models don't share the same attributes
I came to this
module Filterable
extend ActiveSupport::Concern
FILTER_ATTRIBUTES = ['reference', 'description', 'weight', 'created_at']
included do |base|
base.const_get(:FILTER_ATTRIBUTES).each do |filter|
class_eval %Q?
def self.filter_eq_#{filter}(value)
where(#{filter}: value)
end
?
end
end
It works, but I want to have the attributes list in the model class, As issue #2, I think it more their responsability So I moved FILTER_ATTRIBUTES in each class including this module The problem when doiing that, I get an error when calling Article.filter_eq_weight 0.5
NameError: uninitialized constant #Class:0x000055655ed90f80::FILTER_ATTRIBUTES Did you mean? Article::FILTER_ATTRIBUTES
How can I access the base class - having 'base' injected or not doesn't change anything
Or maybe better ideas of implementation ? Thanks
I would suggest a slightly different approach. You already see that you are going to be declaring a list of filters in each class that wants to filter based on some value. Stepping back a bit, you may also realize that you are going to create different types of filters depending on the type of value of the field. With that in mind, I would consider stealing a page from the Rails playbook and create class methods that create the filters for you.
module Filterable
extend ActiveSupport::Concern
class_methods do
def create_equality_filters_for(*filters)
filters.each do |filter|
filter_name = "filter_eq_#{filter}".to_sym
next if respond_to?(filter_name)
class_eval %Q?
def self.filter_eq_#{filter}(value)
where(#{filter}: value)
end
?
end
end
def create_comparison_filters_for(*filters)
filters.each do |filter|
{ lt: '<', lte: '<=', gt: '>', gte: '>=' }.each_pair do |filter_type, comparison|
filter_name = "filter_#{filter_type}_#{filter}".to_sym
puts filter_name
next if respond_to?(filter_name)
class_eval %Q?
def self.filter_#{filter_type}_#{filter}(value)
where('#{filter} #{comparison} \?', value)
end
?
end
end
end
end
end
Then you would use it like this:
class Something < ApplicationRecord
include Filterable
create_equality_filters_for :reference, :description, :weight
create_comparison_filters_for :weight
end
Something.methods.grep(/filter_/)
[:filter_eq_reference, :filter_eq_description, :filter_eq_weight, :filter_lt_weight, :filter_lte_weight, :filter_gt_weight, :filter_gte_weight]
This approach makes your code significantly more declarative -- another developer (even yourself in a year!) won't wonder what the FILTER_ATTRIBUTES constant does or why (if?) it's required (a potential problem when the consuming code is not part of the class you are reading). Though future-you may not remember where the create_xyz methods are defined or exactly how they do what they do, the method names make fairly clear what is being done.