J'ai un petit problème avec une portée constante dans les 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 par défaut être "utilisateur" sauf si elle est déjà définie. Maintenant, je pourrais mélanger cela à plusieurs endroits, mais dans un de ces endroits, USER_KEY doit être différent, de sorte que nous pourrions avoir quelque chose comme ceci.
class ApplicationController < ActionController::Base
USER_KEY = "my_user"
include Auth
def test_auth
authorize
end
end
Je m'attendrais à ce que USER_KEY soit "mon_utilisateur" lorsqu'il est utilisé dans authorize, car il est déjà défini, mais reste "utilisateur", tiré de la définition des modules de USER_KEY. Quelqu'un sait-il comment obtenir l'autorisation d'utiliser la version de classes de USER_KEY?
Le USER_KEY
que vous avez déclaré (même de manière conditionnelle) dans Auth
est globalement appelé Auth::USER_KEY
. Il n'est pas "mélangé" pour inclure des modules, bien que des modules can fassent référence à la clé de manière non qualifiée.
Si vous voulez que chaque module inclus (par exemple, ApplicationController
) puisse 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 allez avoir tous ces problèmes, vous pouvez aussi 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 de niveau 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 portée globale en Ruby. Les constantes peuvent être visibles à partir de n'importe quelle étendue, mais vous devez spécifier où la constante doit être trouvée. Lorsque vous commencez une nouvelle classe, un nouveau module ou un nouveau def, vous commencez une nouvelle portée et si vous voulez une constante d'une autre portée, 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:
USER_KEY
..
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 à Rails, mais est dû au fait que Ruby recherche des constantes si elles ne sont pas explicitement définies via ::
(ce que j'appelle le "défaut" ci-dessus). Les constantes sont recherchées dans "l'étendue lexicale du code en cours d'exécution". Cela signifie que Ruby commence par rechercher la constante dans le module (ou la classe) du code d'exécution, puis se déplace vers chaque module (ou classe) englobant successif jusqu'à ce qu'il trouve la constante définie dans cette étendue.
Dans votre contrôleur, vous appelez authorize
. Mais lorsque 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’il en soit doté par un module englobant, celui-ci serait utilisé. Exemple:
module Outer
USER_KEY = 'outer_key'
module Auth
# code here can access USER_KEY without specifying "Outer::"
# ...
end
end
L'environnement d'exécution de niveau supérieur, considéré comme appartenant à la classe Object
, constitue un cas particulier.
USER_KEY = 'top-level-key'
module Auth
# code here can access the top-level USER_KEY (which is actually Object::USER_KEY)
# ...
end
Un des pièges est de définir un module ou une classe avec l'opérateur de cadrage (::
):
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 vous accédez à USER_KEY, cela fonctionne donc 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 utilise au moins le module ActiveSupport
, vous pouvez réduire considérablement 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 du PO se situait dans une application Rails ...
Il existe une solution beaucoup plus simple à la question du PO que ne le révèlent les autres réponses:
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"
C'est la réponse de @ james-a-rosen qui m'a donné l'inspiration pour essayer cela. Je ne voulais pas suivre sa route car j'avais plusieurs constantes partagées entre plusieurs classes, chacune avec une valeur différente, et sa méthode ressemblait beaucoup à une dactylographie.