Using method missing in Rails
Asked Answered
F

2

6

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?

Filings answered 16/1, 2012 at 20:22 Comment(3)
method_missing is about the last straw you should take. Actually defining methods is much cleaner, leads to a better code design with clear separation of concerns, is much easier to understand and also faster. So if you can define your methods, you should always do it.Chungchungking
As learned from KL-7 there are better approaches than method_missing, but considering I have 4 different date attributes for this model, manually defining each one is not the solution. DRYFilings
Well, the method of KL-7 is actually the preferred one here. Because he's proposing exactly what I also meant: define the methods.Chungchungking
D
12

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).

Deuno answered 16/1, 2012 at 20:29 Comment(3)
oooo nice! I hadn't come across define_method before :) I'd still like to see the method missing implementation, but +1 for a better solutionFilings
Added method_missing version, but don't use it if you can define these methods.Deuno
@Deuno In your method_missing you should directly return with super when not handling the method. Right now, the attribute handling below is always executed.Chungchungking
B
5

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.

Bis answered 16/1, 2012 at 22:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.