Search code examples
ruby-on-railsrubymetaprogrammingactivesupport-concern

Define class and instance methods dynamically in concern


I have a concern:

# app/models/concerns/rolable.rb
module Rolable
  extend ActiveSupport::Concern

  included do
    rolify
    Role.find_each do |role|
      scope "#{role.name.pluralize}", -> { joins(:roles).where(roles: {name: send("#{role.name}_role_name")}).distinct }
    end

  end

 class_methods do
   Role.find_each do |role|
     define_method "#{role.name}_role_name" do
       role.name
     end

     define_method "#{role.name}_role_id" do
       role.id
     end
   end
 end

 Role.find_each do |role|
   define_method("#{role.name}?") do
     has_role? self.class.send("#{role.name}_role_name")
   end
 end

end

As you can see it defines a bunch of scopes, class methods and instance methods. But I'm not happy about repetition of Role.find_each do |role| ... end.

How can I eliminate this duplication? I tried this

Role.find_each do |role|
  included do
    ...
  end
  class_methods do
    ...
  end
end

But it doesn't work because of multiple included blocks. I can extract Role.find_each in method, but it's not much better.

How can improve this code and remove duplication?


Solution

  • If you are pretty sure that your inventory of roles won't expand, then maybe you can dynamically define a bunch of anonymous concerns instead of creating one concern for all roles.

    # models/concerns/rolables.rb
    # no need to call `find_each' because the number or roles will never exceed 1000
    Rolables = Role.all.map do |role|
      Module.new do
        extend ActiveSupport::Concern
    
        included do
          scope "#{role.name.pluralize}", -> { joins(:roles).where(roles: {name: send("#{role.name}_role_name")}).distinct }
    
          define_method("#{role.name}?") do
            has_role? self.class.send("#{role.name}_role_name")
          end
        end
    
        class_methods do
          define_method "#{role.name}_role_name" do
            role.name
          end
    
          define_method "#{role.name}_role_id" do
            role.id
          end
        end
      end
    end
    

    And include all those concerns into your model:

    # models/user.rb
    class User
      Rolables.each{|concern| include concern}
    end