Search code examples
rubyproc

Is Ruby capable of determining whether or not a key (or method argument) is a proc?


Doing this code kata and I have to remove as many of the if statements as I can from the following method:

#  returns permissions if user is set in security context
def get_user_permissions 
  user_permissions = Set.new
  if (@user != nil)
    user_permissions << :DEFAULT_PERMISSION
    if (has_cm_team_role) 
      user_permissions << :CM_TEAM_ROLE_PERMISSION
    end
    if (has_cm_invoice_view_role || has_invoice_finance_role)
      user_permissions << :CM_INVOICE_USER_PERMISSION
      user_permissions << :INVOICE_VIEW_PERMISSION
      user_permissions << :ACCESS_ALL_INVOICE_PERMISSION
    end
    if (has_invoice_finance_role) 
      user_permissions << :FINANCE_INVOICE_PERMISSION
    end
    if (has_application_access)
      user_permissions << :CM_INVOICE_USER_PERMISSION
    end
    if (has_application_access(:CM_INVOICE_ROLE)) 
      user_permissions << :CM_ANY_INVOICE_PERMISSION
    end
    if (has_application_access(:PA_INVOICE_ROLE)) 
      user_permissions << :PA_ANY_INVOICE_PERMISSION
    end
    if (has_application_access(:SDT_INVOICE_ROLE))
      user_permissions << :SDT_ANY_INVOICE_PERMISSION
    end
  end
  user_permissions
end

My first attempt almost works, but a few of the tests fail:

def get_user_permissions 
  user_permissions = Set.new
  if (@user != nil)
    user_permissions << :DEFAULT_PERMISSION
    # add the permissions in another method
    add_permissions(user_permissions)
  end
  user_permissions
end

def add_permissions(user_permissions)
  # a hash where each key is a condition, and each value is a permission
  hash = {
    has_cm_team_role => :CM_TEAM_ROLE_PERMISSION,
    has_cm_invoice_view_role || has_invoice_finance_role => :CM_INVOICE_USER_PERMISSION,
    has_cm_invoice_view_role || has_invoice_finance_role => :INVOICE_VIEW_PERMISSION,
    has_cm_invoice_view_role || has_invoice_finance_role => :ACCESS_ALL_INVOICE_PERMISSION,
    has_invoice_finance_role => :FINANCE_INVOICE_PERMISSION,
    has_application_access => :CM_INVOICE_USER_PERMISSION,
    has_application_access(:CM_INVOICE_ROLE) => :CM_ANY_INVOICE_PERMISSION,
    has_application_access(:PA_INVOICE_ROLE) => :PA_ANY_INVOICE_PERMISSION,
    has_application_access(:SDT_INVOICE_ROLE) => :SDT_ANY_INVOICE_PERMISSION
  }

  # loop through the hash and add permissions if the key is true
  hash.each do |condition, permission|
    if (condition)
      user_permissions << permission
    end
  end

The problem with this approach is that the three OR statements (in the second, third and fourth keys of my hash) are not evaluated by each. So I fix this by using Procs, like so:

def add_permissions(user_permissions)
  hash = {
    Proc.new{has_cm_team_role} => :CM_TEAM_ROLE_PERMISSION,
    Proc.new{has_cm_invoice_view_role || has_invoice_finance_role} => :CM_INVOICE_USER_PERMISSION,
    Proc.new{has_cm_invoice_view_role || has_invoice_finance_role} => :INVOICE_VIEW_PERMISSION,
    Proc.new{has_cm_invoice_view_role || has_invoice_finance_role} => :ACCESS_ALL_INVOICE_PERMISSION,
    Proc.new{has_invoice_finance_role} => :FINANCE_INVOICE_PERMISSION,
    Proc.new{has_application_access} => :CM_INVOICE_USER_PERMISSION,
    Proc.new{has_application_access(:CM_INVOICE_ROLE)} => :CM_ANY_INVOICE_PERMISSION,
    Proc.new{has_application_access(:PA_INVOICE_ROLE)} => :PA_ANY_INVOICE_PERMISSION,
    Proc.new{has_application_access(:SDT_INVOICE_ROLE)} => :SDT_ANY_INVOICE_PERMISSION
  }
  hash.each do |condition, permission|
    if (condition.call)
      user_permissions << permission
    end
  end
end

OK, this works, and the tests all pass, but I'm converting all of the keys into Procs, just so that 3 of them evaluate properly. I'd prefer to only use Procs when they are needed, i.e. for the second, third and fourth keys like so:

