Simon Soriano Simon Soriano - 1 month ago 18
Ruby Question

Extending ActiveRecord::Base

I'm trying to add some custom methods to ActiveRecord. I want to add a

*_after
and
*_before
scopes for every
date
field of a model so I can do something like this:

User.created_at_after(DateTime.now - 3.days).created_at_before(DateTime.now)


I've followed the solution explained here Rails extending ActiveRecord::Base but when I execute the rails console and try to call the methods I get an
undefined method
error.

Here's my code:

# config/initializers/active_record_date_extension.rb
require "active_record_date_extension"

# lib/active_record_date_extension.rb
module ActiveRecordDateExtension
extend ActiveSupport::Concern

included do |base|
base.columns_hash.each do |column_name,column|
if ["datetime","date"].include? column.type
base.define_method("#{column_name}_after") do |date|
where("#{column_name} > ?", date)
end
base.define_method("#{column_name}_before") do |date|
where("#{column_name} < ?", date)
end
end
end
end
end
Rails.application.eager_load!

ActiveRecord::Base.descendants.each do |model|
model.send(:include, ActiveRecordDateExtension)
end


What am I doing wrong?

Answer

Thanks to the previous answer I realised part of the problems. Here are all the problems and the solution that I came to after some research:

  1. column.type is a symbol and I was comparing it with a String.
  2. base.define_method is a private method
  3. I had to define the methods in the singleton_class, not in the base class nor the class.
  4. Rails.application.eager_load! will cause eager load even when it is not required. This wasn't affecting the functionality but in first place the eager load should not be responsibility of this "extension" and in second place it depends in Rails, making the "extension" only Rails compatible.

Taking into account these problems I decided to implement it using the method_missing functionality of ruby and I wrote this gem (https://github.com/simon0191/date_supercharger). Here is the relevant part for this question:

module DateSupercharger
  extend ActiveSupport::Concern

  included do
    def self.method_missing(method_sym, *arguments, &block)
      return super unless descends_from_active_record? 
      matcher = Matcher.new(self,method_sym)
      # Inside matcher
      # method_sym.to_s =~ /^(.+)_(before|after)$/

      if matcher.match?
        method_definer = MethodDefiner.new(self) # self will be klass inside Matcher
        method_definer.define(attribute: matcher.attribute, suffix: matcher.suffix)
        # Inside MethodDefiner
        # new_method = "#{attribute}_#{suffix}"
        # operators = { after: ">", before: "<" }
        # klass.singleton_class.class_eval do
        #   define_method(new_method) do |date|
        #     where("#{attribute} #{operators[suffix]} ?", date)
        #   end
        # end
        send(method_sym, *arguments)
      else
        super
      end
    end

    def self.respond_to?(method_sym, include_private = false)
      return super unless descends_from_active_record?
      if Matcher.new(self,method_sym).match?
        true
      else
        super
      end
    end
  end
end
ActiveRecord::Base.send :include, DateSupercharger
Comments