views:

77

answers:

2

Each user has many roles; to find out whether a user has the "admin" role, we can use the has_role? method:

some_user.has_role?('admin')

Which is defined like this:

def has_role?(role_in_question)
  roles.map(&:name).include?(role_in_question.to_s)
end

I'd like to be able to write some_user.has_role?('admin') as some_user.is_admin?, so I did:

  def method_missing(method, *args)
    if method.to_s.match(/^is_(\w+)[?]$/)
      has_role? $1
    else
      super
    end
  end

This works fine for the some_user.is_admin? case, but fails when I try to call it on a user referenced in another association:

>> Annotation.first.created_by.is_admin?
NoMethodError: undefined method `is_admin?' for "KKadue":User
    from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations/association_proxy.rb:215:in `method_missing'
    from (irb):345
    from :0

What gives?

+3  A: 

Rails checks if you respond_to? "is_admin?" before doing a send.

So you need to specialize respond_to? also like:

def respond_to?(method, include_private=false)
  super || method.to_s.match(/^is_(\w+)[?]$/)
end

Note: Don't ask me why rails checks for respond_to? instead of just doing a send there, I don't see a good reason.

Also: The best way (Ruby 1.9.2+) is to define respond_to_missing? instead, and you can be compatible with all versions with something a bit fancy like:

def respond_to_missing?(method, include_private=false)
  method.to_s.match(/^is_(\w+)[?]$/)
end

unless 42.respond_to?(:respond_to_missing?) # needed for Ruby before 1.9.2:
  def respond_to?(method, include_private=false)
    super || respond_to_missing?(method, include_private)
  end
end
Marc-André Lafortune
To elaborate, you'd need to override `respond_to?` as well to make this work.
thenduks
+2  A: 

The ActiveRecord::Associations::AssociationProxy class overrides method_missing and intercepts the call you are looking for before it gets to the model.

This happens because AP checks if the model respond_to? the method, which in your case, it doesn't.

You have a few solutions aside from editing Rails' source:

First, manually define each of the is_* methods for the user object using metaprogramming. Something like:

class User
  Role.all.each do |role|
    define_method "is_#{role.name}?" do
      has_role?(role.name)
    end
  end
end

Another is to load the User object via some other means such as

User.find(Annotation.first.user_id).is_admin?

Or use one of the other answers listed.

erik