def add_permissions(user_permissions)
  hash = {
    # just use the method name unless a Proc is required
    has_cm_team_role => :CM_TEAM_ROLE_PERMISSION,
    # use a Proc here, so that the OR is evaluated later
    Proc.new{has_cm_invoice_view_role || has_invoice_finance_role} => :CM_INVOICE_USER_PERMISSION,
    Proc.new{has_cm_invoice_view_role || has_invoice_finance_role} => :INVOICE_VIEW_PERMISSION,
    Proc.new{has_cm_invoice_view_role || has_invoice_finance_role} => :ACCESS_ALL_INVOICE_PERMISSION,
    has_invoice_finance_role => :FINANCE_INVOICE_PERMISSION,
    has_application_access => :CM_INVOICE_USER_PERMISSION,
    has_application_access(:CM_INVOICE_ROLE) => :CM_ANY_INVOICE_PERMISSION,
    has_application_access(:PA_INVOICE_ROLE) => :PA_ANY_INVOICE_PERMISSION,
    has_application_access(:SDT_INVOICE_ROLE) => :SDT_ANY_INVOICE_PERMISSION
  }

And then some kind of method to determine if the key is a Proc, and if so, call it, but if not, just treat the condition like any other key:

  hash.each do |condition, permission|
    if condition.is_proc?
      if (condition.call)
        user_permissions << permission
      end
    elsif condition
      user_permissions << permission
    end
  end
end

Any tips? Any better ideas than the way I've tried to do this? Have I made it more cyclically complex by doing this? Should I just stick with my working solution where all of my keys are Procs?


Solution

  • Nothing to do about the proc question. I have never done code kata but there is few things I think you could improve.

    First, having conditions as hash keys is to me a bad idea. You will have many keys duplicates, only false and true.
    I would do it the other way and set your permissions as keys and the value will be true or false.

    You have also 3 times the exact same instructions, just 2 letters change each time.

    if (has_application_access(:CM_INVOICE_ROLE)) 
      user_permissions << :CM_ANY_INVOICE_PERMISSION
    end
    if (has_application_access(:PA_INVOICE_ROLE)) 
      user_permissions << :PA_ANY_INVOICE_PERMISSION
    end
    if (has_application_access(:SDT_INVOICE_ROLE))
      user_permissions << :SDT_ANY_INVOICE_PERMISSION
    end
    

    that could be reduced to

    %(cm pa sdt).each do |key|
        user_permissions << :"#{key}_ANY_INVOICE_PERMISSION" if has_application_access(:"#{key}_INVOICE_ROLE")
    end
    

    I tried myself to do this and got two results. One using hash and one just cutting as much as possible.

    # Hash version
    def get_user_permissions
      return Set.new if !!@user
    
      user_permissions_hash = {:DEFAULT_PERMISSION => true,
                               :CM_TEAM_ROLE_PERMISSION => has_cm_team_role,
                               :CM_INVOICE_USER_PERMISSION => has_cm_invoice_view_role || has_invoice_finance_role || has_application_access,
                               :INVOICE_VIEW_PERMISSION => has_cm_invoice_view_role || has_invoice_finance_role,
                               :ACCESS_ALL_INVOICE_PERMISSION => has_cm_invoice_view_role || has_invoice_finance_role,
                               :FINANCE_INVOICE_PERMISSION => has_invoice_finance_role
      }
      %(cm pa sdt).each do |key|
        user_permissions_hash[:"#{key}_ANY_INVOICE_PERMISSION"] = has_application_access(:"#{key}_INVOICE_ROLE")
      end
    
      return user_permissions_hash.map {|k, v| k if v}.compact.to_set
    end
    
    # Normal version
    def get_user_permissions
      return (user_permissions = Set.new) if !!@user
    
      user_permissions << :DEFAULT_PERMISSION
      user_permissions << :CM_TEAM_ROLE_PERMISSION if has_cm_team_role
      user_permissions << :CM_INVOICE_USER_PERMISSION if has_cm_invoice_view_role || has_invoice_finance_role || has_application_access
      if (has_cm_invoice_view_role || has_invoice_finance_role)
        user_permissions << :INVOICE_VIEW_PERMISSION
        user_permissions << :ACCESS_ALL_INVOICE_PERMISSION
      end
      user_permissions << :FINANCE_INVOICE_PERMISSION if has_invoice_finance_role
      %(cm pa sdt).each do |key|
        user_permissions << :"#{key}_ANY_INVOICE_PERMISSION" if has_application_access(:"#{key}_INVOICE_ROLE")
      end
    
      user_permissions
    end