callbacksの簡易版を実装

いやぁ、Ruby on Rails Guilds:Get Startedをやってみて、validationまわりがどうなっているのか気になったので読んでみました。validatesはActive SupportのCallbacksモジュールのset_callbackでValidatorオブジェクトがvalidateをコールするのをvalid?を実行するまで遅延させていたので、まずはそっちの方を読みながら簡易版を作りました。

#callbacks.rb
class Class
  def class_attribute name
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def self.#{name}() nil end
      def self.#{name}=(val)
        singleton_class.class_eval {
          undef_method :#{name}
          define_method(:#{name}) { val }
        }
        val
      end 
    RUBY
  end
end
module MyRails
  module Concern
    def append_features(base)
      base.extend const_get("ClassMethods") 
      super
    end
  end
  module Callbacks
    extend Concern
    class Callback
      @@_callback_sequence = 0
      attr_reader :filter
      def initialize(callback_name, callback_obj, callbacks_class)
        @filter = "_callback_#{callback_name}_#{next_id}"
        callbacks_class.send(:define_method, "#{@filter}_object") { callback_obj }
        callbacks_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
          def #{@filter}
            #{@filter}_object.#{callback_name}
          end
        RUBY_EVAL
      end
      def next_id
        @@_callback_sequence += 1
      end
    end
    def run_callbacks(callback_name)
      self.class.send("_#{callback_name}_callbacks").each do|callback|
        send(callback.filter)
      end
    end
    module ClassMethods
      def define_callback(callback_name)
        attr_name = "_#{callback_name}_callbacks"
        class_attribute attr_name
        send("#{attr_name}=", Array.new)
      end
      def set_callback(callback_name, callback_obj)
        send("_#{callback_name}_callbacks").push(Callback.new(callback_name, callback_obj, self))
      end
    end
  end
end

class_evalは文字列も受け付けるんですね。テストはこんな感じ。

require './callbacks'
require 'test/unit'
require 'stringio'
class C 
  class_attribute :name 
end
class CallbackObj
  def callback_method
    puts "1"
  end
end
class CallbackObj2
  def callback_method
    puts "2"
  end
end
class Callbacks
  include MyRails::Callbacks
  define_callback :callback_method
  set_callback(:callback_method, CallbackObj.new)
  set_callback(:callback_method, CallbackObj2.new)
end
class MyRailsTests < Test::Unit::TestCase
  def test_class_attribute
     name = "nabeyang"
     assert_equal name, (C.name = name)
     assert_equal name, C.name
  end
  def test_callbacks_set_callback
    assert_equal 2, Callbacks._callback_method_callbacks.size
  end
  def test_callbacks_run_callback
    obj = Callbacks.new
    s = StringIO.new
    $stdout = s 
    obj.run_callbacks(:callback_method)
    $stdout = STDOUT
    assert_equal "1\n2\n", s.string
  end
end