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

caller_info() click to toggle source

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
caller_to_hash(a_caller) click to toggle source
# 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
default_modules() click to toggle source

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
default_modules=(modules_to_protect) click to toggle source

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
new(*modules_to_protect, &blk) click to toggle source

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.

Calls superclass method
# 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
no_method_error(obj, m_name) click to toggle source

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
require(resource, *modules_to_protect) click to toggle source

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() click to toggle source

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
file_wide() click to toggle source

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() click to toggle source

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
open?() click to toggle source

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

enabled_ranges() click to toggle source

Stores enabled line ranges of caller files for this module.

# File lib/namebox.rb, line 371
def enabled_ranges
  @enabled_ranges ||= {}
end
get_included_modules() click to toggle source

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() click to toggle source

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
protector_module(new_module) click to toggle source
Calls superclass method
# 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
ranges_info(ci = Namebox.caller_info) click to toggle source

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