Simon Soriano
Simon Soriano

Reputation: 813

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?

Upvotes: 9

Views: 1346

Answers (2)

Simon Soriano
Simon Soriano

Reputation: 813

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

Upvotes: 2

yez
yez

Reputation: 2378

Using Rails 4.1.9 and Ruby 2.2.1, I noticed a few issues with the code above.

  1. You are comparing column.type with strings, and Rails returns symbols for that attribute.
  2. base.define_method is trying to call a private method, you can get around that with send

This is the tweaked code

module ActiveRecordDateExtension
  extend ActiveSupport::Concern

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

Upvotes: 4

Related Questions