Search code examples
ruby-on-railsruby

Accessing class constant from within a module


I need to access an ActiveRecord constant from an included module in a Rails app.

class User < ApplicationRecord
  include ModOne
  
  OPTIONS = {a: 1}.freeze

  ...
end
Module ModOne
  extend ActiveSupport::Concern

  included do
    do_semething(self::OPTIONS)
  end

  class_methods do
   def do_something(opts)
     ...
   end
  end
end

But I get

NameError: uninitialized constant User (call 'User.connection' to establish a connection)::OPTIONS Did you mean? User::OPTIONS

What am I missing here?

I have also tried to replace self with base_class and event User but I get the same error.


Solution

  • It's simply a matter of order. You have to define the constant first. Ruby classes are really just a block of code and run top down and when you call include you're also calling the #included method on the module.

    But a better approach if you want the functionality provided by a module to be customizable is to just write a so called macro method instead of hinging everything on the Module#included hook:

    # You can obscure this with some syntactic sugar from ActiveSupport::Concern 
    # if it makes you happy.
    module Awesomeizer
      def foo
        self.class.awesomeize_options[:foo]
      end
    
      module ClassMethods
        def awesomeize_options
          @awesomeize_options ||= defaults
        end
    
        def awesomeize(**options) 
           awesomeize_options.merge!(options)
        end 
    
        def defaults
          {}
        end
      end
    
      def self.included(base)
        base.extend(ClassMethods)
      end
    end
    
    class Thing
      include Awesomeizer
      awesomeize(foo: :bar)
    end
    

    This pattern can be found everywhere in Ruby and is great way to get around the fact that Module#include doesn't allow you to pass any additional arguments.

    In this example the awesomeize method just stores the options as a class instance variable.

    One of the strongest reasons why this is preferable is that it lets you define a signature for the interface between the module and it's consumers instead of just making assumptions. Some gems like Devise even use this method to include it's submodules.