web-dev-qa-db-fra.com

Portée des constantes dans les modules Ruby

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?

48
user204078

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
53
James A. Rosen

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
38
Fred

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 pour le type 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 à 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.
12
Kelvin

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 ...

1
Frank Koehl

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.

0
Derrell Durrett