Scope of Constants in Ruby Modules

Ruby on-RailsRubyModuleConstants

Ruby on-Rails Problem Overview


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?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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 - Ruby on-Rails

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 - Ruby on-Rails

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 - Ruby on-Rails

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 - Ruby on-Rails

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser204078View Question on Stackoverflow
Solution 1 - Ruby on-RailsJames A. RosenView Answer on Stackoverflow
Solution 2 - Ruby on-RailsFredView Answer on Stackoverflow
Solution 3 - Ruby on-RailsKelvinView Answer on Stackoverflow
Solution 4 - Ruby on-RailsFrank KoehlView Answer on Stackoverflow
Solution 5 - Ruby on-RailsDerrell DurrettView Answer on Stackoverflow