require 'dynamic' # require 'pp' module ContextR @@genid = 0 def self.gensym(name, kind="") @@genid += 1 ("_#{name}_#{kind}_%05d_" % @@genid).intern end class MethodNature < Struct.new(:arguments, :return_value, :break, :block) def break!(*value) self.break = true self.return_value = value.first unless value.empty? end def call_next block.call(*arguments) end def call_next_with(*args) block.call(*args) end end CONTEXTUALIZED_METHODS = {} class ContextualizedMethod def initialize(meth) @method = meth @pre_hooks = [] @post_hooks = [] @wrappers = [] end def pre(layer, hook) @pre_hooks << [layer, hook] end def post(layer, hook) @post_hooks << [layer, hook] end def wrap(layer, hook) @wrappers << [layer, hook] end attr_reader :pre_hooks, :post_hooks, :wrappers def merge(other) pre_hooks.concat other.pre_hooks post_hooks.concat other.post_hooks wrappers.concat other.wrappers self end def call(s, *args) current_layers = Dynamic[:layers] nature = MethodNature.new(args, nil, false) @pre_hooks.reverse_each { |layer, hook| next unless current_layers.include? layer s.__send__(hook, nature) return nature.return_value if nature.break == true } unless @wrappers.empty? # Do this only when we need to. wrappers = @wrappers.map { |layer, w| current_layers.include?(layer) ? w : nil }.compact unless wrappers.empty? catch(:break_in_wrap) { nature.block = lambda { unless wrappers.empty? r = s.__send__(wrappers.pop, nature) throw :break_in_wrap, r if nature.break == true else nature.return_value = s.__send__(@method, *args) end } s.__send__ wrappers.pop, nature } else nature.return_value = s.__send__(@method, *args) end else nature.return_value = s.__send__(@method, *args) end return nature.return_value if nature.break == true @post_hooks.each { |layer, hook| next unless current_layers.include? layer s.__send__(hook, nature) return nature.return_value if nature.break == true } return nature.return_value end end class Layer attr_reader :layer def initialize(klass, name) @klass, @layer = klass, name end def contextualize(meth) @klass.class_eval { CONTEXTUALIZED_METHODS[self].fetch(meth) { name = ContextR.gensym(meth, "core") alias_method name, meth private name context_method = ContextualizedMethod.new name CONTEXTUALIZED_METHODS[self][meth] = context_method define_method(meth) { |*args| context_method.call self, *args } } } end def pre(meth, &block) contextualize meth name = ContextR.gensym meth, "pre" @klass.__send__ :define_method, name, &block @klass.__send__ :private, name CONTEXTUALIZED_METHODS[@klass][meth].pre(@layer, name) end def post(meth, &block) contextualize meth name = ContextR.gensym meth, "post" @klass.__send__ :define_method, name, &block @klass.__send__ :private, name CONTEXTUALIZED_METHODS[@klass][meth].post(@layer, name) end def wrap(meth, &block) contextualize meth name = ContextR.gensym meth, "wrap" @klass.__send__ :define_method, name, &block @klass.__send__ :private, name CONTEXTUALIZED_METHODS[@klass][meth].wrap(@layer, name) end def claim(*meths) layer = @layer # Keep a reference meths.each { |meth| @klass.class_eval { name = ContextR.gensym(meth, "claimed") alias_method name, meth private name define_method(meth) { |*args| if Dynamic[:layers].include? layer # tricky... __send__(name, *args) else raise NoMethodError, "undefined method `#{meth}' for #{self}:#{self.class} " + "(only in :#{layer})" end } } contextualize meth } end end end # ContextR module ContextR def self.with_layers(*layers, &block) Dynamic.let(:layers => Dynamic[:layers] | layers, &block) end def self.without_layers(*layers, &block) Dynamic.let(:layers => Dynamic[:layers] - layers, &block) end end class Module def _layer(name) klass_von_braun = class << self; self; end unless ContextR::CONTEXTUALIZED_METHODS.has_key? self ContextR::CONTEXTUALIZED_METHODS[self] = {} klass_von_braun.__send__(:define_method, :included) { |o| super ocm = ContextR::CONTEXTUALIZED_METHODS[o] ||= {} ContextR::CONTEXTUALIZED_METHODS[self].each { |name, value| if ocm.has_key? name ocm[name].merge value else ocm[name] = value end } } end ContextR::Layer.new(self, name) end def layer(name) l = _layer name # Define an accessor (class << self; self; end).instance_eval { define_method(name) { l } private name } l end end Dynamic.variable :layers => [:default] if $0 == __FILE__ class Foo def bar(*args) p "quux" end end Foo.new.bar class Foo layer :default layer :wraptest layer :suicide default.pre :bar do |nature| p "pre-bar in #{self} (called with #{nature.arguments})" end default.post :bar do p "post-bar" end default.pre :bar do p "now it starts:" end default.post :bar do p "finally over." end wraptest.wrap :bar do |n| p "<" n.call_next p ">" end wraptest.wrap :bar do |n| p "{" n.call_next p "}" end suicide.pre :bar do |n| p "suicide!" n.break! :death end end p Foo.new.bar(1, 2, 3) ContextR.with_layers :wraptest do p Foo.new.bar(1, 2, 3) ContextR.with_layers :suicide do p Foo.new.bar(1, 2, 3) end end p Foo.private_instance_methods.grep(/\d\d\d/) end