Created
March 31, 2009 13:06
-
-
Save siefca/88178 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Author:: Paweł Wilk (mailto:[email protected]) | |
# Copyright:: Copyright (c) 2009 Paweł Wilk | |
# License:: LGPL | |
# | |
# This module is intended to be used as extension | |
# (class level mixin) for classes using some buffers | |
# that may be altered by calling certain methods. | |
# | |
# It automates resetting of buffers by installing | |
# wrappers for invasive methods you choose. It rewrites | |
# selected methods by adding to them code that calls | |
# buffer(s) flushing method created by you. | |
# | |
# === Markers | |
# | |
# To select which methods are invasive for your buffer(s) | |
# you should use markers which in usage are similar to | |
# accessors, e.g: | |
# | |
# attr_affects_buffers :domain | |
# | |
# Markers may be placed anywhere in the class. Wrapping | |
# routine will wait for methods to be defined if you | |
# mark them too early in your code. | |
# | |
# ==== Marking methods | |
# | |
# To mark methods which should trigger reset operation | |
# when called use method_affects_buffers which takes | |
# comma-separated list of symbols describing names | |
# of these methods. | |
# | |
# ==== Marking attributes (setters) | |
# | |
# The marker attr_affects_buffers is similar but it takes | |
# instance members not methods as arguments. It just installs | |
# hooks for corresponding setters. | |
# | |
# === Buffers flushing method | |
# | |
# Default instance method called to reset buffers should be | |
# defined under name +reset_buffers+ | |
# You may also want to set up your own name by calling | |
# buffers_reset_method class method. The name of your | |
# buffers flushing method is passed to subclasses but | |
# each subclass may redefine it. | |
# | |
# Be aware that sub-subclass | |
# will still need redefinition since it's kind of one-level | |
# inheritance. | |
# | |
# Buffers flushing method may take none or exactly one argument. | |
# If your method will take an argument then a name of calling | |
# method will be passed to it as symbol. | |
# | |
# === Inherited classes | |
# | |
# This module tries to be inheritance-safe but you will have to | |
# mark methods and members in subclasses if you are going | |
# to redefine them. The smooth way is of course to use +super+ | |
# in overloaded methods so it will also do the job. | |
# | |
# === Caution | |
# | |
# This code uses Module#method_added hook. If you're going | |
# to redefine that method in class using this module remember | |
# to wrap and call original version or add one line to your | |
# definition: +ba_check_method(name)+ | |
# | |
# === Example | |
# | |
# class Main | |
# | |
# extend BufferAffects | |
# | |
# buffers_reset_method :reset_path_buffer | |
# attr_affects_buffers :subpart | |
# attr_accessor :subpart, :otherpart | |
# | |
# def reset_path_buffer(name) | |
# @path = nil | |
# p "reset called for #{name}" | |
# end | |
# | |
# def path | |
# @path ||= @subpart.to_s + @otherpart.to_s | |
# end | |
# | |
# end | |
# | |
# obj = Main.new | |
# obj.subpart = 'test' | |
# p obj.path | |
# obj.subpart = '1234' | |
# p obj.path | |
module BufferAffects | |
@@__ba_wrapped__ = {} | |
@@__ba_reset_m__ = nil | |
# This method sets name of method that will be used to reset buffers. | |
def buffers_reset_method(name) | |
name = name.to_s.strip | |
raise ArgumentError.new('method name cannot be empty') if name.empty? | |
@__ba_reset_method__ = name.to_sym | |
@@__ba_reset_m__ ||= @__ba_reset_method__ | |
end | |
private :buffers_reset_method | |
# This method sets the marker for hook to be installed. | |
# It ignores methods for which wrapper already exists. | |
def method_affects_buffers(*names) | |
@__ba_methods__ ||= {} | |
names.uniq! | |
names.collect! { |name| name.to_sym } | |
names.delete_if { |name| @__ba_methods__.has_key?(name) } | |
ba_methods_wrap(*names) | |
end | |
private :method_affects_buffers | |
# This method searches for setter methods for given | |
# member names and tries to wrap them into buffers | |
# resetting hooks usting method_affects_buffers | |
def attr_affects_buffers(*names) | |
names.collect! { |name| :"#{name}=" } | |
method_affects_buffers(*names) | |
end | |
private :attr_affects_buffers | |
# This method installs hook for given methods or puts their names | |
# on the queue if methods haven't been defined yet. The queue is | |
# tested each time ba_check_hook is called. | |
# | |
# Each processed method can be in one of 2 states: | |
# * false - method is not processed now | |
# * true - method is now processed | |
# | |
# After successful wrapping method name (key) and object ID (value) pairs | |
# are added two containers: @@__ba_wrapped__ and @__ba_methods__ | |
def ba_methods_wrap(*names) | |
names.delete_if { |name| @__ba_methods__[name] == true } # don't handle methods being processed | |
kmethods = public_instance_methods + | |
private_instance_methods + | |
protected_instance_methods | |
install_now = names.select { |name| kmethods.include?(name) } # select methods for immediate wrapping | |
install_now.delete_if do |name| # but don't wrap already wrapped | |
@@__ba_wrapped__.has_key?(name) && # - wrapped by our class or other class | |
!@__ba_methods__.has_key?(name) # - not wrapped by our class | |
end | |
install_later = names - install_now # collect undefined and wrapped methods | |
install_later.each { |name| @__ba_methods__[name] = false } # and add them to the waiting queue | |
install_now.each { |name| @__ba_methods__[name] = true } # mark methods as currently processed | |
installed = ba_install_hook(*install_now) # and install hooks for them | |
install_now.each { |name| @__ba_methods__[name] = false } # mark methods as not processed again | |
installed.each_pair do |name,id| # and note the object IDs of wrapped methods | |
@@__ba_wrapped__[name] = id # shared container | |
@__ba_methods__[name] = id # this class's container | |
end | |
end | |
private :ba_methods_wrap | |
# This method checks whether method which name is given | |
# is now available and should be installed. | |
def ba_check_method(name) | |
name = name.to_sym | |
@__ba_methods__ ||= {} | |
if @__ba_methods__.has_key?(name) | |
ba_methods_wrap(name) | |
end | |
end | |
private :ba_check_method | |
# This method installs hook which alters given methods by wrapping | |
# them into method that invokes buffers resetting routine. It will | |
# not install hook for methods beginning with __ba, which signalizes | |
# that they are wrappers for other methods. | |
def ba_install_hook(*names) | |
@__ba_reset_method__ ||= @@__ba_reset_m__ | |
@__ba_reset_method__ ||= 'reset_buffers' | |
installed = {} | |
names.uniq.each do |name| | |
new_method = name.to_s | |
next if new_method[0..3] == '__ba' | |
orig_id = instance_method(name.to_sym).object_id | |
orig_method = '__ba' + orig_id.to_s + '__' | |
reset_method = @__ba_reset_method__.to_s | |
module_eval %{ | |
alias_method :#{orig_method}, :#{new_method} | |
private :#{orig_method} | |
def #{new_method}(*args, &block) | |
if method(:#{reset_method}).arity == 1 | |
#{reset_method}(:#{new_method}) | |
else | |
#{reset_method} | |
end | |
return #{orig_method}(*args, &block) | |
end | |
} | |
installed[name] = orig_id | |
end | |
return installed | |
end | |
private :ba_install_hook | |
# Hook that intercepts added methods. | |
def method_added(name) | |
ba_check_method(name) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment