Rails: Carga dinamica de constantes

Antes de empezar y por si alguien quiere, aquí tiene una brevísima intruducción a las constantes en Ruby

Vamos entonces con un ejemplo sencillo en una consola de Ruby (sin el entorno de Rails cargado):

madtrick::ruby$ irb
irb(main):001:0> Constante
NameError: uninitialized constant Constante
        from (irb):1
irb(main):002:0>

Como la constante no esta definida en el contexto donde la reclamamos, se genera una excepción.

Pero si queremos, podemos capturar esta “referencia a una constante indefinida” dentro del contexto en el que nos encontramos, redefiniéndo el método const_missing de Object:

irb(main):010:0> class << Module
irb(main):011:1> def const_missing(name)
irb(main):012:2> p "Constante infenida: #{name}"
irb(main):013:2> end
irb(main):014:1> end
=> nil
irb(main):015:0> Constante
"Constante infenida: Constante"
=> nil

Si queremos que la excepción continúe debemos de indicarlo con un raise al final del método:

irb(main):016:0> class << Module
irb(main):017:1> def const_missing(name)
irb(main):018:2> p "Constante infenida: #{name}"
irb(main):019:2> raise
irb(main):020:2> end
irb(main):021:1> end
=> nil
irb(main):022:0> Constante
"Constante infenida: Constante"
RuntimeError: 
        from (irb):19:in `const_missing'
        from (irb):22
        from :0

Rails , se aprovecha de esta funcionalidad para cargar dinámicamente constantes.¿Cómo?.Éso es lo que explicaremos a continuación:

Para seguir esta explicación de una manera mas cómoda, recomiendo tener a mano el fichero dependencies.rb incluido en active-support.

1º.HOYGA NO HENCUENTRO LA CONSTANTE

Rails , al igual que hicimos nosotros antes , abre las clases Module y Class para redefinir el método const_missing.

class Module #:nodoc:
  ...
  # Use const_missing to autoload associations so we don't have to
  # require_association when using single-table inheritance.
  def const_missing(class_id)
    ActiveSupport::Dependencies.load_missing_constant self, class_id
  end
...
end
 
class Class
  def const_missing(const_name)
    if [Object, Kernel].include?(self) || parent ==
      super
    else
      begin
        begin
          ActiveSupport::Dependencies.load_missing_constant self, const_name
        rescue NameError
          parent.send :const_missing, const_name
        end
      rescue NameError => e
        # Make sure that the name we are missing is the one that caused the error
        parent_qualified_name = ActiveSupport::Dependencies.qualified_name_for parent, const_name
        raise unless e.missing_name? parent_qualified_name
        qualified_name = ActiveSupport::Dependencies.qualified_name_for self, const_name
        raise NameError.new("uninitialized constant #{qualified_name}").copy_blame!(e)
      end
    end
  end
end

No entiendo porque en el método const_missing redefinido en Class comprobamos si self es Kernel ya que Kernel es un Module y por tanto nunca invocaria al const_missing de una clase.
Pero ambas “redefiniciones” nos llevan al mismo punto: ActiveSupport::Dependencies.load_missing_constant

2º.Carguémos la constante

Una vez dentro de load_missing_constant lo primero que se lleva a cabo es una comprobacion para determinar si el modulo o clase que desea cargar la constante es Kernel para en caso afirmativo intercambiar este por Object, si Object no tiene definida la constante deseada o devolver el valor de la constante en caso contrario.Esto tiene sentido ya que Kernel es un mixin de Object y por tanto sus constantes lo serán a la vez de Object.

if from_mod == Kernel
        if ::Object.const_defined?(const_name)
          log "Returning Object::#{const_name} for Kernel::#{const_name}"
          return ::Object.const_get(const_name)
        else
          log "Substituting Object for Kernel"
          from_mod = Object
        end
      end

En caso de no encontrarse dicha constante en Object pasamos a lo siguiente:

from_mod = Object if from_mod.name.blank?
 
      unless qualified_const_defined?(from_mod.name) && from_mod.name.constantize.object_id == from_mod.object_id
        raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
      end

Con lo que comprobamos:

  • Si estamos ante un modulo anónimo
  • La segunda parte no me queda muy claro cuando se puede dar

En la siguiente linea de código, se me plantea otra nueva duda

raise ArgumentError, "#{from_mod} is not missing constant #{const_name}!" if uninherited_const_defined?(from_mod, const_name)

Si estamos buscando una constante (const_name) no definida en from_mod, ¿Por qué uninherited_const_defined?(from_mod,const_name) habría de devolver true? Uninherited_const_defined? es como sigue:

if Module.method(:const_defined?).arity == 1
      # Does this module define this constant?
      # Wrapper to accomodate changing Module#const_defined? in Ruby 1.9
      def uninherited_const_defined?(mod, const)
        mod.const_defined?(const)
      end
    else
      def uninherited_const_defined?(mod, const) #:nodoc:
        mod.const_defined?(const, false)
      end
    end

Dependiendo de la versión de Ruby ( 1.9 o inferior) utilizara un método un otro con mismo resultado: invocar const_defined? sobre el módulo.Y esto es lo que no entiendo, si const_name genero un const_missing en dicho modulo, porque ahora no habría de hacerlo….

En fin, sigamos.

qualified_name = qualified_name_for from_mod, const_name
path_suffix = qualified_name.underscore
name_error = NameError.new("uninitialized constant #{qualified_name}")

qualified_name_for devuelve el nombre cualificado para el modulo y constante dados

# Return the constant path for the provided parent and constant name.
    def qualified_name_for(mod, name)
      mod_name = to_constant_name mod
      (%w(Object Kernel).include? mod_name) ? name.to_s : "#{mod_name}::#{name}"
    end

Ejemplos:

  • Para mod A y name C , tendriamos A::C
  • Para mod A::C y name D, tendriamos A::C::D
  • Para mod Object (o Kernel) y name C, tendriamos C


path_suffix = qualified_name.underscore
.De esta linea obtendremos lo siguiente, ejemplos:

  • Para qualified_name A::C , tendremos a/c
  • Para qualified_name A::B::C, tendremos a/b/c
  • Para qualified_name A, tendremos a

Y por ultimo name_error = NameError.new prepara un objeto de clase NameError para el caso de que no podamos hacer nada por cargar la constante.

3º.¿Pero cargamos la constante o no?

Hasta ahora no hemos realizado ningún intento por cargar la constante perdida (buen titulo para una peli…Las aventuras de la constante perdida), pero a partir de ahora es cuando empieza lo divertido.

Para empezar intentamos buscar un directorio en nuestro load_path (definido en Dependencies.load_paths) que albergue una ruta igual a path_suffix (indicamos como se determina dicho path_suffix, lineas mas arriba) con terminación “.rb”.

file_path = search_for_file(path_suffix)

Es decir, si nuestro path_suffix es “a/b/c.rb”, buscamos algún path de entre los que tenemos disponibles para cargar ficheros que contenga la ruta “a/b/c.rb”.Ejemplo:

Si nuestro load_path esta formado por:

  • /home/lib/funny_lib, la ruta completa seria /home/lib/funny_lib/a/b/c.rb
  • /home/lib/dll_hell, la ruta completa seria /home/lib/dll_hell/a/b/c.rb
  • /home/lib/act_as_pr0n, la ruta completa seria /home/lib/act_as_pr0n/a/b/c.rb

El primero de los anteriores que exista será el valor devuelto por search_for_file o nil en caso que ninguno de ellos exista.

Lo siguiente que nos encontramos es una gran estructura condicional

      if file_path && ! loaded.include?(File.expand_path(file_path)) # We found a matching file to load
        require_or_load file_path
        raise LoadError, "Expected #{file_path} to define #{qualified_name}" unless uninherited_const_defined?(from_mod, const_name)
        return from_mod.const_get(const_name)
      elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
        return mod
      elsif (parent = from_mod.parent) && parent != from_mod &&
            ! from_mod.parents.any? { |p| uninherited_const_defined?(p, const_name) }
        # If our parents do not have a constant named +const_name+ then we are free
        # to attempt to load upwards. If they do have such a constant, then this
        # const_missing must be due to from_mod::const_name, which should not
        # return constants from from_mod's parents.
        begin
          return parent.const_missing(const_name)
        rescue NameError => e
          raise unless e.missing_name? qualified_name_for(parent, const_name)
          raise name_error
        end
      else
        raise name_error
      end

Vayamos por partes

3.1 Cargando la constante desde un fichero

if file_path && ! loaded.include?(File.expand_path(file_path)) # We found a matching file to load
        require_or_load file_path
        raise LoadError, "Expected #{file_path} to define #{qualified_name}" unless uninherited_const_defined?(from_mod, const_name)
        return from_mod.const_get(const_name)

Si se da que tenemos un file_path, y este no esta cargado actualmente, lo cargamos mediante require_or_load.Este método necesita para el solo otra entrada en el blog de lo amplio que es.

Aquí podemos apreciar por tanto otra convención de Rails.Es decir, si por ejemplo tenemos un plugin que añade una constante a ActiveRecord (sea la constante Constant) y queremos que Rails cargue automáticamente esta constante sin tener que indicarlo nosotros explícitamente mediante un require, debemos de:

  • En el directorio /lib de nuestro plugin crear otro que se llame active_record
  • Dentro de este ultimo directorio crear un fichero que se llame constant.rb

Si no cumplimos dicha convencion, tras cargar el fichero podemos obtener el siguiente error

raise LoadError, "Expected #{file_path} to define #{qualified_name}" unless uninherited_const_defined?(from_mod, const_name)

Hay que tener cuidado con este método ya que como se indico anteriormente, el file_path elegido sera el primero que exista, es decir que si hay dos plugins con la misma estructura y en ninguno de ellos requerimos explícitamente el fichero donde se define la constante el primero en ser elegido ocultara la presencia del otro.

3.2

elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
        return mod

con autoload_module! como sigue,

# Attempt to autoload the provided module name by searching for a directory
# matching the expect path suffix. If found, the module is created and assigned
# to +into+'s constants with the name +const_name+. Provided that the directory
# was loaded from a reloadable base path, it is added to the set of constants
# that are to be unloaded.
    def autoload_module!(into, const_name, qualified_name, path_suffix)
      return nil unless base_path = autoloadable_module?(path_suffix)
      mod = Module.new
      into.const_set const_name, mod
      autoloaded_constants << qualified_name unless load_once_paths.include?(base_path)
      return mod
    end

Como bien indica el comentario, autoload_module! primero determina mediante autoloadable_module? si existe un path de entre los disponibles en nuestro load_path al cual añadiéndole el path_suffix, exista como directorio.Si es así, continuamos, sino devolvemos nil.

Si existe dicho path, creamos un modulo anónimo y añadimos la constante que falta a from_mod con el valor de este modulo anónimo.
Para terminar en autoload_module!, incluimos la constante cualificada en Dependencies.autoloaded_constants a menos que el directorio sea un directorio que solo se puede cargar una vez durante la ejecución de Rails (definidos en Dependencies.load_once_paths).

3.3 Ni fichero, ni directorio.¿Dónde coj***s esta la constante?

Llegados a este punto es porque ningún load_path de los disponibles alberga o bien un directorio o bien un fichero que siga la estructura designada por path_suffix.

elsif (parent = from_mod.parent) && parent != from_mod &&
            ! from_mod.parents.any? { |p| uninherited_const_defined?(p, const_name) }
        # If our parents do not have a constant named +const_name+ then we are free
        # to attempt to load upwards. If they do have such a constant, then this
        # const_missing must be due to from_mod::const_name, which should not
        # return constants from from_mod's parents.
        begin
          return parent.const_missing(const_name)
        rescue NameError => e
          raise unless e.missing_name? qualified_name_for(parent, const_name)
          raise name_error
        end

Resumiendo que esto se alarga demasiado….en este ultimo bloque, buscaremos la constante en los padres de from_mod.La explicacion a como se obtienen los padres para un path dado es la siguiente:

En introspection.rb:

# Returns all the parents of this module according to its name, ordered from
# nested outwards. The receiver is not contained within the result.
#
# module M
# module N
# end
# end
# X = M::N
#
# p M.parents # => [Object]
# p M::N.parents # => [M, Object]
# p X.parents # => [M, Object]
#

y si tampoco esta definida en ninguno de sus padres, invocaremos denuevo todo el proceso desde el principio sobre el padre de from_mod

return parent.const_missing(const_name)

Mencionar que si no encontramos la constante en ningun sitio se lanzara la excepcion de clase NameError creada anteriormente.

Category: Uncategorized | Tags: Comment »


Leave a Reply