0

Is it possible to DRY-up the following code:

def is_user?
  is_role? ROLES[:user]
end

def is_mod?
  is_role? ROLES[:mod]
end

def is_admin?
  is_role? ROLES[:admin]
end

private

def is_role?(role)
  self.roles & role == role
end

Into a single function, yet still have the ability to call the function names as currently (is_user?, is_mod?, etc)

UPDATE:

Using Aetherus' answer below I created the following for managing user roles (where a user can have multiple roles):

class User < ActiveRecord::Base
  # Use bitwise values for more roles (double the previous values)
  ROLES = { user: 1, dummy: 2, mod: 4, admin: 8 } 

  # Add the desired role
  def add_role(role)
    self.roles |= ROLES[role]
  end
  # eg: add_role :admin

  # Removed the desired role
  def remove_role(role)
    self.roles &= ~ROLES[role]
  end

  # methods for each role (mod? admin? etc)
  ROLES.keys.each do |role|
    define_method("#{role}?") do
      self.roles & ROLES[role] == ROLES[role]
    end
  end
end
4
  • ok now I see what you mean, that makes sense. Thanks for discussion :) Commented Aug 3, 2015 at 9:38
  • No problem at all :) Commented Aug 3, 2015 at 9:39
  • I would suggest changing "function" to "method" in your question. Commented Aug 3, 2015 at 12:30
  • @HunterStevens Done. Commented Aug 3, 2015 at 12:31

3 Answers 3

4

You can define multiple methods with one single ruby code block.

%w(user mod admin).each do |role|
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{role}?
      role == '#{role}' && roles.include?('#{role}')
    end
  RUBY
end

Or a more clear way:

%w(user mod admin).each do |role|
  define_method("#{role}?") do
    self.role == role && roles.include?(role)
  end
end

By the way, in ruby, the is_ prefix is not needed, since the trailing ? tells the programmers that method returns a true or a false.

Sign up to request clarification or add additional context in comments.

2 Comments

The second one is definitely more clearer to see what's going on. The first one would likely confuse others on the team (myself included a week from now :)
Thanks for the advice about not using is_. I've removed them from my code.
2

You can go with method_missing.

The simplest possible solution would be something like:

class Roles
  def method_missing(method_name, *args, &block)
    if /^is_(?<role_name>\w+)\?$/ =~ method_name
      is_role?(role_name.to_sym)
    else
      super
    end
  end

  private

  def is_role?(role_name)
    # just for demo purposes
    p "Checking #{role_name}"
  end
end

roles = Roles.new

roles.is_user?
roles.is_mod?
roles.is_admin?

In method_missing I'm trying to catch any method that is not implemented (please note, I removed the proper methods is_user?, is_mod? and is_admin?), later, I'm checking if the name of method is of proper format with Regex (/^is_(?<role_name>\w+)\?$/), and if it is, I'm reusing captured role_name.

Slightly more restrictive method_missing.

Problem with this approach is, it will accept any method call, like let's say is_super_user?. In some cases this might be desirable, sometimes not. If you would like to restrict it only to the 3 type of users you've mentioned, you can change the Regex to:

/^is_(user|mod|admin)\?$/

One last thing. When implementing method_missing, you should also take care about respond_to_missing?, which is quite crucial when you would like to assert if the object responds to those magic methods:

class Roles
  # ...
  def respond_to_missing?(method_name, include_private = false)
    /^is_(user|mod|admin)\?$/ =~ method_name
  end
end

With this in place, you are able to do:

roles = Roles.new

roles.respond_to? :is_admin? # => true
roles.respond_to? :is_super_user? # => false

Read more here.

Hope that helps!

3 Comments

I don't like method_missing, because every time when a method call hits method_missing, it has already climbed the whole ancestors chain, at least once. This can be expensive.
Thanks @Aetherus, for your input! You are absolutely right - it might be expensive. However, method_missing lays as the most basic way of metaprogramming in ruby and as such I'm advertising it. Additionally, even if it can be expensive, I'm accepting the "cost" of it (as it gives so much flexibility) and I'm not refactoring it unless I hit some real performance issues. If it doesn't case any - usually I'm ok with it.
You're right. I also started with method_missing, and maybe it's the only way to meta programming features when the data is inadequate until runtime.
0

@Jacob, if you use rails 4, you can use AR#enum feature (http://api.rubyonrails.org/classes/ActiveRecord/Enum.html), no need to implement this by hands.

1 Comment

I tried using enums but wasn't able to use bitwise operations on it and each user can have multiple roles. Enums would only support one.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.