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
CodePudding user response:
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.
CodePudding user response:
You could just create a simple module that defines a method to construct all of these filters and then call it with a list of filterable attributes.
module Filterable
def add_filters(*filters)
filters.each do |filter|
define_singleton_method("filter_eq_#{filter}"){ |val| where(filter => val) }
end
end
end
Then
class MyClass < ApplicationRecord
extend Filterable
add_filters :reference, :description, :weight, :created_at
end
You could make this more comprehensive as (this solution utilizes Arel::Predications
to build query filters so you can expand based on that list)
module Filterable
FILTERS = [:eq,:gt,:lt,:gteq,:lteq]
def add_filters(*fields, filters: Filterable::FILTERS)
raise ArgumentError, "filter must be one of #{Filterable::FILTERS}" if filters.any? {|filter| !Filterable::FILTERS.include?(filter)}
filters.each do |predicate|
fields.each do |field|
_add_filter(predicate,field)
end
end
end
def _add_filter(predicate,field)
define_singleton_method("filter_#{predicate}_#{field}") do |val|
where(arel_table[field].public_send(predicate,val))
end
end
end
Then call as
class MyClass < ApplicationRecord
extend Filterable
# add equality only filters
add_filters :description, :ref1, :ref2, :ref3, filters: [:eq]
# add all filters
add_filters :weight, :height, :created_at
end