Portée des constantes dans les modules Ruby

J’ai un petit problème avec la scope constante des modules mixin. Disons que j’ai quelque chose comme ça

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

La constante USER_KEY doit être définie par défaut sur “utilisateur” sauf si elle est déjà définie. Maintenant, je pourrais mélanger cela à quelques endroits, mais dans un de ces endroits, USER_KEY doit être différent, donc nous pourrions avoir quelque chose comme ça.

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

Je m’attendrais à ce que USER_KEY soit “my_user” lorsqu’il est utilisé dans authorize, puisqu’il est déjà défini, mais qu’il rest “utilisateur”, issu de la définition des modules USER_KEY. Quelqu’un at-il une idée de comment obtenir l’autorisation d’utiliser la version de classes de USER_KEY?

Le USER_KEY vous avez déclaré (même conditionnellement) dans Auth est appelé globalement Auth::USER_KEY . Il n’est pas mélangé pour inclure des modules, bien que l’inclusion de modules puisse référencer la clé d’une manière non qualifiée.

Si vous voulez que chaque module (par exemple ApplicationController ) soit capable de définir son propre USER_KEY , essayez ceci:

 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 

Si vous voulez faire face à tous ces problèmes, vous pourriez tout aussi bien en faire une méthode de classe:

 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 

ou un accesseur au niveau de la classe:

 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 

Les constantes n’ont pas de scope globale dans Ruby. Les constantes peuvent être visibles depuis n’importe quelle scope, mais vous devez spécifier où la constante doit être trouvée. Lorsque vous commencez une nouvelle classe, un nouveau module ou une nouvelle définition, vous commencez une nouvelle étendue et si vous voulez une constante d’une autre scope, vous devez spécifier où la trouver.

 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 

Voici une solution simple.

Changements:

  • Pas besoin de vérifier l’existence de USER_KEY .
  • Essayez de rechercher la constante sur le module / la classe du récepteur (dans votre cas, ce serait le contrôleur). S’il existe, utilisez-le, sinon utilisez le module / classe par défaut (voir ci-dessous la valeur par défaut).

.

 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 

Explication

Le comportement que vous voyez n’est pas spécifique aux rails, mais est dû au fait que Ruby recherche les constantes si elles ne sont pas explicitement définies via :: (ce que j’appelle le “défaut” ci-dessus). Les constantes sont recherchées en utilisant la “scope lexicale du code en cours d’exécution”. Cela signifie que ruby ​​cherche d’abord la constante dans le module (ou la classe) du code d’exécution, puis se déplace vers chaque module (ou classe) englobant successivement jusqu’à ce qu’il trouve la constante définie sur cette étendue.

Dans votre contrôleur, vous appelez authorize . Mais lorsque l’ authorize est en cours d’exécution, le code en cours d’exécution est dans Auth . C’est donc là que les constantes sont recherchées. Si Auth n’avait pas USER_KEY , mais qu’un module le USER_KEY , alors celui qui le USER_KEY serait utilisé. Exemple:

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

Un cas particulier est l’environnement d’exécution de niveau supérieur, traité comme appartenant à la classe Object .

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

Un écueil consiste à définir un module ou une classe avec l’opérateur de scope ( :: :):

 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 

Notez que la constante peut être définie beaucoup plus tard que la méthode est définie. La recherche ne se produit que lorsque l’on accède à USER_KEY, donc cela fonctionne aussi:

 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. 

Si votre projet est dans Rails, ou du moins utilise le module ActiveSupport , vous pouvez réduire de manière significative le sucre logique nécessaire:

 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 

Je suis surpris que personne n'ait suggéré cette approche, vu que le scénario de l'OP se trouvait dans une application Rails ...

Il y a une solution beaucoup plus simple à la question du PO que les autres réponses révèlent ici:

 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 => # 2.3.1 :005 > r.test_it => "bar" 2.3.1 :006 > z = Baz.new => # 2.3.1 :007 > z.test_it => "foo" 

C’est la réponse de @james-a-rosen qui m’a inspiré pour essayer ceci. Je ne voulais pas suivre sa route car j’avais plusieurs constantes partagées entre plusieurs classes, chacune ayant une valeur différente, et sa méthode ressemblait beaucoup à la saisie.