zimbatm-monkeypatch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,34 @@
1
+ = MonkeyPatch
2
+
3
+ <INSERT NICE INTRO HERE>
4
+
5
+ Do you monkeys, patch ? If so, use this library, you won't regret it.
6
+
7
+ Why use a library when you could do all this by hand ? For two reasons:
8
+
9
+ 1. We provide the mechanism to protect from patch collision
10
+ 2. By including this gem as dependency, you declare your project is
11
+ monkeypatching.
12
+
13
+ </INSERT NICE INTRO HERE>
14
+
15
+ == Usage
16
+
17
+ Example usage:
18
+
19
+ :include:example/patch_usage.rb
20
+
21
+ == Related projects
22
+
23
+ * http://github.com/coderrr/monkey_shield/ : provides sorts of namespaces to avoid patch collision
24
+
25
+ == Ideas
26
+
27
+ * method re-definition or module/class extension could be detected, especially when using Gems. The load-path is not the same between the original definition and the new-one.
28
+ * load-path as namespace
29
+
30
+ == TODO
31
+
32
+ * Add programmable patching conditions
33
+ * Add reason string
34
+ * Add 'monkeywarn' that warns when a monkeypatch is applied
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ $:.push('lib')
2
+ require 'monkeypatch'
3
+
4
+ Dir['task/*.rake'].each do |lib|
5
+ load lib
6
+ end
7
+
8
+ desc "Same as `rake test`"
9
+ task :default => :test
@@ -0,0 +1,24 @@
1
+ require 'monkeypatch'
2
+
3
+ # Define a new extension that adds the #to_blob method
4
+ date_patch = MonkeyPatch.add_method(:to_blob) do
5
+ def to_blob; "<blob>" end
6
+ end
7
+
8
+ x = "something"
9
+ date_patch.patch_instance(x)
10
+ x.to_blob #=> "<blob>"
11
+
12
+ # Define a patch, that replaces the #to_date method
13
+ each_patch = MonkeyPatch.replace_method(:to_s) do
14
+ def to_s; "..." end
15
+ end
16
+
17
+ class ExampleClass
18
+ def to_s; "hello" end
19
+ end
20
+
21
+ (date_patch & each_patch).patch_class(ExampleClass)
22
+
23
+ ExampleClass.new.to_s #=> "..."
24
+ ExampleClass.new.to_blob #=> "<blob>"
@@ -0,0 +1,212 @@
1
+ =begin rdoc
2
+
3
+ This is the public API to produce new patches.
4
+
5
+ Once you have a patch, look at Patch and it's children to see how to use it.
6
+
7
+ =end
8
+ module MonkeyPatch
9
+ # MonkeyPatch's version as a string
10
+ VERSION = '0.1.0'
11
+ # May be raised on check_conflicts
12
+ class ConflictError < StandardError; end
13
+
14
+ # A collection of patches. Used to collect one or more patches with the #& operator
15
+ #
16
+ # NOTE: didn't use the Set class, to not force a dependency
17
+ class PatchSet
18
+ def initialize(patches) #:nodoc:
19
+ @patches = patches.to_a.uniq
20
+ end
21
+
22
+ # Aggregates Patch (es) and PatchSet (s)
23
+ def &(other)
24
+ PatchSet.new(@patches + other.to_a)
25
+ end
26
+ def to_a; @patches.dup end
27
+
28
+ # Delegates to patches
29
+ # TODO: determine what happens if a patch fails
30
+ def patch_class(klass)
31
+ @patches.each do |patch|
32
+ patch.patch_class(klass)
33
+ end
34
+ end
35
+
36
+ # Delegates to patches
37
+ def patch_instance(obj)
38
+ for patch in @patches
39
+ patch.patch_instance(obj)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Abstract definition of a patch.
45
+ #
46
+ # You cannot create Patch instance yourself, they are spawned from
47
+ # MonkeyPatch class-methods.
48
+ class Patch
49
+ class << self
50
+ # Hide new to force api usage
51
+ private :new
52
+ end
53
+ # The callstack it was defined in
54
+ attr_reader :from
55
+
56
+ def initialize(&patch_def) #:nodoc:
57
+ raise ArgumentError, "patch_def not given" unless block_given?
58
+ @patch_def = patch_def
59
+ end
60
+
61
+ # Combine patches together. Produces a PatchSet instance.
62
+ def &(other)
63
+ PatchSet.new([self]) & other
64
+ end
65
+
66
+ # Returns [self], used by #&
67
+ def to_a; [self] end
68
+
69
+ # Patches a class or module instance methods
70
+ def patch_class(klass)
71
+ raise ArgumentError, "klass is not a Class" unless klass.kind_of?(Class)
72
+
73
+ return false if !check_conditions(klass)
74
+
75
+ check_conflicts!(klass)
76
+
77
+ # Apply
78
+ apply_patch(klass)
79
+
80
+ return true
81
+ end
82
+
83
+ # Patches the instance's metaclass
84
+ def patch_instance(obj)
85
+ meta = (class << obj; self end)
86
+ patch_class(meta)
87
+ end
88
+
89
+ protected
90
+
91
+ # Condition are checks that raise nothing
92
+ #
93
+ # If a condition isn't met, the patch is not applied
94
+ #
95
+ # Returns true if all conditions are matched
96
+ def check_conditions(klass) #:nodoc:
97
+ if klass.respond_to?(:applied_patches) && klass.applied_patches.include?(self)
98
+ log "WARN: Patch already applied"
99
+ return false
100
+ end
101
+ true
102
+ end
103
+
104
+ # Re-implement in childs. Make sure super is called
105
+ #
106
+ # raises a ConflictError if an error is found
107
+ def check_conflicts!(klass) #:nodoc:
108
+ end
109
+
110
+ def apply_patch(klass) #:nodoc:
111
+ klass.extend IsPatched
112
+ klass.class_eval(&@patch_def)
113
+ klass.applied_patches.push self
114
+ end
115
+
116
+ def log(msg) #:nodoc:
117
+ MonkeyPatch.logger.log(msg)
118
+ end
119
+ end
120
+
121
+ class MethodPatch < Patch
122
+ # The name of the method to be patched
123
+ attr_reader :method_name
124
+ def initialize(method_name, &patch_def) #:nodoc:
125
+ super(&patch_def)
126
+ @method_name = method_name.to_s
127
+ unless Module.new(&patch_def).instance_methods(true) == [@method_name]
128
+ raise ArgumentError, "&patch_def does not define the specified method"
129
+ end
130
+ end
131
+
132
+ protected
133
+ def check_conflicts!(klass) #:nodoc:
134
+ super
135
+ if klass.respond_to? :applied_patches
136
+ others = klass.applied_patches.select{|p| p.respond_to?(:method_name) && p.method_name == method_name }
137
+ if others.any?
138
+ raise ConflictError, "Conflicting patches: #{([self] + others).inspect}"
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # Spawned by MonkeyPatch.add_method
145
+ class AddMethodPatch < MethodPatch
146
+ protected
147
+ def check_conflicts!(klass) #:nodoc:
148
+ super
149
+ if klass.method_defined?(method_name)
150
+ raise ConflictError, "Add already existing method #{method_name} in #{klass}"
151
+ end
152
+ end
153
+ end
154
+
155
+ # Spawned by MonkeyPatch.replace_method
156
+ class ReplaceMethodPatch < MethodPatch
157
+ protected
158
+ def check_conflicts!(klass) #:nodoc:
159
+ super
160
+ unless klass.method_defined?(method_name)
161
+ raise ConflictError, "Replacing method #{method_name} does not exist in #{klass}"
162
+ end
163
+ end
164
+ end
165
+
166
+ # Default MonkeyPatch::logger . Use the same interface
167
+ # if you want to replace it.
168
+ class STDERRLogger
169
+ def log(msg); STDERR.puts msg end
170
+ end
171
+
172
+ # This module extends patched objects to keep track of the applied patches
173
+ module IsPatched
174
+ def applied_patches; @__applied_patches__ ||= [] end
175
+ end
176
+
177
+ @logger = STDERRLogger.new
178
+ @loaded_patches = []
179
+ class << self
180
+ # Here goes the messages, this object should respond_to? :log
181
+ # Default is STDERRLogger
182
+ attr_accessor :logger
183
+
184
+ # All defined patches are stored here
185
+ attr_reader :loaded_patches
186
+
187
+ # Creates a new patch that adds a method to a class or a module
188
+ #
189
+ # Returns a Patch (AddMethodPatch)
190
+ def add_method(method_name, &definition)
191
+ new_patch AddMethodPatch.send(:new, method_name, &definition)
192
+ end
193
+
194
+ # Creates a new patch that replaces a method of a class or a module
195
+ #
196
+ # Returns a Patch (ReplaceMethodPatch)
197
+ def replace_method(method_name, &definition)
198
+ new_patch ReplaceMethodPatch.send(:new, method_name, &definition)
199
+ end
200
+
201
+ protected
202
+
203
+ def new_patch(patch, &definition) #:nodoc:
204
+ #patch.validate
205
+ patch.instance_variable_set(:@from, caller[1])
206
+ @loaded_patches.push(patch)
207
+ patch
208
+ end
209
+
210
+ end
211
+ end
212
+
data/task/gem.rake ADDED
@@ -0,0 +1,37 @@
1
+ require 'rake/gempackagetask'
2
+ require 'monkeypatch'
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.name = 'monkeypatch'
6
+ s.version = MonkeyPatch::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = "Monkey patching made safe(er)"
9
+ s.homepage = "http://github.com/zimbatm/ruby-monkeypatch"
10
+ s.description = "Provides a mechanism to avoid patch collision. It's also useful to tell if your project is using monkeypatching or not."
11
+ s.authors = ["zimbatm"]
12
+ s.email = "zimbatm@oree.ch"
13
+ s.has_rdoc = true
14
+ s.files = FileList['README.rdoc', 'Rakefile', 'lib/*', 'test/*', 'task/*', 'example/*']
15
+ s.test_files = FileList['test/test*.rb']
16
+ end
17
+
18
+ file "ruby-monkeypatch.gemspec" do |t|
19
+ File.open(t.name, 'w') do |f|
20
+ f.write(spec.to_ruby)
21
+ end
22
+ end
23
+
24
+ Rake::GemPackageTask.new(spec) do |pkg|
25
+ # pkg.need_zip = true
26
+ # pkg.need_tar = true
27
+ end
28
+
29
+ namespace :gem do
30
+ desc "Updates the ruby-monkeypatch.gemspec file"
31
+ task :spec do
32
+ File.open("ruby-monkeypatch.gemspec", 'w') do |f|
33
+ f.write(spec.to_ruby)
34
+ end
35
+ end
36
+ end
37
+
data/task/rcov.rake ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'rcov/rcovtask'
3
+
4
+ Rcov::RcovTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/test*.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ rescue LoadError
11
+ if !Rake.application.options.silent
12
+ STDERR.puts "*** Install the RCov for code coverage"
13
+ end
14
+ end
data/task/rdoc.rake ADDED
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'rdoc/task'
3
+ RDocTask = RDoc::Task
4
+ rescue LoadError
5
+ if !Rake.application.options.silent
6
+ STDERR.puts "*** Install the RDoc 2.X gem for nicer docs"
7
+ end
8
+
9
+ require 'rake/rdoctask'
10
+ RDocTask = Rake::RDocTask
11
+ end
12
+
13
+ RDocTask.new do |rd|
14
+ rd.main = "README.rdoc"
15
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
16
+ end
17
+
data/task/test.rake ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList['test/test*.rb']
6
+ t.verbose = true
7
+ t.ruby_opts = ["-Ilib"]
8
+ end
@@ -0,0 +1,139 @@
1
+ require 'test/unit'
2
+
3
+ require 'monkeypatch'
4
+
5
+ class TestMonkeypatch < Test::Unit::TestCase
6
+ include MonkeyPatch
7
+
8
+ class FakeLogger
9
+ attr_reader :logs
10
+ def initialize
11
+ @logs = []
12
+ end
13
+ def log(msg)
14
+ @logs.push msg
15
+ end
16
+ end
17
+
18
+ def setup
19
+ @logger = MonkeyPatch.logger = FakeLogger.new
20
+ @logs = @logger.logs
21
+ @c = Class.new do
22
+ def existing_method; "original" end
23
+ end
24
+ @p_add = MonkeyPatch.add_method(:new_method) do
25
+ def new_method; "exists" end
26
+ end
27
+ @p_repl = MonkeyPatch.replace_method(:existing_method) do
28
+ def existing_method; "replaced" end
29
+ end
30
+ end
31
+
32
+ def test_invalid_patches
33
+ assert_raise(ArgumentError) do
34
+ MonkeyPatch.add_method(:new_method)
35
+ end
36
+ assert_raise(ArgumentError) do
37
+ MonkeyPatch.replace_method(:new_method)
38
+ end
39
+ assert_raise(ArgumentError) do
40
+ MonkeyPatch.add_method(:new_method) do
41
+ def new_method_typo; end
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_do_not_apply_twice
47
+ @p_add.patch_class(@c)
48
+ @p_add.patch_class(@c)
49
+ assert_equal 1, @logs.size, "Should be the notice about the twice application"
50
+ assert_equal [@p_add], @c.applied_patches
51
+ end
52
+
53
+ def test_add_method
54
+ @p_add.patch_class(@c)
55
+
56
+ assert_equal("exists", @c.new.new_method )
57
+ end
58
+
59
+ def test_check_conflict
60
+ p1 = MonkeyPatch.add_method(:some) do
61
+ def some; "one" end
62
+ end
63
+ p2 = MonkeyPatch.add_method(:some) do
64
+ def some; "thing" end
65
+ end
66
+ c = Class.new
67
+ assert_nothing_raised do
68
+ p1.patch_class(c)
69
+ end
70
+ assert_raise(ConflictError) do
71
+ p2.patch_class(c)
72
+ end
73
+ end
74
+
75
+ def test_replace_method
76
+ @p_repl.patch_class(@c)
77
+
78
+ assert @c.respond_to?(:applied_patches)
79
+
80
+ assert_equal("replaced", @c.new.existing_method )
81
+ end
82
+
83
+ def test_patch_class_application
84
+ assert !@c.new.respond_to?(:applied_patches)
85
+ @p_add.patch_class(@c)
86
+ assert_respond_to @c, :applied_patches
87
+
88
+ assert_equal [@p_add], @c.applied_patches
89
+
90
+ @p_repl.patch_class(@c)
91
+ assert_equal [@p_add, @p_repl], @c.applied_patches
92
+ end
93
+
94
+ def test_patch_instance_application
95
+ p = MonkeyPatch.replace_method(:to_s) do
96
+ def to_s; "right" end
97
+ end
98
+ i = "left"
99
+ p.patch_instance(i)
100
+ assert_equal("right", i.to_s)
101
+ end
102
+
103
+ def test_replace_conflict
104
+ c = Class.new
105
+ assert_raise(ConflictError) do
106
+ @p_repl.patch_class(c)
107
+ end
108
+ assert !c.new.respond_to?(:existing_method)
109
+ end
110
+
111
+ def test_add_method_conflict
112
+ c = Class.new do
113
+ def new_method; "not replaced" end
114
+ end
115
+ assert_raise(ConflictError) do
116
+ @p_add.patch_class(c)
117
+ end
118
+ assert_equal "not replaced", c.new.new_method
119
+ end
120
+
121
+ def test_patch_from
122
+ assert_equal(File.basename(__FILE__), File.basename(@p_add.from).gsub(/:.*/,''))
123
+ end
124
+
125
+ def test_patch_set
126
+ both = (@p_add & @p_repl)
127
+ assert_equal(PatchSet, both.class)
128
+
129
+ inst = @c.new
130
+ both.patch_instance(inst)
131
+ assert_equal("exists", inst.new_method )
132
+ assert_equal("replaced", inst.existing_method )
133
+
134
+ both.patch_class(@c)
135
+ assert_equal("exists", @c.new.new_method )
136
+ assert_equal("replaced", @c.new.existing_method )
137
+ end
138
+
139
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zimbatm-monkeypatch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - zimbatm
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-26 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Provides a mechanism to avoid patch collision. It's also useful to tell if your project is using monkeypatching or not.
17
+ email: zimbatm@oree.ch
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README.rdoc
26
+ - Rakefile
27
+ - lib/monkeypatch.rb
28
+ - test/test_monkeypatch.rb
29
+ - task/gem.rake
30
+ - task/rcov.rake
31
+ - task/rdoc.rake
32
+ - task/test.rake
33
+ - example/patch_usage.rb
34
+ has_rdoc: true
35
+ homepage: http://github.com/zimbatm/ruby-monkeypatch
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: Monkey patching made safe(er)
60
+ test_files:
61
+ - test/test_monkeypatch.rb