kinda-hookable 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/hookable.rb +145 -0
  2. data/lib/kinda-hookable.rb +1 -0
  3. metadata +3 -1
data/lib/hookable.rb ADDED
@@ -0,0 +1,145 @@
1
+ require 'rubygems'
2
+ require 'kinda-core'
3
+
4
+ module Kinda
5
+ module Hookable
6
+ include Core
7
+
8
+ module ClassMethods
9
+ extend container.extensions
10
+
11
+ def add_hook(kind, method_name, &block)
12
+ hooked_methods[method_name.to_sym].hooks[kind] << block
13
+ patch_method_if_necessary(method_name.to_sym)
14
+ end
15
+
16
+ [:before, :after, :around].each do |kind|
17
+ define_method(kind) do |method_name, &block|
18
+ add_hook(kind, method_name, &block)
19
+ end
20
+ end
21
+
22
+ alias_method :on, :after
23
+
24
+ def exec_method(method_name, original_method, original_self, *args, &block)
25
+ result = nil
26
+
27
+ chained_hooks = [] << lambda do |*args, &block|
28
+ find_hooks(method_name, :before).each do |hook|
29
+ original_self.call_with_this(hook, *args, &block)
30
+ end
31
+ result = original_method.bind(original_self).call(*args, &block) if original_method
32
+ find_hooks(method_name, :after).each do |hook|
33
+ original_self.call_with_this(hook, *args, &block)
34
+ end
35
+ end
36
+
37
+ hooks = find_hooks(method_name, :around)
38
+ hooks.each_index do |index|
39
+ chained_hooks << lambda do |*args, &block|
40
+ original_self.call_with_this(hooks[index], chained_hooks[index], *args, &block)
41
+ end
42
+ end
43
+ chained_hooks.last.call(*args, &block)
44
+
45
+ result
46
+ end
47
+
48
+ def hooked_methods
49
+ @hooked_methods ||= Hash.new do |hash, method_name|
50
+ hash[method_name] = HookedMethod.new(self, method_name)
51
+ end
52
+ end
53
+
54
+ def find_hooks(method_name, kind)
55
+ hooks = []
56
+ self_and_ancestors.each do |ancestor|
57
+ ancestor.is_a?(Module) ? next : break unless ancestor.respond_to?(:hooked_methods)
58
+ next unless ancestor.hooked_methods.include?(method_name)
59
+ next unless ancestor.hooked_methods[method_name].hooks.include?(kind)
60
+ hooks.concat(ancestor.hooked_methods[method_name].hooks[kind])
61
+ end
62
+ hooks
63
+ end
64
+
65
+ def hooked_method_defined?(method_name)
66
+ self_and_ancestors.each do |ancestor|
67
+ ancestor.is_a?(Module) ? next : break unless ancestor.respond_to?(:hooked_methods)
68
+ return true if ancestor.hooked_methods.include?(method_name)
69
+ end
70
+ false
71
+ end
72
+
73
+ def method_added(method_name)
74
+ super
75
+ if hooked_method_defined?(method_name)
76
+ patch_method(method_name)
77
+ end
78
+ end
79
+
80
+ def patch_method(method_name)
81
+ return if Thread.current[:__adding_method__]
82
+ # puts "Patching method ##{method_name} in #{self.inspect}"
83
+ original_method = instance_method(method_name)
84
+ Thread.current[:__adding_method__] = true
85
+ define_method(method_name) do |*args, &block|
86
+ singleton_class.exec_method(method_name, original_method, self, *args, &block)
87
+ end
88
+ Thread.current[:__adding_method__] = false
89
+ hooked_methods[method_name].method_patched = true
90
+ end
91
+
92
+ def patch_method_if_necessary(method_name)
93
+ self_and_ancestors.each do |ancestor|
94
+ ancestor.is_a?(Module) ? next : break unless ancestor.respond_to?(:method_defined_in_this_class?)
95
+ if ancestor.method_defined_in_this_class?(method_name)
96
+ ancestor.patch_method(method_name) unless ancestor.hooked_methods[method_name].method_patched
97
+ break
98
+ end
99
+ end
100
+ end
101
+
102
+ def method_defined_in_this_class?(name) # Works for singleton class too
103
+ instance_method(name).owner == self rescue false
104
+ end
105
+ end
106
+
107
+ inheritable_extension ClassMethods
108
+
109
+ ###
110
+
111
+ delegate_to_class :before
112
+ delegate_to_class :after
113
+ alias_method :on, :after
114
+ delegate_to_class :around
115
+ delegate_to_class :singleton_method_added, :method_added
116
+
117
+ def method_missing(method_name, *args, &block)
118
+ if singleton_class.hooked_method_defined?(method_name)
119
+ singleton_class.exec_method(method_name, nil, self, *args, &block)
120
+ else
121
+ super
122
+ end
123
+ end
124
+
125
+ def respond_to?(method_name, include_private=false)
126
+ super || singleton_class.hooked_method_defined?(method_name)
127
+ end
128
+
129
+ ###
130
+
131
+ class HookedMethod
132
+ attr_reader :klass, :name
133
+ attr_accessor :method_patched
134
+
135
+ def initialize(klass, name)
136
+ @klass = klass
137
+ @name = name
138
+ end
139
+
140
+ def hooks
141
+ @hooks ||= Hash.new { |hash, kind| hash[kind] = [] }
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'hookable')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kinda-hookable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Vila
@@ -31,6 +31,8 @@ extensions: []
31
31
  extra_rdoc_files:
32
32
  - README.rdoc
33
33
  files:
34
+ - lib/hookable.rb
35
+ - lib/kinda-hookable.rb
34
36
  - README.rdoc
35
37
  has_rdoc: true
36
38
  homepage: http://github.com/kinda/hookable