booty-call 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/booty_call.rb'
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH << File.dirname(__FILE__) + '/booty_call'
2
+
3
+ module BootyCall
4
+ VERSION = '0.0.5'
5
+ end
6
+
7
+ require 'rubygems'
8
+ require 'nakajima'
9
+ require 'callbacker'
10
+ require 'observed_method'
11
+ require 'introspector'
12
+ require 'hook'
@@ -0,0 +1,115 @@
1
+ module BootyCall
2
+ class Callbacker
3
+ attr_reader :klass
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ extend_klass
8
+ end
9
+
10
+ def before(method_id, *symbols, &block)
11
+ add_callback(:before, method_id, *symbols, &block)
12
+ end
13
+
14
+ def after(method_id, *symbols, &block)
15
+ add_callback(:after, method_id, *symbols, &block)
16
+ end
17
+
18
+ def around(method_id, *symbols, &block)
19
+ add_callback(:around, method_id, *symbols, &block)
20
+ end
21
+
22
+ private
23
+
24
+ def extend_klass
25
+ klass.class_eval do
26
+ @pristine_cache = Hash.new
27
+ @callback_cache = { :after => Hash.new([]), :before => Hash.new([]), :around => Hash.new([]) }
28
+ extend ClassMethods
29
+ include InstanceMethods
30
+ end
31
+ end
32
+
33
+ def add_callback(position, method_id, *symbols, &block)
34
+ klass.callback_cache[position][method_id] << block if block_given?
35
+ klass.callback_cache[position][method_id] += symbols
36
+ klass.callback_cache[position][method_id].compact!
37
+ klass.callback_cache[position][method_id].uniq!
38
+
39
+ klass.pristine_cache[method_id] ||= begin
40
+ klass.instance_method(method_id).tap do
41
+ redefine_method method_id
42
+ end
43
+ end
44
+ end
45
+
46
+ def redefine_method(method_id)
47
+ safe_method_id = method_id.to_s
48
+ safe_method_id.gsub!(/([\w_]+)(\?|!|=|\b)/) { |m| "#{$1}_without_callbacks#{$2}" }
49
+ klass.class_eval(<<-EOS, "(__DELEGATION__)", 1)
50
+ def #{method_id}(*args, &block)
51
+ catch(#{method_id.to_sym.inspect}) do
52
+ res = nil
53
+ run_callbacks(:before, #{method_id.inspect})
54
+ run_callbacks(:around, #{method_id.inspect}) { res = __PRISTINE__(#{method_id.inspect}, *args, &block) }
55
+ run_callbacks(:after, #{method_id.inspect}, res)
56
+ res
57
+ end
58
+ end
59
+
60
+ def #{safe_method_id}(*args, &block)
61
+ __PRISTINE__(#{method_id.inspect}, *args, &block)
62
+ end
63
+ EOS
64
+ end
65
+
66
+ module ClassMethods
67
+ def pristine_cache
68
+ @pristine_cache || superclass.pristine_cache
69
+ end
70
+
71
+ def callback_cache
72
+ @callback_cache || superclass.callback_cache
73
+ end
74
+
75
+ def run_callbacks_for(target, position, method_id, *results, &block)
76
+ callbacks = callback_cache[position][method_id.to_sym]
77
+
78
+ handle = proc do |fn|
79
+ if fn.is_a?(Proc)
80
+ block ? proc { instance_exec(block, &fn) } : fn
81
+ else
82
+ target.method(fn).arity.abs == results.length ?
83
+ proc { send(fn, *results, &block) } :
84
+ proc { send(fn, &block) }
85
+ end
86
+ end
87
+
88
+ cancel = proc do
89
+ block ? target.instance_eval(&block) : true
90
+ end
91
+
92
+ callbacks.empty? ? cancel[block] : callbacks.map { |fn|
93
+ target.instance_exec(*results, &handle[fn])
94
+ }.all?
95
+ end
96
+ end
97
+
98
+ module InstanceMethods
99
+ def run_callbacks(position, method_id, *args, &block)
100
+ self.class.run_callbacks_for(self, position, method_id, *args, &block).tap do |result|
101
+ # Halt method propagation if before callbacks return false
102
+ throw(method_id, false) if position.eql?(:before) and result.eql?(false)
103
+ end
104
+ end
105
+
106
+ def __PRISTINE__(method_id, *args, &block)
107
+ if method = self.class.pristine_cache[method_id]
108
+ method.bind(self).call(*args, &block)
109
+ else
110
+ raise NoMethodError, "No method named #{method_id.inspect}"
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,51 @@
1
+ module BootyCall
2
+ def self.included(klass)
3
+ klass.send(:include, Hook)
4
+ end
5
+
6
+ module Hook
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ extend(ClassMethods)
10
+ @callbacker = BootyCall::Callbacker.new(self)
11
+ @introspector = BootyCall::Introspector.new(self)
12
+ @meta_introspector = BootyCall::Introspector.new(self, :meta => true)
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def before(method_id, *args, &block)
18
+ callback(:before, method_id, *args, &block)
19
+ end
20
+
21
+ def after(method_id, *args, &block)
22
+ callback(:after, method_id, *args, &block)
23
+ end
24
+
25
+ def around(method_id, *args, &block)
26
+ callback(:around, method_id, *args, &block)
27
+ end
28
+
29
+ def observe(method_id, options={}, &block)
30
+ observer = options[:meta] ? @meta_introspector : @introspector
31
+ observer.observe(method_id, options, &block)
32
+ end
33
+
34
+ def callback(position, method_id, *args, &block)
35
+ case method_id
36
+ when Regexp
37
+ return if @introspector.observing?(method_id)
38
+ observe(method_id, :times => :all) { |m| callback(position, m, *(args << block)) }
39
+ @introspector.defined_methods \
40
+ .select { |m| m.to_s =~ method_id } \
41
+ .reject { |m| @introspector.observing?(m) } \
42
+ .each { |m| send(position, m, *args, &block) }
43
+ when Symbol
44
+ @introspector.has_method?(method_id) ?
45
+ @callbacker.send(position, method_id, *args, &block) :
46
+ observe(method_id) { send(position, method_id, *(args << block)) }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,59 @@
1
+ module BootyCall
2
+ class Introspector
3
+ attr_reader :klass, :options
4
+
5
+ def initialize(klass, options={})
6
+ @klass, @options = klass, options
7
+ @observed_methods = { }
8
+ end
9
+
10
+ def observe(method_id, options={}, &block)
11
+ observe_klass!
12
+ @observed_methods[method_id] ||= ObservedMethod.new(method_id, options)
13
+ @observed_methods[method_id].push(block) if block_given?
14
+ end
15
+
16
+ def observe_klass!
17
+ @observed ||= begin
18
+ name = options[:meta] ? :singleton_method_added : :method_added
19
+ install_hook(name) or true
20
+ end
21
+ end
22
+
23
+ def defined_methods
24
+ (klass.instance_methods - Object.instance_methods).map(&:to_sym)
25
+ end
26
+
27
+ def has_method?(method_id)
28
+ klass.instance_method(method_id) rescue false
29
+ end
30
+
31
+ def observing?(method_id)
32
+ not not @observed_methods[method_id]
33
+ end
34
+
35
+ private
36
+
37
+ def install_hook(name)
38
+ this = self
39
+ klass.meta_def(name) do |m|
40
+ this.send :check_method, m
41
+ end
42
+ end
43
+
44
+ def check_method(sym)
45
+ @observed_methods.each do |method_id, observer|
46
+ without_observers(method_id) do
47
+ observer.match(sym)
48
+ observer.valid?
49
+ end
50
+ end
51
+ end
52
+
53
+ def without_observers(method_id, &block)
54
+ @observed_methods.delete(method_id).tap do |observer|
55
+ @observed_methods[method_id] = observer if block[observer]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ module BootyCall
2
+ class ObservedMethod
3
+ attr_reader :method_id
4
+
5
+ def initialize(method_id, options={})
6
+ @method_id = method_id
7
+ @handlers = []
8
+ @count = 0
9
+ @times = options[:times] || 1
10
+ end
11
+
12
+ def match(sym)
13
+ return unless case method_id
14
+ when Symbol then valid? and method_id === sym
15
+ when Regexp then valid? and method_id.try(:match, sym.to_s)
16
+ else ; nil
17
+ end
18
+ @handlers.each { |fn| fn[sym] }
19
+ @count += 1
20
+ end
21
+
22
+ def valid?
23
+ if @times.to_s.match(/^(inf|any|all|every)/)
24
+ def self.valid?; true end
25
+ true # memoizing the result
26
+ else
27
+ @count < @times
28
+ end
29
+ end
30
+
31
+ def push(*args)
32
+ @handlers += args
33
+ @handlers.tap.compact!.uniq!
34
+ end
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: booty-call
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Pat Nakajima
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-12 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nakajima-nakajima
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: patnakajima@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/booty_call
35
+ - lib/booty-call.rb
36
+ - lib/booty_call.rb
37
+ - lib/booty_call/hook.rb
38
+ - lib/booty_call/callbacker.rb
39
+ - lib/booty_call/introspector.rb
40
+ - lib/booty_call/observed_method.rb
41
+ has_rdoc: false
42
+ homepage: http://github.com/nakajima/booty-call
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: Callbacks for your Ruby
67
+ test_files: []
68
+