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: dependencies rails constantes Comment »