class Namebox
Namebox
for Ruby
© 2013 Sony Fermino dos Santos rubychallenger.blogspot.com.br/2013/01/namebox.html
License: Public Domain
This software is released “AS IS”, without any warranty. The author is not responsible for the consequences of use of this software.
This version is only compatible with Ruby 1.9.2 or greater. For use with Ruby 1.8.7 or 1.9.1, get the version 0.1.8 or 0.1.9.
Constants
- CORE
Currently loaded top-level modules
Public Class Methods
Get the caller info in a structured way (hash).
# File lib/namebox.rb, line 86 def caller_info # search for last reference to this file in caller and take the next one cr = caller.reverse c = cr[cr.index { |s| s.start_with? __FILE__ } - 1] raise "Unable to find a valid caller file in #{caller.inspect}" unless c # convert into hash caller_to_hash c end
# File lib/namebox.rb, line 110 def caller_to_hash a_caller # match the info m = a_caller.match(/^(.*?):(\d+)(:in `(.*)')?$/) raise "Unexpected caller syntax in \"#{a_caller}\"" unless m # label them {:file => m[1], :line => m[2].to_i, :method => m[4]} end
Array with default modules to protect. You may want redefine this, protecting Namebox
itself inside another namebox, and assign it to a constant, which can be available thru other files in the project.
# File lib/namebox.rb, line 81 def default_modules (@default_modules ||= {})[caller_info[:file]] || [] end
Set the default modules to protect (file-wide, for the caller file). Each file you want to use default_modules, you must define them again. This is to avoid conflicts when using other people libraries, which may want to protect different modules by default. See default_modules
# File lib/namebox.rb, line 73 def default_modules= modules_to_protect (@default_modules ||= {})[caller_info[:file]] = [modules_to_protect].flatten end
modules_to_protect
must be the classes themselves, e.g., String, Symbol, not their names (“String” or :Symbol).
Special names are:
:all
=> protect all known modules and submodules. It’s safer but slower.
:core
=> protect the known top-level modules.
:default
=> protect the modules defined in Namebox.default_modules
.
Obs.: :core
and :default
can be used together and/or with other classes, but :all
must be the only parameter if it is used.
# File lib/namebox.rb, line 132 def initialize *modules_to_protect, &blk # initialize @enabled_files = [] @protected_methods = [] # this_nb will be useful in a closure, where self will be different this_nb = self # by default, get Namebox.default_modules modules_to_protect = Namebox.default_modules if modules_to_protect.empty? # check for :all modules if modules_to_protect.include? :all # :all must be the only parameter if modules_to_protect.length > 1 raise "If :all is used in Namebox.new, :all must be the only parameter!" end # get all modules @modules = ObjectSpace.each_object(Module).to_a else # take off the symbols modules = modules_to_protect.select { |m| m.is_a? Module } @modules = modules # include default modules if wanted @modules += Namebox.default_modules if modules_to_protect.include? :default # include core modules if wanted @modules += CORE if modules_to_protect.include? :core # avoid redundancy @modules.uniq! # include all ancestors for modules and singleton classes of classes modules.each do |m| @modules |= m.ancestors @modules |= m.singleton_class.ancestors if m.is_a? Class end end # modules must be given or Namebox.default_modules must be set if @modules.empty? raise ("Modules to protect were not given and there's no " + "Namebox.default_modules defined for file " + Namebox.caller_info[:file]) end # select classes to protect against included modules @classes = @modules.select { |m| m.is_a? Class } # get singleton_classes singleton_classes = @classes.map { |c| c.singleton_class } # include singleton_classes into @classes and @modules @classes |= singleton_classes @modules |= singleton_classes # permits to call the previous (_old) version of the method. @modules.each { |m| m.send(:include, Namebox::Old) unless m == Namebox::Old } # save preexisting methods and included modules inc_mods_before = get_included_modules methods_before = get_methods # ############################################################### # RUN THE CODE, which can change the methods of protected modules # blk.call # # ############################################################### # get data after changes inc_mods_after = get_included_modules methods_after = get_methods # compare with preexisting data to discover affected methods and modules # compare included modules (before vs after) unless inc_mods_after == inc_mods_before inc_mods_after.each do |klass, inc_mods| old_modules = inc_mods_before[klass] new_modules = inc_mods - old_modules # there's no new included module for this class next if new_modules.empty? new_modules.each do |new_module| # Get a protector module for new_module; don't recreate it # if a protector was already created for new_module. # protector = protector_module(new_module) # reincludes the new_module with super_tunnel # to allow bind(self) inside protector code. # klass.send :include, new_module # finally, include the protector in the class klass.send :include, protector end end end # Compare changed methods (before vs after) unless methods_after == methods_before methods_after.each do |fullname, info| # get old method info_old = methods_before[fullname] || {} new_method = info[:method] old_method = info_old[:method] # don't touch unmodified methods next if new_method == old_method # method was modified! take some info method_name = info[:name] klass = info[:class] # redefine the method, which will check namebox visibility dinamically klass.send :define_method, method_name do |*args, &blk| # check namebox visibility if this_nb.open? && !_calling_old?(fullname) # namebox method; bind instance method to self. r = new_method.bind(self).call(*args, &blk) else # old method or super if old_method old_method.bind(self).call(*args, &blk) else super(*args, &blk) end end end end end end
Raises NoMethodError, limiting the length of obj.inspect
.
# File lib/namebox.rb, line 99 def no_method_error(obj, m_name) # if inspect is too big, shorten it obj_name = obj.inspect.to_s obj_name = obj_name[0..45] + '...' + obj_name[-1] if obj_name.length > 50 msg = "Undefined method `#{m_name}' for #{obj_name}:#{obj.class}" raise NoMethodError.new(msg) end
Wrapper to create a namebox only to protect modules when requiring.
# File lib/namebox.rb, line 57 def require resource, *modules_to_protect new(*modules_to_protect) do # need to refer to top-level binding, which is lost inside this def. TOPLEVEL_BINDING.eval("require '#{resource}'") end end
Public Instance Methods
Close the namebox visible region in the caller file.
# File lib/namebox.rb, line 302 def close info = ranges_info # there must be an open range in progress unless info[:last] raise "Namebox was not opened in #{info[:file]} before line #{info[:line]}" end # begin of range must be before end r_beg = info[:last] r_end = info[:line] unless r_end >= r_beg raise ("Namebox#close in #{info[:file]}:#{r_end} should be after " + "Namebox#open (line #{r_beg})") end # replace the single initial line with the range, making sure it's unique r = Range.new(r_beg, r_end) info[:ranges].pop info[:ranges] << r unless info[:ranges].include? r end
Open namebox in entire caller file (valid only after called!).
# File lib/namebox.rb, line 351 def file_wide @enabled_files << Namebox.caller_info[:file] end
Open a namebox region for visibility in the caller file at caller line.
# File lib/namebox.rb, line 288 def open info = ranges_info # there must be no open range if info[:last] raise "Namebox was already opened in #{info[:file]}:#{info[:last]}" end # range in progress info[:ranges] << info[:line] end
Check namebox visibility (openness).
# File lib/namebox.rb, line 325 def open? # check file before checking ranges ci = Namebox.caller_info return true if @enabled_files.include? ci[:file] # check ranges for this file info = ranges_info ci info[:ranges].each do |r| case r when Range # check if caller is in an open range for this namebox return true if r.include?(info[:line]) when Integer # check if caller is after an initied range (Namebox#open) return true if info[:line] >= r end end false end
Private Instance Methods
Stores enabled line ranges of caller files for this module.
# File lib/namebox.rb, line 371 def enabled_ranges @enabled_ranges ||= {} end
Get modules included by the classes being protected.
# File lib/namebox.rb, line 392 def get_included_modules inc_mods = {} @classes.each do |c| super_c = c.superclass # superclass included modules must be [] even when superclass is nil super_inc_mods = super_c && super_c.included_modules || [] # get modules included by the class only, not by superclasses. inc_mods[c] = c.included_modules - super_inc_mods end inc_mods end
Get methods in use on modules to protect.
# File lib/namebox.rb, line 376 def get_methods gm = {} @modules.each do |c| c.instance_methods(false).each do |m| fullname = "#{c}##{m}" gm[fullname] = {:class => c, :name => m, :method => c.instance_method(m)} end end gm end
# File lib/namebox.rb, line 408 def protector_module(new_module) @protector_modules ||= {} if @protector_modules[new_module] @protector_modules[new_module] else this_nb = self # Create a protector module to protect the changed methods. protector = Module.new # Create a module to enable a tunnel to bypass the new_module # in super method lookup when namebox is closed to new_module. # super_tunnel = Module.new # Give a name to protector module; useful when using Method#owner. # protector.singleton_class.send(:define_method, :to_s) do "Protector:#{new_module}" end new_module.instance_methods.each do |method_name| new_method = new_module.instance_method(method_name) protector.send :define_method, method_name do |*args, &blk| # check namebox visibility fullname = "#{protector}##{method_name}" if this_nb.open? && !_calling_old?(fullname) # namebox method; bind instance method to self. r = new_method.bind(self).call(*args, &blk) else # super (above module thru super_tunnel); can raise NoMethodError. super_tunnel.instance_method(method_name).bind(self).call(*args, &blk) end end super_tunnel.send :define_method, method_name do |*args, &blk| super(*args, &blk) end end # include super_tunnel in new_module (must be included after Old) new_module.send :include, super_tunnel @protector_modules[new_module] = protector end end
Get line ranges info for the caller file.
# File lib/namebox.rb, line 358 def ranges_info ci = Namebox.caller_info ranges = enabled_ranges[ci[:file]] ||= [] ci[:ranges] = ranges # check whether there is an opened range in progress for the caller file last = ranges[-1] ci[:last] = last if last.is_a? Integer ci end