Scope of Constants in Ruby Modules

58,213

Solution 1

The USER_KEY you declared (even conditionally) in Auth is globally known as Auth::USER_KEY. It doesn't get "mixed in" to including modules, though including modules can reference the key in a non-fully-qualified fashion.

If you want each including module (e.g. ApplicationController) to be able to define its own USER_KEY, try this:

module Auth
  DEFAULT_USER_KEY = 'user'
  def self.included(base)
    unless base.const_defined?(:USER_KEY)
      base.const_set :USER_KEY, Auth::DEFAULT_USER_KEY
    end
  end
  def authorize
    user_id = session[self.class.const_get(:USER_KEY)]
  end
end

class ApplicationController < ActionController::Base
  USER_KEY = 'my_user'
  include Auth
end

If you're going to go to all this trouble, though, you might as well just make it a class method:

module Auth
  DEFAULT_USER_KEY = 'user'
  def self.included(base)
    base.extend Auth::ClassMethods
    base.send :include, Auth::InstanceMethods
  end
  module ClassMethods
    def user_key
      Auth::DEFAULT_USER_KEY
    end
  end
  module InstanceMethods
    def authorize
      user_id = session[self.class.user_key]
    end
  end
end

class ApplicationController < ActionController::Base
  def self.user_key
    'my_user'
  end
end

or a class-level accessor:

module Auth
  DEFAULT_USER_KEY = 'user'
  def self.included(base)
    base.send :attr_accessor :user_key unless base.respond_to?(:user_key=)
    base.user_key ||= Auth::DEFAULT_USER_KEY
  end
  def authorize
    user_id = session[self.class.user_key]
  end
end

class ApplicationController < ActionController::Base
  include Auth
  self.user_key = 'my_user'
end

Solution 2

Constants don't have global scope in Ruby. Constants can be visible from any scope, but you must specify where the constant is to be found. When you begin a new class, module, or def, you begin a new scope, and if you want a constant from another scope, you have to specify where to find it.

X = 0
class C
  X = 1
  module M
    X = 2
    class D
      X = 3
      puts X          # => 3
      puts C::X       # => 1
      puts C::M::X    # => 2
      puts M::X       # => 2
      puts ::X        # => 0
    end
  end
end

Solution 3

Here's a simple solution.

Changes:

  • No need to check for existence of USER_KEY.
  • Try to look up the constant on the receiver's module/class (in your case it would be the controller). If it exists, use it, otherwise use the default module/class (see below for what the default is).

.

module Auth
  USER_KEY = "user"

  def authorize
    user_key = self.class.const_defined?(:USER_KEY) ? self.class::USER_KEY : USER_KEY
    user_id = session[user_key]
  def
end

Explanation

The behavior you're seeing isn't specific to rails, but is due to where ruby looks for constants if not explicitly scoped via :: (what I call the "default" above). Constants are looked up using the "lexical scope of the currently executing code". This means that ruby first looks for the constant in the executing code's module (or class), then moves outward to each successive enclosing module (or class) until it finds the constant defined on that scope.

In your controller, you call authorize. But when authorize is executing, the currently executing code is in Auth. So that is where constants are looked up. If Auth didn't have USER_KEY, but an enclosing module has it, then the enclosing one would be used. Example:

module Outer
  USER_KEY = 'outer_key'
  module Auth
     # code here can access USER_KEY without specifying "Outer::"
     # ...
  end
end

A special case of this is the top-level execution environment, which is treated as belonging to class Object.

USER_KEY = 'top-level-key'
module Auth
  # code here can access the top-level USER_KEY (which is actually Object::USER_KEY)
  # ...
end

One pitfall is defining a module or class with the scoping operator (::):

module Outer
  USER_KEY = 'outer_key'
end
module Outer::Auth
  # methods here won't be able to use USER_KEY,
  # because Outer isn't lexically enclosing Auth.
  # ...
end

Note that the constant can be defined much later than the method is defined. The lookup only happens when USER_KEY is accessed, so this works too:

module Auth
  # don't define USER_KEY yet
  # ...
end

# you can't call authorize here or you'll get an uninitialized constant error

Auth::USER_KEY = 'user'

# now you can call authorize.

Solution 4

If your project is in Rails, or at least utilizes the ActiveSupport module, you can significantly reduce the necessary logic sugar:

module Auth

  extend ActiveSupport::Concern

  included do
    # set a global default value
    unless self.const_defined?(:USER_KEY)
      self.const_set :USER_KEY, 'module_user'
    end
  end

end

class ApplicationController < ActionController::Base
  # set an application default value
  USER_KEY = "default_user"
  include Auth  
end

class SomeController < ApplicationController
  # set a value unique to a specific controller
  USER_KEY = "specific_user"
end

I'm surprised no one suggested this approach, seeing as how the OP's scenario resided within a Rails app...

Solution 5

There's a far simpler solution to the OP's question than the other answers here reveal:

module Foo
  THIS_CONST = 'foo'

  def show_const
    self.class::THIS_CONST
  end
end

class Bar
  include Foo

  THIS_CONST ='bar'
  def test_it
    show_const
  end
end

class Baz
  include Foo

  def test_it
    show_const
  end
end

2.3.1 :004 > r = Bar.new
 => #<Bar:0x000000008be2c8> 
2.3.1 :005 > r.test_it
 => "bar" 
2.3.1 :006 > z = Baz.new
 => #<Baz:0x000000008658a8> 
2.3.1 :007 > z.test_it
 => "foo" 

It was @james-a-rosen's answer that gave me the inspiration to try this. I didn't want to go his route because I had several constants that are shared among several classes, each with a different value, and his method looked like a lot of typing.

Share:
58,213
user204078
Author by

user204078

Updated on July 08, 2022

Comments

  • user204078
    user204078 almost 2 years

    I'm having a little problem with constant scope in mixin modules. Let's say I have something like this

    module Auth
    
      USER_KEY = "user" unless defined? USER_KEY
    
      def authorize
        user_id = session[USER_KEY]
      def
    
    end
    

    The USER_KEY constant should default to "user" unless it's already defined. Now I might mix this into a couple of places, but in one of those places the USER_KEY needs to be different, so we might have something like this

    class ApplicationController < ActionController::Base
    
      USER_KEY = "my_user"
    
      include Auth
    
      def test_auth
        authorize
      end
    
    end
    

    I would expect that USER_KEY would be "my_user" when used in authorize, since it's already defined, but it's still "user", taken from the modules definition of USER_KEY. Anyone have any idea how to get authorize to use the classes version of USER_KEY?

  • Lloeki
    Lloeki about 11 years
    For the sake of completeness, you forgot X = 0 outside the class and puts ::X.
  • Nick
    Nick about 10 years
    @Lloeki - The answer has been edited to include your comment.
  • Zequez
    Zequez over 8 years
    It's worth noting that if you open the class or module with the shorthand like class C::M::D; end, inside you won't be able to access the M scope directly so M::X would be undefined, you only could access it like C::M::X.
  • Joshua Pinter
    Joshua Pinter about 6 years
    I'm surprised that puts ::X doesn't return 2 (the value of X in module M. So what does :: mean? The "top-level"?
  • Peter H. Boling
    Peter H. Boling over 3 years
    Yes, :: at the front of a constant means "top-level" in Ruby. You can add any namespaces you want after it, e.g. ::C::M::D::X in the example would be the same as C::M::D::X, because C is a top level constant.