Using method missing in Rails

5.5k views Asked by At

I have a model with several date attributes. I'd like to be able to set and get the values as strings. I over-rode one of the methods (bill_date) like so:

  def bill_date_human
    date = self.bill_date || Date.today
    date.strftime('%b %d, %Y')
  end
  def bill_date_human=(date_string)
    self.bill_date = Date.strptime(date_string, '%b %d, %Y')
  end

This performs great for my needs, but I want to do the same thing for several other date attributes... how would I take advantage of method missing so that any date attribute can be set/get like so?

2

There are 2 answers

3
KL-7 On BEST ANSWER

As you already know signature of desired methods it might be better to define them instead of using method_missing. You can do it like that (inside you class definition):

[:bill_date, :registration_date, :some_other_date].each do |attr|
  define_method("#{attr}_human") do
    (send(attr) || Date.today).strftime('%b %d, %Y')
  end   

  define_method("#{attr}_human=") do |date_string|
    self.send "#{attr}=", Date.strptime(date_string, '%b %d, %Y')
  end
end

If listing all date attributes is not a problem this approach is better as you are dealing with regular methods instead of some magic inside method_missing.

If you want to apply that to all attributes that have names ending with _date you can retrieve them like that (inside your class definition):

column_names.grep(/_date$/)

And here's method_missing solution (not tested, though the previous one is not tested either):

def method_missing(method_name, *args, &block)
  # delegate to superclass if you're not handling that method_name
  return super unless /^(.*)_date(=?)/ =~ method_name

  # after match we have attribute name in $1 captured group and '' or '=' in $2
  if $2.blank?
    (send($1) || Date.today).strftime('%b %d, %Y')
  else
    self.send "#{$1}=", Date.strptime(args[0], '%b %d, %Y')
  end
end

In addition it's nice to override respond_to? method and return true for method names, that you handle inside method_missing (in 1.9 you should override respond_to_missing? instead).

0
Frederick Cheung On

You might be interested in ActiveModel's AttributeMethods module (which active record already uses for a bunch of stuff), which is almost (but not quite) what you need.

In a nutshell you should be able to do

class MyModel < ActiveRecord::Base

  attribute_method_suffix '_human'

  def attribute_human(attr_name)
    date = self.send(attr_name) || Date.today
    date.strftime('%b %d, %Y')
  end
end

Having done this, my_instance.bill_date_human would call attribute_human with attr_name set to 'bill_date'. ActiveModel will handle things like method_missing, respond_to for you. The only downside is that these _human methods would exist for all columns.