Guaranteeing that module includer classes include another module in Ruby

Viewed 36

I am using the SemanticLogger for logging. It provides a Loggable module that classes can include to create logger class and instance methods that return a class-specific logger object.

I am working with a module in my own code base that requires access to a logger method in its instance methods. If I call include SemanticLogger::Loggable in the module, it makes a logger method accessible to module "class" methods, but not instance methods, since modules don't have instances and the module instance methods become methods of the including class.

So for the logger call not to fail, I need to ensure that any class that includes my module also includes SemanticLogger::Loggable. What's the best way to deal with this? Maybe this?:

module MyModule
# ...
  def included(_includer_module_or_class)
    require 'semantic_logger'
    include SemanticLogger::Loggable
  end
...
end
1 Answers

SemanticLogger is a Class-Level Mixin

SemanticLogger is a complex bit of code, and some of its internals aren't very well documented. The key to understanding what it's really doing (and why you're having problems) is that under the hood it's really creating class-level objects that are then accessible to instances. In particular, :logger is an accessor defined in the context of the including class, which is why you can't simply add it to an #included block without being very explicit about what self is when you include SemanticLogger::Loggable.

Call #class_exec on the Including Class

Once you understand that, the solution becomes a little more self-evident: you have to call Module#class_exec on the including class inside the module method :included when including your module. In other words, make sure #included is a module method and that inside the method self is the including class rather than the enclosing module. For example:

require "bundler/setup"
require "semantic_logger"

module MyModule
  def self.included klass
    klass.class_exec do
      include SemanticLogger
      include SemanticLogger::Loggable

      SemanticLogger[klass]
      # any other class-level configuration you want
      # for SementicLogger or your :logger accessor
    end
  end
end

Mixing Your Customized Logging Module Into a Class

Once you have the mixin properly defined, you can mix it into any class easily. For example:

class Foo
  include MyModule
end

@foo = Foo.new

both the Foo class and the @foo instance will have access to SemanticLogger capabilities, and you'll be able to call @foo.logger.info "Test Message" on your instance from outside your class, or simply use :logger as an accessor from within your Foo class.

If there's a more elegant way to do this, I haven't found it yet. This works consistently for me, and should at least get you started down the right path when using customized SemanticLogger mixins within your own classes.

Related