chubas-peeping 1.0.0
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.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.markdown +74 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/peeping.rb +5 -0
- data/lib/peeping/exceptions.rb +6 -0
- data/lib/peeping/hook.rb +8 -0
- data/lib/peeping/peeping.rb +314 -0
- data/lib/peeping/util.rb +13 -0
- data/spec/helpers/test_helpers.rb +42 -0
- data/spec/hook_methods_spec.rb +139 -0
- data/spec/hooked_behavior_spec.rb +165 -0
- metadata +68 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ruben Medellin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# peeping
|
2
|
+
|
3
|
+
Like aspect-oriented, but cooler!
|
4
|
+
|
5
|
+
## Motivation
|
6
|
+
|
7
|
+
This library intends to provide a simple yet flexible way for defining hook calls for your methods,
|
8
|
+
without trying to be a complete aspect-oriented programming framework implementation.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Just point to the root directory of the library in your require path and include `peeping.rb` (Gem packaging soon)
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
For defining hooks, use the methods of `Peeping::Peep` class
|
17
|
+
|
18
|
+
include Peeping
|
19
|
+
|
20
|
+
class Foo
|
21
|
+
def some_instance_method
|
22
|
+
puts "Hi there!"
|
23
|
+
end
|
24
|
+
def self.some_class_method
|
25
|
+
puts "Cool!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
foo = Foo.new
|
30
|
+
|
31
|
+
Peep.hook_class!(Foo, :some_class_method,
|
32
|
+
:before => Proc.new{ puts "Before class method" },
|
33
|
+
:after => Proc.new{ puts "After class method" })
|
34
|
+
Peep.hook_instances!(Foo, :some_instance_method,
|
35
|
+
:before => Proc.new{ puts "Before instance method" },
|
36
|
+
:after => Proc.new{ puts "After instance method" })
|
37
|
+
Peep.hook_object!(foo, :some_instance_method,
|
38
|
+
:before => Proc.new{ puts "Before singleton instance method" },
|
39
|
+
:after => Proc.new{ puts "After singleton instance method" })
|
40
|
+
|
41
|
+
Foo.some_class_method
|
42
|
+
Foo.new.some_instance_method
|
43
|
+
foo.some_instance_method
|
44
|
+
|
45
|
+
produces output:
|
46
|
+
|
47
|
+
Before class method
|
48
|
+
Cool!
|
49
|
+
After class method
|
50
|
+
Before instance method
|
51
|
+
Hi there!
|
52
|
+
After instance method
|
53
|
+
Before singleton instance method
|
54
|
+
Hi there!
|
55
|
+
After singleton instance method
|
56
|
+
|
57
|
+
|
58
|
+
[See documentation online][1] and TODO notes for more info
|
59
|
+
|
60
|
+
## Tests
|
61
|
+
|
62
|
+
The library comes with its rspec test suite, located in folder _tests_
|
63
|
+
|
64
|
+
## Updates
|
65
|
+
|
66
|
+
## TODO
|
67
|
+
|
68
|
+
- Add option to eval in the context of the caller with instance_exec
|
69
|
+
- Selective remove for existing hooks
|
70
|
+
- Add option to either replace or wrap singleton method hooks when instance method hooks are already defined
|
71
|
+
- Accept options for wrapping, replacing or adding hooks at custom positions in the bubble chain
|
72
|
+
- Gem packaging
|
73
|
+
|
74
|
+
[1]: http://rdoc.info/projects/chubas/peeping
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
|
8
|
+
gem.name = "peeping"
|
9
|
+
gem.email = "ruben.medellin.c@gmail.com"
|
10
|
+
gem.summary = %Q{Lightweight AOP framework for managing method hooks}
|
11
|
+
gem.description = <<-DESCRIPTION
|
12
|
+
Add, remove and manage hooks for class, instance and singleton method calls.
|
13
|
+
Intended to be not a full Aspect Oriented Programming framework, but a lightweight
|
14
|
+
simpler one.
|
15
|
+
DESCRIPTION
|
16
|
+
|
17
|
+
gem.homepage = "http://github.com/chubas/peeping"
|
18
|
+
gem.authors = ["Ruben Medellin"]
|
19
|
+
|
20
|
+
gem.requirements << 'none'
|
21
|
+
gem.require_path = 'lib'
|
22
|
+
|
23
|
+
gem.has_rdoc = true
|
24
|
+
gem.test_files = Dir.glob('spec/*.rb')
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
rescue LoadError
|
29
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
30
|
+
end
|
31
|
+
|
32
|
+
task :default => :test
|
33
|
+
require 'rake/rdoctask'
|
34
|
+
Rake::RDocTask.new do |rdoc|
|
35
|
+
if File.exist?('VERSION.yml')
|
36
|
+
config = YAML.load(File.read('VERSION.yml'))
|
37
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
38
|
+
elsif File.exist?('VERSION')
|
39
|
+
version = File.read('VERSION')
|
40
|
+
else
|
41
|
+
version = ""
|
42
|
+
end
|
43
|
+
|
44
|
+
rdoc.rdoc_dir = 'rdoc'
|
45
|
+
rdoc.title = "peeping #{version}"
|
46
|
+
rdoc.rdoc_files.include('README*')
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
48
|
+
end
|
49
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/peeping.rb
ADDED
data/lib/peeping/hook.rb
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
# Peeping is sort of an aspect-oriented programming module -- just without the fancy name
|
2
|
+
# It allows to hook class, instance and singleton methods for adding -and nicely managing-
|
3
|
+
# before and after procedures.
|
4
|
+
#
|
5
|
+
# Author:: Ruben Medellin (mailto:ruben.medellin.c@gmail.com)
|
6
|
+
# License:: Distributes under the same terms as Ruby
|
7
|
+
|
8
|
+
require 'peeping/util'
|
9
|
+
require 'peeping/hook'
|
10
|
+
require 'peeping/exceptions'
|
11
|
+
|
12
|
+
# Wraps classes and modules for the +peeping+ library
|
13
|
+
module Peeping
|
14
|
+
|
15
|
+
# This class defines the methods for hooking and unhooking methods, as well as contains the variables that
|
16
|
+
# hold the hook definitions
|
17
|
+
class Peep
|
18
|
+
|
19
|
+
#--
|
20
|
+
## ========== Holder variables section ==========
|
21
|
+
#++
|
22
|
+
|
23
|
+
# Holds the hooks declared for class methods
|
24
|
+
CLASS_METHOD_HOOKS = {}
|
25
|
+
|
26
|
+
# Holds the hooks defined for instance methods
|
27
|
+
INSTANCE_METHOD_HOOKS = {}
|
28
|
+
|
29
|
+
# Holds the hooks defined for singleton methods
|
30
|
+
SINGLETON_METHOD_HOOKS = {}
|
31
|
+
|
32
|
+
#--
|
33
|
+
## ===================================================
|
34
|
+
## =============== Class hooks section ===============
|
35
|
+
## ===================================================
|
36
|
+
#++
|
37
|
+
|
38
|
+
# Returns true if hooks given for class methods of the class +klass+ exist
|
39
|
+
def self.is_class_hooked?(klass)
|
40
|
+
CLASS_METHOD_HOOKS.has_key?(klass)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a hash containing all hooked methods for class +klass+ as keys, or an empty hash if don't exist
|
44
|
+
def self.hooked_class_methods_for(klass)
|
45
|
+
CLASS_METHOD_HOOKS[klass] || {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adds +hooks+ hooks to the given +klass+ class methods +hooked_methods+
|
49
|
+
# This parameter can be either a symbol or an array of symbols.
|
50
|
+
#
|
51
|
+
# Possible thrown exceptions:
|
52
|
+
# NotAClassException:: When the specified parameter +klass+ is not a class
|
53
|
+
# UndefinedMethodException:: When one of the symbols in +hooked_methods+ param is effectively not a defined class method
|
54
|
+
# AlreadyDefinedHookException:: When a hook for this class and specified method exists
|
55
|
+
# InvalidHooksException:: When hooks contains keys other than :before and :after calls
|
56
|
+
def self.hook_class!(klass, hooked_methods, hooks)
|
57
|
+
define_method_hooks!(klass, hooked_methods, hooks, :class, CLASS_METHOD_HOOKS)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Removes hook class methods as well as returns the hooked methods to their original definition
|
61
|
+
def self.unhook_class!(klass)
|
62
|
+
klass.metaclass.class_eval do
|
63
|
+
instance_methods.grep(/^__proxied_class_method_/).each do |proxied_method|
|
64
|
+
proxied_method =~ (/^__proxied_class_method_(.*)$/)
|
65
|
+
original = $1
|
66
|
+
eval "alias #{original} #{proxied_method}"
|
67
|
+
eval "undef #{proxied_method}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
CLASS_METHOD_HOOKS.delete(klass)
|
71
|
+
end
|
72
|
+
|
73
|
+
#--
|
74
|
+
## ======================================================
|
75
|
+
## =============== Instance hooks section ===============
|
76
|
+
## ======================================================
|
77
|
+
#++
|
78
|
+
|
79
|
+
# Returns true if hooks given for instance methods of the class +klass+ exist
|
80
|
+
def self.are_instances_hooked?(klass)
|
81
|
+
INSTANCE_METHOD_HOOKS.has_key?(klass)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns a hash containing all hooked methods for class +klass+ as keys, or an empty hash if don't exist
|
85
|
+
def self.hooked_instance_methods_for(klass)
|
86
|
+
INSTANCE_METHOD_HOOKS[klass] || {}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Adds +hooks+ hooks to the given +klass+ instance methods +hooked_methods+
|
90
|
+
# This parameter can be either a symbol or an array of symbols.
|
91
|
+
#
|
92
|
+
# Possible thrown exceptions:
|
93
|
+
# NotAClassException:: When the specified parameter +klass+ is not a class
|
94
|
+
# UndefinedMethodException:: When one of the symbols in +hooked_methods+ param is effectively not a defined instance method
|
95
|
+
# AlreadyDefinedHookException:: When a hook for this class and specified instance method exists
|
96
|
+
# InvalidHooksException:: When hooks contains keys other than :before and :after calls
|
97
|
+
def self.hook_instances!(klass, hooked_methods, hooks)
|
98
|
+
define_method_hooks!(klass, hooked_methods, hooks, :instance, INSTANCE_METHOD_HOOKS)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Removes hook instance methods as well as returns the hooked methods to their original definition
|
102
|
+
def self.unhook_instances!(klass)
|
103
|
+
klass.class_eval do
|
104
|
+
instance_methods.grep(/^__proxied_instance_method_/).each do |proxied_method|
|
105
|
+
proxied_method =~ (/^__proxied_instance_method_(.*)$/)
|
106
|
+
original = $1
|
107
|
+
eval "alias #{original} #{proxied_method}"
|
108
|
+
eval "undef #{proxied_method}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
INSTANCE_METHOD_HOOKS.delete(klass)
|
112
|
+
end
|
113
|
+
|
114
|
+
#--
|
115
|
+
## =======================================================
|
116
|
+
## =============== Singleton hooks section ===============
|
117
|
+
## =======================================================
|
118
|
+
#++
|
119
|
+
|
120
|
+
# Returns true if hooks given for singleton methods of the class +klass+ exist
|
121
|
+
def self.is_hooked?(object)
|
122
|
+
SINGLETON_METHOD_HOOKS.has_key?(object)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns a hash containing all hooked methods for class +klass+ as keys, or an empty hash if don't exist
|
126
|
+
def self.hooked_singleton_methods_for(object)
|
127
|
+
SINGLETON_METHOD_HOOKS[object] || {}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Adds +hooks+ hooks to the given +object+ singleton methods +hooked_methods+
|
131
|
+
# This parameter can be either a symbol or an array of symbols.
|
132
|
+
#
|
133
|
+
# Possible thrown exceptions:
|
134
|
+
# UndefinedMethodException:: When one of the symbols in +hooked_methods+ param is not a defined method for object +object+
|
135
|
+
# AlreadyDefinedHookException:: When a hook for this object and specified instance method exists.
|
136
|
+
# Note that this error *will not* be raised if an instance method hook exists for this
|
137
|
+
# method in the object's class. It will override it.
|
138
|
+
# InvalidHooksException:: When hooks contains keys other than :before and :after calls
|
139
|
+
def self.hook_object!(object, hooked_methods, hooks)
|
140
|
+
define_method_hooks!(object, hooked_methods, hooks, :singleton, SINGLETON_METHOD_HOOKS)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Removes hook singleton methods as well as returns the hooked methods to their original definition
|
144
|
+
def self.unhook_object!(object)
|
145
|
+
object.metaclass.class_eval do
|
146
|
+
instance_methods.grep(/^__proxied_singleton_method_/).each do |proxied_method|
|
147
|
+
proxied_method =~ (/^__proxied_singleton_method_(.*)$/)
|
148
|
+
original = $1
|
149
|
+
eval "alias #{original} #{proxied_method}"
|
150
|
+
eval "undef #{proxied_method}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
SINGLETON_METHOD_HOOKS.delete(object)
|
154
|
+
end
|
155
|
+
|
156
|
+
#--
|
157
|
+
## ========== Protected auxiliar methods section ==========
|
158
|
+
class << self
|
159
|
+
protected
|
160
|
+
|
161
|
+
def define_method_hooks!(what, hooked_methods, hooks, mode, container)
|
162
|
+
hooked_methods = [hooked_methods] if hooked_methods.is_a? Symbol
|
163
|
+
|
164
|
+
validate_hooks(hooks) # Validate hooks
|
165
|
+
validate_is_class(what, mode) # Validate class
|
166
|
+
|
167
|
+
where_to_eval = (mode == :class or mode == :singleton) ? what.metaclass : what
|
168
|
+
hooked_methods.each do |hooked_method| # Validate methods defined
|
169
|
+
validate_has_method_defined(where_to_eval, hooked_method, mode)
|
170
|
+
end
|
171
|
+
|
172
|
+
hooked_methods.each do |hooked_method|
|
173
|
+
container[what] ||= {}
|
174
|
+
hooked_method_name = "__proxied_#{mode}_method_#{hooked_method}"
|
175
|
+
|
176
|
+
if container[what].has_key?(hooked_method)
|
177
|
+
raise AlreadyDefinedHookException.new("Hook signature present for #{hooked_method} in #{where_to_eval}")
|
178
|
+
end
|
179
|
+
if what.respond_to?(hooked_method_name)
|
180
|
+
raise AlreadyDefinedHookException.new("Method #{hooked_method_name} hook already defined for #{hooked_method} in #{where_to_eval}")
|
181
|
+
end
|
182
|
+
|
183
|
+
container[what][hooked_method] = {}
|
184
|
+
|
185
|
+
hooks.each do |where, callback|
|
186
|
+
container[what][hooked_method][where] = callback
|
187
|
+
end
|
188
|
+
|
189
|
+
hook_key = (mode == :class or mode == :instance) ? what.name : 'self'
|
190
|
+
before_hook_call = if hooks.include?(:before)
|
191
|
+
"Peeping::Peep.hooked_#{mode}_methods_for(#{hook_key})[:\"#{hooked_method}\"][:before].call(self, *args)"
|
192
|
+
end
|
193
|
+
after_hook_call = if hooks.include?(:after)
|
194
|
+
"Peeping::Peep.hooked_#{mode}_methods_for(#{hook_key})[:\"#{hooked_method}\"][:after].call(self, proxied_result)"
|
195
|
+
end
|
196
|
+
should_override_instance_call = (mode == :singleton).to_s
|
197
|
+
class_eval_call = Proc.new do
|
198
|
+
eval <<-REDEF
|
199
|
+
if #{should_override_instance_call} and method_defined?(:"__proxied_instance_method_#{hooked_method}")
|
200
|
+
alias :"#{hooked_method_name}" :"__proxied_instance_method_#{hooked_method}"
|
201
|
+
else
|
202
|
+
alias :"#{hooked_method_name}" :"#{hooked_method}"
|
203
|
+
end
|
204
|
+
def #{hooked_method}(*args, &block)
|
205
|
+
#{before_hook_call}
|
206
|
+
proxied_result = if block_given?
|
207
|
+
__send__("#{hooked_method_name}", *args, &block)
|
208
|
+
else
|
209
|
+
__send__("#{hooked_method_name}", *args)
|
210
|
+
end
|
211
|
+
#{after_hook_call}
|
212
|
+
proxied_result
|
213
|
+
end
|
214
|
+
REDEF
|
215
|
+
end
|
216
|
+
where_to_eval.class_eval(&class_eval_call)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Validates that the array consists only in the keys :before or/and :after
|
221
|
+
#--
|
222
|
+
# TODO: Add extensibility for possible keys. Ideas: warn_on_override, :dont_override, :execute_in_place
|
223
|
+
#++
|
224
|
+
def validate_hooks(hooks)
|
225
|
+
raise InvalidHooksException.new("At least an :after or a :before hook are expected") if hooks.empty?
|
226
|
+
unknown_keys = hooks.keys - [:before, :after]
|
227
|
+
raise InvalidHooksException.new("Unknown keys #{unknown_keys.join(', ')}") unless unknown_keys.empty?
|
228
|
+
end
|
229
|
+
|
230
|
+
# Validates that the passed object is a class in :class and :instance modes
|
231
|
+
def validate_is_class(what, mode)
|
232
|
+
if mode == :class or mode == :instance
|
233
|
+
raise NotAClassException.new("#{what} is not a Class") unless what.is_a? Class
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Validates that the the method that will be hooked for the given object is defined.
|
238
|
+
def validate_has_method_defined(holding_class, method, mode)
|
239
|
+
unless holding_class.method_defined?(method)
|
240
|
+
raise UndefinedMethodException.new("Undefined #{mode} method #{method.inspect} for #{holding_class}")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
#++
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
## ========== Auxiliar methods ==========
|
249
|
+
|
250
|
+
class Peep
|
251
|
+
# Completely removes all hooks for all defined classes and objects
|
252
|
+
def self.clear_all_hooks!
|
253
|
+
clear_all_class_hooks!
|
254
|
+
clear_all_singleton_hooks!
|
255
|
+
clear_all_instance_hooks!
|
256
|
+
end
|
257
|
+
|
258
|
+
#--
|
259
|
+
class << self
|
260
|
+
protected
|
261
|
+
|
262
|
+
# TODO: Refactor lots of this
|
263
|
+
|
264
|
+
def clear_all_class_hooks!
|
265
|
+
Peep::CLASS_METHOD_HOOKS.each do |klass, hooked_methods|
|
266
|
+
hooked_methods.each do |hooked_method, callbacks|
|
267
|
+
klass.metaclass.class_eval <<-UNDEF_EVAL
|
268
|
+
if method_defined?(:"__proxied_class_method_#{hooked_method}")
|
269
|
+
alias :"#{hooked_method}" :"__proxied_class_method_#{hooked_method}"
|
270
|
+
undef :"__proxied_class_method_#{hooked_method}"
|
271
|
+
end
|
272
|
+
UNDEF_EVAL
|
273
|
+
end
|
274
|
+
end
|
275
|
+
Peep::CLASS_METHOD_HOOKS.clear
|
276
|
+
end
|
277
|
+
|
278
|
+
def clear_all_instance_hooks!
|
279
|
+
Peep::INSTANCE_METHOD_HOOKS.each do |klass, hooked_methods|
|
280
|
+
hooked_methods.each do |hooked_method, callbacks|
|
281
|
+
klass.class_eval <<-UNDEF_EVAL
|
282
|
+
if method_defined?(:"__proxied_instance_method_#{hooked_method}")
|
283
|
+
alias :"#{hooked_method}" :"__proxied_instance_method_#{hooked_method}"
|
284
|
+
undef :"__proxied_instance_method_#{hooked_method}"
|
285
|
+
end
|
286
|
+
UNDEF_EVAL
|
287
|
+
end
|
288
|
+
end
|
289
|
+
Peep::INSTANCE_METHOD_HOOKS.clear
|
290
|
+
end
|
291
|
+
|
292
|
+
def clear_all_singleton_hooks!
|
293
|
+
Peep::SINGLETON_METHOD_HOOKS.each do |object, hooked_methods|
|
294
|
+
hooked_methods.each do |hooked_method, callbacks|
|
295
|
+
object.metaclass.class_eval <<-UNDEF_EVAL
|
296
|
+
if method_defined?(:"__proxied_singleton_method_#{hooked_method}")
|
297
|
+
if method_defined?(:"__proxied_instance_method_#{hooked_method}")
|
298
|
+
alias :"#{hooked_method}" :"__proxied_instance_method_#{hooked_method}"
|
299
|
+
else
|
300
|
+
alias :"#{hooked_method}" :"__proxied_singleton_method_#{hooked_method}"
|
301
|
+
end
|
302
|
+
undef :"__proxied_singleton_method_#{hooked_method}"
|
303
|
+
end
|
304
|
+
UNDEF_EVAL
|
305
|
+
end
|
306
|
+
end
|
307
|
+
Peep::SINGLETON_METHOD_HOOKS.clear
|
308
|
+
end
|
309
|
+
end
|
310
|
+
#++
|
311
|
+
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
data/lib/peeping/util.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec'
|
2
|
+
|
3
|
+
shared_examples_for 'a clean test' do
|
4
|
+
before{ Peeping::Peep.clear_all_hooks! }
|
5
|
+
after { Peeping::Peep.clear_all_hooks! }
|
6
|
+
end
|
7
|
+
|
8
|
+
#--
|
9
|
+
# Dummy class, used for testing
|
10
|
+
class Dog
|
11
|
+
|
12
|
+
attr_accessor :name, :trained, :owner
|
13
|
+
attr_accessor :a, :b
|
14
|
+
|
15
|
+
def initialize(name)
|
16
|
+
@name = name
|
17
|
+
end
|
18
|
+
|
19
|
+
def bark(times)
|
20
|
+
(["woof"] * times).join(' ') + "!"
|
21
|
+
end
|
22
|
+
|
23
|
+
def lick_owner
|
24
|
+
if @owner
|
25
|
+
"#{@owner} is now all wet!"
|
26
|
+
else
|
27
|
+
"I have no owner to lick :("
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.train(dog)
|
32
|
+
dog.trained = true
|
33
|
+
dog
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.eats?(thing)
|
37
|
+
%w{meat bones milk}.include?(thing)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
#++
|
42
|
+
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'peeping')
|
3
|
+
require File.join(File.dirname(__FILE__), 'helpers', 'test_helpers')
|
4
|
+
|
5
|
+
|
6
|
+
include Peeping
|
7
|
+
|
8
|
+
describe Peep, "determining if objects are hooked, and get hooks for class or object" do
|
9
|
+
|
10
|
+
it_should_behave_like "a clean test"
|
11
|
+
|
12
|
+
it "should allow me to determine wether a class is hooked" do
|
13
|
+
include Peeping
|
14
|
+
Peep.should respond_to(:is_class_hooked?)
|
15
|
+
Peep.should respond_to(:hooked_class_methods_for)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should allow me to determine if all instances of a class are hooked" do
|
19
|
+
Peep.should respond_to(:are_instances_hooked?)
|
20
|
+
Peep.should respond_to(:hooked_instance_methods_for)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should allow me to determine if a single object is hooked" do
|
24
|
+
Peep.should respond_to(:is_hooked?)
|
25
|
+
Peep.should respond_to(:hooked_singleton_methods_for)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe Peep, "when hooking and unhooking objects" do
|
31
|
+
|
32
|
+
it_should_behave_like "a clean test"
|
33
|
+
|
34
|
+
it "should allow to me to hook and unhook classes" do
|
35
|
+
Peep.is_class_hooked?(Dog).should == false
|
36
|
+
Peep.hooked_class_methods_for(Dog).should be_empty
|
37
|
+
|
38
|
+
after_call = Proc.new{|klass, result| result}
|
39
|
+
Peep.hook_class!(Dog, :train, :after => after_call)
|
40
|
+
|
41
|
+
Peep.is_class_hooked?(Dog).should == true
|
42
|
+
Peep.hooked_class_methods_for(Dog).should_not be_empty
|
43
|
+
Peep.hooked_class_methods_for(Dog).should have(1).keys
|
44
|
+
Peep.hooked_class_methods_for(Dog)[:train][:after].should == after_call
|
45
|
+
|
46
|
+
Peep.unhook_class!(Dog)
|
47
|
+
|
48
|
+
Peep.is_class_hooked?(Dog).should == false
|
49
|
+
Peep.hooked_class_methods_for(Dog).should be_empty
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should allow me to hook and unhook instance methods" do
|
53
|
+
Peep.are_instances_hooked?(Dog).should == false
|
54
|
+
Peep.hooked_instance_methods_for(Dog).should be_empty
|
55
|
+
|
56
|
+
after_call = Proc.new{|klass, result| result}
|
57
|
+
Peep.hook_instances!(Dog, :bark, :after => after_call)
|
58
|
+
|
59
|
+
Peep.are_instances_hooked?(Dog).should == true
|
60
|
+
Peep.hooked_instance_methods_for(Dog).should_not be_empty
|
61
|
+
Peep.hooked_instance_methods_for(Dog).should have(1).keys
|
62
|
+
Peep.hooked_instance_methods_for(Dog)[:bark][:after].should == after_call
|
63
|
+
|
64
|
+
Peep.unhook_instances!(Dog)
|
65
|
+
|
66
|
+
Peep.are_instances_hooked?(Dog).should == false
|
67
|
+
Peep.hooked_instance_methods_for(Dog).should be_empty
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should allow me to hook and unhook single objects" do
|
71
|
+
scooby = Dog.new("scooby")
|
72
|
+
Peep.is_hooked?(scooby).should == false
|
73
|
+
|
74
|
+
after_call = Proc.new{|klass, result| result}
|
75
|
+
Peep.hook_object!(scooby, :bark, :after => after_call)
|
76
|
+
|
77
|
+
Peep.is_hooked?(scooby).should == true
|
78
|
+
Peep.hooked_singleton_methods_for(scooby).should_not be_empty
|
79
|
+
Peep.hooked_singleton_methods_for(scooby).should have(1).keys
|
80
|
+
Peep.hooked_singleton_methods_for(scooby)[:bark][:after].should == after_call
|
81
|
+
|
82
|
+
Peep.are_instances_hooked?(Dog).should == false
|
83
|
+
Peep.hooked_instance_methods_for(Dog).should be_empty
|
84
|
+
|
85
|
+
Peep.unhook_object!(scooby)
|
86
|
+
|
87
|
+
Peep.is_hooked?(scooby).should == false
|
88
|
+
Peep.hooked_singleton_methods_for(scooby).should be_empty
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe Peep, "when sending valid and unvalid parameters" do
|
93
|
+
|
94
|
+
it_should_behave_like "a clean test"
|
95
|
+
|
96
|
+
it "should only accept valid parameters for hooks" do
|
97
|
+
scooby = Dog.new("scooby")
|
98
|
+
invalid_keys = { :invalid => :key }
|
99
|
+
|
100
|
+
Proc.new{ Peep.hook_class! Dog, :hello, invalid_keys }.should raise_error InvalidHooksException
|
101
|
+
Proc.new{ Peep.hook_class! Dog, :hello, {} }.should raise_error InvalidHooksException
|
102
|
+
|
103
|
+
Proc.new{ Peep.hook_instances! Dog, :hello, invalid_keys }.should raise_error InvalidHooksException
|
104
|
+
Proc.new{ Peep.hook_instances! Dog, :hello, {} }.should raise_error InvalidHooksException
|
105
|
+
|
106
|
+
Proc.new{ Peep.hook_object! scooby, :hello, invalid_keys }.should raise_error InvalidHooksException
|
107
|
+
Proc.new{ Peep.hook_object! scooby, :hello, {} }.should raise_error InvalidHooksException
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should validate that methods are defined" do
|
111
|
+
p = Proc.new{|klass, result| result }
|
112
|
+
scooby = Dog.new("scooby")
|
113
|
+
|
114
|
+
Proc.new{ Peep.hook_class! Dog, :dance, :after => p }.should raise_error UndefinedMethodException
|
115
|
+
Proc.new{ Peep.hook_instances! Dog, :dance, :after => p }.should raise_error UndefinedMethodException
|
116
|
+
Proc.new{ Peep.hook_object! scooby, :dance, :after => p }.should raise_error UndefinedMethodException
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should validate that passed objects are classes for class and instance hooks" do
|
120
|
+
p = Proc.new{|klass, result| result}
|
121
|
+
Proc.new{ Peep.hook_class! :not_a_class, :hello, :after => p }.should raise_error NotAClassException
|
122
|
+
Proc.new{ Peep.hook_instances! :not_a_class, :hello, :after => p }.should raise_error NotAClassException
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should rise an error if a hook is declared twice" do
|
126
|
+
p = Proc.new{|klass, result| result}
|
127
|
+
Peep.hook_class! Dog, :train, :after => p
|
128
|
+
Proc.new{ Peep.hook_class! Dog, :train, :after => p }.should raise_error AlreadyDefinedHookException
|
129
|
+
|
130
|
+
Peep.hook_instances! Dog, :bark, :after => p
|
131
|
+
Proc.new{ Peep.hook_instances! Dog, :bark, :after => p }.should raise_error AlreadyDefinedHookException
|
132
|
+
|
133
|
+
scooby = Dog.new("scooby")
|
134
|
+
Peep.hook_object! scooby, :bark, :after => p
|
135
|
+
Proc.new{ Peep.hook_object! scooby, :bark, :after => p }.should raise_error AlreadyDefinedHookException
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'peeping')
|
3
|
+
require File.join(File.dirname(__FILE__), 'helpers', 'test_helpers')
|
4
|
+
|
5
|
+
include Peeping
|
6
|
+
|
7
|
+
describe Peep, "when calling hook methods" do
|
8
|
+
|
9
|
+
it_should_behave_like "a clean test"
|
10
|
+
|
11
|
+
it "should evaluate class proc calls" do
|
12
|
+
before_class_hook_called = false
|
13
|
+
after_class_hook_called = false
|
14
|
+
Peep.hook_class!(
|
15
|
+
Dog,
|
16
|
+
:eats?,
|
17
|
+
:before => Proc.new{|*args| before_class_hook_called = true },
|
18
|
+
:after => Proc.new{|*args| after_class_hook_called = true } )
|
19
|
+
Dog.eats?('french fries')
|
20
|
+
before_class_hook_called.should == true
|
21
|
+
after_class_hook_called.should == true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should evaluate instance proc calls" do
|
25
|
+
before_instance_hook_called = false
|
26
|
+
after_instance_hook_called = false
|
27
|
+
Peep.hook_instances!(
|
28
|
+
Dog,
|
29
|
+
:bark,
|
30
|
+
:before => Proc.new{|*args| before_instance_hook_called = true},
|
31
|
+
:after => Proc.new{|*args| after_instance_hook_called = true} )
|
32
|
+
Dog.new('scooby').bark(10)
|
33
|
+
before_instance_hook_called.should == true
|
34
|
+
after_instance_hook_called.should == true
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should evaluate singleton instance proc calls" do
|
38
|
+
before_singleton_hook_called = false
|
39
|
+
after_singleton_hook_called = false
|
40
|
+
scooby = Dog.new('scooby')
|
41
|
+
Peep.hook_object!(
|
42
|
+
scooby,
|
43
|
+
:bark,
|
44
|
+
:before => Proc.new{|*args| before_singleton_hook_called = true},
|
45
|
+
:after => Proc.new{|*args| after_singleton_hook_called = true} )
|
46
|
+
scooby.bark(10)
|
47
|
+
before_singleton_hook_called.should == true
|
48
|
+
after_singleton_hook_called.should == true
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should evaluate singleton hooks for singleton methods instead of instance methods" do
|
52
|
+
scooby = Dog.new('scooby')
|
53
|
+
class << scooby
|
54
|
+
def eat_scooby_snacks(how_many)
|
55
|
+
if how_many > 5
|
56
|
+
@mood = :happy
|
57
|
+
"Scooby doobee doo!"
|
58
|
+
else
|
59
|
+
@mood = :hungry
|
60
|
+
"I'm hungry, give me more!"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Peep.hook_object!(
|
66
|
+
scooby,
|
67
|
+
:eat_scooby_snacks,
|
68
|
+
:before => (Proc.new do |object, *params|
|
69
|
+
object.instance_variable_get(:@mood).should == nil
|
70
|
+
params.should == [10]
|
71
|
+
end),
|
72
|
+
:after => (Proc.new do |object, result|
|
73
|
+
object.should == scooby
|
74
|
+
result.should == "Scooby doobee doo!"
|
75
|
+
object.instance_variable_get(:@mood).should == :happy
|
76
|
+
end))
|
77
|
+
scooby.eat_scooby_snacks(10)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should always override instance method calls with singleton method calls" do
|
81
|
+
scooby = Dog.new('scooby')
|
82
|
+
Peep.hook_object!(
|
83
|
+
scooby,
|
84
|
+
:lick_owner,
|
85
|
+
:before => Proc.new{|object, *args| object.owner = "Shaggy" },
|
86
|
+
:after => Proc.new{|object, result| result.should == "Shaggy is now all wet!" } )
|
87
|
+
lassie = Dog.new('lassie')
|
88
|
+
Peep.hook_instances!(
|
89
|
+
Dog,
|
90
|
+
:lick_owner,
|
91
|
+
:after => Proc.new{|objetc, result| result.should == "I have no owner to lick :(" } )
|
92
|
+
scooby.lick_owner
|
93
|
+
lassie.lick_owner
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
describe Peep, "when using parameters in block calls" do
|
99
|
+
|
100
|
+
it_should_behave_like "a clean test"
|
101
|
+
|
102
|
+
it "should accept the same parameters in before hooks as the hooked class methods" do
|
103
|
+
Peep.hook_class!(
|
104
|
+
Dog,
|
105
|
+
:eats?,
|
106
|
+
:before => (Proc.new do |klass, *params|
|
107
|
+
klass.should == Dog
|
108
|
+
params.should == ['meat']
|
109
|
+
end))
|
110
|
+
Dog.eats?('meat')
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should accept the same parameters in before hooks as the hooked instance methods" do
|
114
|
+
scooby = Dog.new('scooby')
|
115
|
+
Peep.hook_instances!(
|
116
|
+
Dog,
|
117
|
+
:bark,
|
118
|
+
:before => (Proc.new do |object, *params|
|
119
|
+
object.should == scooby
|
120
|
+
params.should == [3]
|
121
|
+
end))
|
122
|
+
scooby.bark(3)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should accept the same parameters in before hooks as the hooked singleton method" do
|
126
|
+
scooby = Dog.new('scooby')
|
127
|
+
Peep.hook_object!(
|
128
|
+
scooby,
|
129
|
+
:bark,
|
130
|
+
:before => (Proc.new do |object, *params|
|
131
|
+
params.should == [4]
|
132
|
+
object.should == scooby
|
133
|
+
end))
|
134
|
+
scooby.bark(4)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should return the same object after a hooked class method call" do
|
138
|
+
Peep.hook_class!(
|
139
|
+
Dog,
|
140
|
+
:eats?,
|
141
|
+
:after => Proc.new{|klass, result| result.should == true } )
|
142
|
+
Dog.eats?('bones')
|
143
|
+
end
|
144
|
+
|
145
|
+
it "hould return the same object after a hooked instance method call" do
|
146
|
+
scooby = Dog.new('scooby')
|
147
|
+
Peep.hook_instances!(
|
148
|
+
Dog,
|
149
|
+
:bark,
|
150
|
+
:after => Proc.new{|klass, result| result.should == "woof woof woof!" } )
|
151
|
+
scooby.bark(3)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should return the same object after a hooked singleton method call" do
|
155
|
+
scooby = Dog.new('scooby')
|
156
|
+
Peep.hook_object!(
|
157
|
+
scooby,
|
158
|
+
:lick_owner,
|
159
|
+
:before => Proc.new{|object, *args| object.owner = "John" },
|
160
|
+
:after => Proc.new{|object, result| result.should == "John is now all wet!" })
|
161
|
+
scooby.lick_owner
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chubas-peeping
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ruben Medellin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-12 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Add, remove and manage hooks for class, instance and singleton method calls. Intended to be not a full Aspect Oriented Programming framework, but a lightweight simpler one.
|
17
|
+
email: ruben.medellin.c@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.markdown
|
25
|
+
files:
|
26
|
+
- .document
|
27
|
+
- .gitignore
|
28
|
+
- LICENSE
|
29
|
+
- README.markdown
|
30
|
+
- Rakefile
|
31
|
+
- VERSION
|
32
|
+
- lib/peeping.rb
|
33
|
+
- lib/peeping/exceptions.rb
|
34
|
+
- lib/peeping/hook.rb
|
35
|
+
- lib/peeping/peeping.rb
|
36
|
+
- lib/peeping/util.rb
|
37
|
+
- spec/helpers/test_helpers.rb
|
38
|
+
- spec/hook_methods_spec.rb
|
39
|
+
- spec/hooked_behavior_spec.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/chubas/peeping
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --charset=UTF-8
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements:
|
60
|
+
- none
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.2.0
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: Lightweight AOP framework for managing method hooks
|
66
|
+
test_files:
|
67
|
+
- spec/hooked_behavior_spec.rb
|
68
|
+
- spec/hook_methods_spec.rb
|