kinda-hookable 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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