Scope of Constants in Ruby Modules
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)] endendclass ApplicationController < ActionController::Base USER_KEY = 'my_user' include Authend
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 endendclass ApplicationController < ActionController::Base def self.user_key 'my_user' endend
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] endendclass ApplicationController < ActionController::Base include Auth self.user_key = 'my_user'end
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 = 0class 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 endend
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] defend
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::" # ... endend
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'endmodule 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 errorAuth::USER_KEY = 'user'# now you can call authorize.