monkey_bars 0.1.2 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d42e4b3ee72dbd54a294b38ad21d21a43a08c3c941b0a9dab17bcdb4508f227f
4
- data.tar.gz: 47f08f29eb06feb4b73d70679aef3769d4cab20ee3d32b08561522d869e27feb
3
+ metadata.gz: 959654729be5d09d997d89340df3d00652a0a39f962cbf0f87a3223a24b22195
4
+ data.tar.gz: b36eaea242ef45cb939040f6436af3ed3f9cb806f261141ab5c68e9ba7c24967
5
5
  SHA512:
6
- metadata.gz: 0fceae1976fc70683a2aa5633ed612c5b207ddefe66d01666fee6950130d888070bcdfe3cd47af0c3be24cea92ef8167cfeb67ae8d7379eb1b9e0961555fc3bb
7
- data.tar.gz: e6dff3e7bc72d25a1fec7b903b022aad32c5440c1228b6c8c9409158aee9523a2fff23c41691a31b2e0e95c1f3cb48937df92e088f42b0aab124433d21a0b289
6
+ metadata.gz: 5084d4cc7c224bf0fcf5eeaae3aa02da22ea2b5cac93e8561983bb1d1d3d61a371f7a5401d319117b9efce894ff2e85c1f908a7e2a8777d585791160a4959359
7
+ data.tar.gz: 4633ed06204dd319134fd7c05ff437c649c1c441ab890abe4b3c51b38bcc02c0bc976c41f7e634d17b9bc461b2d0294007bc055f82fa8c71b47e4770322eacd2
data/README.md CHANGED
@@ -73,6 +73,7 @@ declared inside a block and then applied with `patch` (immediately) or
73
73
  - `patch!` applies a prepared patch
74
74
 
75
75
  If `patch!` runs without any methods or constants defined, it emits a warning.
76
+ Calling `prepare_for_patching` or `patch!` more than once raises an error.
76
77
 
77
78
  The `monkey` can be:
78
79
 
@@ -96,6 +97,30 @@ You can call any of these helpers multiple times; MonkeyBars will combine them.
96
97
  This is helpful when you want to ignore some arity check errors (see below) for
97
98
  some methods but not others.
98
99
 
100
+ When patching existing methods, visibility must match the target method exactly.
101
+ If the target is public, keep the patch method public. If the target is
102
+ protected/private, mark the patch method as protected/private in the block.
103
+
104
+ ```ruby
105
+ patch_instance_methods do
106
+ private
107
+
108
+ def internal_token
109
+ "#{super}-patched"
110
+ end
111
+ end
112
+ ```
113
+
114
+ ```ruby
115
+ patch_instance_methods do
116
+ def internal_token(*args)
117
+ super(*args)
118
+ end
119
+
120
+ protected :internal_token
121
+ end
122
+ ```
123
+
99
124
  ### Method arity checks
100
125
 
101
126
  When patching existing methods, arity must match by default. You can opt out:
@@ -209,11 +234,16 @@ MonkeyBars raises specific errors to keep patches safe and explicit:
209
234
  - `MonkeyBars::NoPatchableClassMethodFoundError`
210
235
  - `MonkeyBars::MismatchedInstanceMethodArityError`
211
236
  - `MonkeyBars::MismatchedClassMethodArityError`
237
+ - `MonkeyBars::PatchableInstanceMethodIsPrivateError`
238
+ - `MonkeyBars::PatchableInstanceMethodIsNotPrivateError`
239
+ - `MonkeyBars::PatchableClassMethodIsPrivateError`
240
+ - `MonkeyBars::PatchableClassMethodIsNotPrivateError`
212
241
  - `MonkeyBars::NewInstanceMethodAlreadyExistsError`
213
242
  - `MonkeyBars::NewClassMethodAlreadyExistsError`
214
243
  - `MonkeyBars::PatchConstantNotFoundError`
215
244
  - `MonkeyBars::NewConstantAlreadyExistsError`
216
245
  - `MonkeyBars::PatchAlreadyPerformedError`
246
+ - `MonkeyBars::PrepareForPatchingAlreadyPerformedError`
217
247
 
218
248
  ## Development
219
249
 
data/docs/llm-usage.md CHANGED
@@ -46,6 +46,8 @@ compares it with `version` using exact equality. Mismatches raise
46
46
  - `prepare_for_patching(...)` validates and stores the patch.
47
47
  - `patch!` applies a previously prepared patch and can only be called once.
48
48
 
49
+ Calling `prepare_for_patching` more than once raises
50
+ `MonkeyBars::PrepareForPatchingAlreadyPerformedError`.
49
51
  Calling `patch!` more than once raises `MonkeyBars::PatchAlreadyPerformedError`.
50
52
 
51
53
  ## API reference
@@ -86,6 +88,14 @@ Overrides existing instance methods via `prepend`.
86
88
  - Error: `MonkeyBars::NoPatchableInstanceMethodFoundError` if method does not exist.
87
89
  - Error: `MonkeyBars::MismatchedInstanceMethodArityError` when arity differs,
88
90
  unless `ignore_arity_errors: true` is set.
91
+ - Error: `MonkeyBars::PatchableInstanceMethodIsPrivateError` when the target
92
+ method is private but the patch method is defined as public/protected.
93
+ - Error: `MonkeyBars::PatchableInstanceMethodIsNotPrivateError` when the target
94
+ method is public/protected but the patch method is marked `private`.
95
+ - Error: `MonkeyBars::PatchableInstanceMethodIsProtectedError` when the target
96
+ method is protected but the patch method is defined as public/private.
97
+ - Error: `MonkeyBars::PatchableInstanceMethodIsNotProtectedError` when the target
98
+ method is public/private but the patch method is marked `protected`.
89
99
  - `include_super_super: true` makes `#super_super` available for these methods.
90
100
 
91
101
  #### `new_class_methods(&block)`
@@ -101,6 +111,14 @@ Overrides existing class methods via `singleton_class.prepend`.
101
111
  - Error: `MonkeyBars::NoPatchableClassMethodFoundError` if method does not exist.
102
112
  - Error: `MonkeyBars::MismatchedClassMethodArityError` when arity differs,
103
113
  unless `ignore_arity_errors: true` is set.
114
+ - Error: `MonkeyBars::PatchableClassMethodIsPrivateError` when the target class
115
+ method is private but the patch method is defined as public/protected.
116
+ - Error: `MonkeyBars::PatchableClassMethodIsNotPrivateError` when the target
117
+ class method is public/protected but the patch method is marked `private`.
118
+ - Error: `MonkeyBars::PatchableClassMethodIsProtectedError` when the target class
119
+ method is protected but the patch method is defined as public/private.
120
+ - Error: `MonkeyBars::PatchableClassMethodIsNotProtectedError` when the target
121
+ class method is public/private but the patch method is marked `protected`.
104
122
  - `include_super_super: true` makes `#super_super` available for these methods.
105
123
 
106
124
  #### `patch_constants(&block)`
@@ -140,6 +158,24 @@ class FixPatch
140
158
  end
141
159
  ```
142
160
 
161
+ ### Patch a protected method
162
+
163
+ ```ruby
164
+ class ProtectedPatch
165
+ extend MonkeyBars
166
+
167
+ patch(SomeLibrary, version: "1.0.0", version_check: -> { SomeLibrary::VERSION }) do
168
+ patch_instance_methods do
169
+ def internal_token(value)
170
+ super(value) + "-patched"
171
+ end
172
+
173
+ protected :internal_token
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
143
179
  ### Add new class method and constant
144
180
 
145
181
  ```ruby
@@ -200,8 +236,31 @@ Use this section when the patch fails. The message usually suggests the fix.
200
236
  Update the version string or the version check.
201
237
  - `MonkeyBars::NoPatchableInstanceMethodFoundError`: method does not exist.
202
238
  Move it to `new_instance_methods` or fix the method name.
239
+ - `MonkeyBars::PatchableInstanceMethodIsPrivateError`: target method is private
240
+ but patch method is public/protected. Mark the patch method as `private`.
241
+ - `MonkeyBars::PatchableInstanceMethodIsNotPrivateError`: target method is
242
+ public/protected but patch method is private. Remove `private` in the patch
243
+ block or patch a private target.
244
+ - `MonkeyBars::PatchableInstanceMethodIsProtectedError`: target method is
245
+ protected but patch method is public/private. Mark the patch method as
246
+ `protected`.
247
+ - `MonkeyBars::PatchableInstanceMethodIsNotProtectedError`: target method is
248
+ public/private but patch method is protected. Remove `protected` in the patch
249
+ block or patch a protected target.
203
250
  - `MonkeyBars::NoPatchableClassMethodFoundError`: method does not exist.
204
251
  Move it to `new_class_methods` or fix the method name.
252
+ - `MonkeyBars::PatchableClassMethodIsPrivateError`: target class method is
253
+ private but patch method is public/protected. Mark the patch method as
254
+ `private`.
255
+ - `MonkeyBars::PatchableClassMethodIsNotPrivateError`: target class method is
256
+ public/protected but patch method is private. Remove `private` in the patch
257
+ block or patch a private target.
258
+ - `MonkeyBars::PatchableClassMethodIsProtectedError`: target class method is
259
+ protected but patch method is public/private. Mark the patch method as
260
+ `protected`.
261
+ - `MonkeyBars::PatchableClassMethodIsNotProtectedError`: target class method is
262
+ public/private but patch method is protected. Remove `protected` in the patch
263
+ block or patch a protected target.
205
264
  - `MonkeyBars::MismatchedInstanceMethodArityError`: arity differs.
206
265
  Match the signature or set `ignore_arity_errors: true`.
207
266
  - `MonkeyBars::MismatchedClassMethodArityError`: arity differs.
@@ -216,6 +275,9 @@ Use this section when the patch fails. The message usually suggests the fix.
216
275
  Move it to `patch_constants`.
217
276
  - `MonkeyBars::PatchAlreadyPerformedError`: `patch!` was called twice.
218
277
  Ensure you only call `patch!` once per prepared patch.
278
+ - `MonkeyBars::PrepareForPatchingAlreadyPerformedError`:
279
+ `prepare_for_patching` was called twice. Ensure you only prepare once per
280
+ patcher.
219
281
 
220
282
  ## Best practices for LLMs
221
283
 
@@ -223,6 +285,8 @@ Use this section when the patch fails. The message usually suggests the fix.
223
285
  - Prefer `patch(...)` for immediate application unless delayed patching is required.
224
286
  - Use `patch_*` helpers for existing methods/constants and `new_*` for new ones.
225
287
  - Keep patched method arity identical to the original unless explicitly allowed.
288
+ - Keep patched method visibility aligned exactly with the target
289
+ (`public`/`protected`/`private`).
226
290
  - Add targeted tests around the patched behavior; avoid testing internal details.
227
291
 
228
292
  ## Testing checklist
@@ -61,6 +61,54 @@ module MonkeyBars
61
61
  end
62
62
  end
63
63
 
64
+ class PatchableClassMethodIsNotPrivateError < StandardError
65
+ def initialize(patcher_name, monkey: nil, method: nil)
66
+ super("[#{patcher_name}] The class method `.#{method}` on `#{monkey}` is not private, but is on the patch. Remove it from `private` in your patch.")
67
+ end
68
+ end
69
+
70
+ class PatchableClassMethodIsPrivateError < StandardError
71
+ def initialize(patcher_name, monkey: nil, method: nil)
72
+ super("[#{patcher_name}] The class method `.#{method}` on `#{monkey}` is private, but isn't on the patch. Mark it as `private` in your patch.")
73
+ end
74
+ end
75
+
76
+ class PatchableClassMethodIsNotProtectedError < StandardError
77
+ def initialize(patcher_name, monkey: nil, method: nil)
78
+ super("[#{patcher_name}] The class method `.#{method}` on `#{monkey}` is not protected, but is on the patch. Remove it from `protected` in your patch.")
79
+ end
80
+ end
81
+
82
+ class PatchableClassMethodIsProtectedError < StandardError
83
+ def initialize(patcher_name, monkey: nil, method: nil)
84
+ super("[#{patcher_name}] The class method `.#{method}` on `#{monkey}` is protected, but isn't on the patch. Mark it as `protected` in your patch.")
85
+ end
86
+ end
87
+
88
+ class PatchableInstanceMethodIsNotPrivateError < StandardError
89
+ def initialize(patcher_name, monkey: nil, method: nil)
90
+ super("[#{patcher_name}] The instance method `##{method}` on `#{monkey}` is not private, but is on the patch. Remove it from `private` in your patch.")
91
+ end
92
+ end
93
+
94
+ class PatchableInstanceMethodIsPrivateError < StandardError
95
+ def initialize(patcher_name, monkey: nil, method: nil)
96
+ super("[#{patcher_name}] The instance method `##{method}` on `#{monkey}` is private, but isn't on the patch. Mark it as `private` in your patch.")
97
+ end
98
+ end
99
+
100
+ class PatchableInstanceMethodIsNotProtectedError < StandardError
101
+ def initialize(patcher_name, monkey: nil, method: nil)
102
+ super("[#{patcher_name}] The instance method `##{method}` on `#{monkey}` is not protected, but is on the patch. Remove it from `protected` in your patch.")
103
+ end
104
+ end
105
+
106
+ class PatchableInstanceMethodIsProtectedError < StandardError
107
+ def initialize(patcher_name, monkey: nil, method: nil)
108
+ super("[#{patcher_name}] The instance method `##{method}` on `#{monkey}` is protected, but isn't on the patch. Mark it as `protected` in your patch.")
109
+ end
110
+ end
111
+
64
112
  class PatchAlreadyPerformedError < StandardError
65
113
  def initialize(patcher_name)
66
114
  super("[#{patcher_name}] `#patch!` has already been called and cannot be called again")
@@ -72,4 +120,10 @@ module MonkeyBars
72
120
  super("[#{patcher_name}] Couldn't find constant `#{constant}` on `#{monkey}` despite it being marked as patchable. Perhaps move it to the `new_constants` block?")
73
121
  end
74
122
  end
123
+
124
+ class PrepareForPatchingAlreadyPerformedError < StandardError
125
+ def initialize(patcher_name)
126
+ super("[#{patcher_name}] `#prepare_for_patching` has already been called and cannot be called again")
127
+ end
128
+ end
75
129
  end
@@ -34,6 +34,16 @@ module MonkeyBars
34
34
  preexisting_instance_method => {block:, ignore_arity_errors:, include_super_super:}
35
35
  module_to_prepend.module_eval(&block)
36
36
  module_to_prepend.instance_methods.each do |method_name|
37
+ next if module_to_prepend.protected_method_defined?(method_name)
38
+
39
+ if @monkey.protected_method_defined?(method_name)
40
+ raise(PatchableInstanceMethodIsProtectedError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
41
+ end
42
+
43
+ if @monkey.private_method_defined?(method_name)
44
+ raise(PatchableInstanceMethodIsPrivateError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
45
+ end
46
+
37
47
  unless @monkey.method_defined?(method_name)
38
48
  raise(NoPatchableInstanceMethodFoundError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
39
49
  end
@@ -47,6 +57,42 @@ module MonkeyBars
47
57
  end
48
58
  end
49
59
 
60
+ module_to_prepend.protected_instance_methods.each do |method_name|
61
+ if @monkey.private_method_defined?(method_name) || (@monkey.method_defined?(method_name) && !@monkey.protected_method_defined?(method_name))
62
+ raise(PatchableInstanceMethodIsNotProtectedError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
63
+ end
64
+
65
+ unless @monkey.protected_method_defined?(method_name)
66
+ raise(NoPatchableInstanceMethodFoundError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
67
+ end
68
+
69
+ next if ignore_arity_errors
70
+
71
+ prepatched_method = @monkey.instance_method(method_name)
72
+ patched_method = module_to_prepend.instance_method(method_name)
73
+ if prepatched_method.arity != patched_method.arity
74
+ raise(MismatchedInstanceMethodArityError.new(@monkey_patcher_name, monkey: @monkey, method: method_name, prepatched_arity: prepatched_method.arity, patched_arity: patched_method.arity))
75
+ end
76
+ end
77
+
78
+ module_to_prepend.private_instance_methods.each do |method_name|
79
+ if @monkey.method_defined?(method_name)
80
+ raise(PatchableInstanceMethodIsNotPrivateError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
81
+ end
82
+
83
+ unless @monkey.private_method_defined?(method_name)
84
+ raise(NoPatchableInstanceMethodFoundError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
85
+ end
86
+
87
+ next if ignore_arity_errors
88
+
89
+ prepatched_method = @monkey.instance_method(method_name)
90
+ patched_method = module_to_prepend.instance_method(method_name)
91
+ if prepatched_method.arity != patched_method.arity
92
+ raise(MismatchedInstanceMethodArityError.new(@monkey_patcher_name, monkey: @monkey, method: method_name, prepatched_arity: prepatched_method.arity, patched_arity: patched_method.arity))
93
+ end
94
+ end
95
+
50
96
  # If anyone asks for super_super, include it
51
97
  @include_instance_super_super = true if include_super_super
52
98
  @patch_instance_methods_module.module_eval(&block)
@@ -80,6 +126,16 @@ module MonkeyBars
80
126
  preexisting_class_method => {block:, ignore_arity_errors:, include_super_super:}
81
127
  module_to_prepend.module_eval(&block)
82
128
  module_to_prepend.instance_methods.each do |method_name|
129
+ next if module_to_prepend.protected_method_defined?(method_name)
130
+
131
+ if @monkey.singleton_class.protected_method_defined?(method_name)
132
+ raise(PatchableClassMethodIsProtectedError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
133
+ end
134
+
135
+ if @monkey.singleton_class.private_method_defined?(method_name)
136
+ raise(PatchableClassMethodIsPrivateError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
137
+ end
138
+
83
139
  unless @monkey.singleton_class.method_defined?(method_name)
84
140
  raise(NoPatchableClassMethodFoundError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
85
141
  end
@@ -93,6 +149,42 @@ module MonkeyBars
93
149
  end
94
150
  end
95
151
 
152
+ module_to_prepend.protected_instance_methods.each do |method_name|
153
+ if @monkey.singleton_class.private_method_defined?(method_name) || (@monkey.singleton_class.method_defined?(method_name) && !@monkey.singleton_class.protected_method_defined?(method_name))
154
+ raise(PatchableClassMethodIsNotProtectedError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
155
+ end
156
+
157
+ unless @monkey.singleton_class.protected_method_defined?(method_name)
158
+ raise(NoPatchableClassMethodFoundError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
159
+ end
160
+
161
+ next if ignore_arity_errors
162
+
163
+ prepatched_method = @monkey.singleton_class.instance_method(method_name)
164
+ patched_method = module_to_prepend.instance_method(method_name)
165
+ if prepatched_method.arity != patched_method.arity
166
+ raise(MismatchedClassMethodArityError.new(@monkey_patcher_name, monkey: @monkey, method: method_name, prepatched_arity: prepatched_method.arity, patched_arity: patched_method.arity))
167
+ end
168
+ end
169
+
170
+ module_to_prepend.private_instance_methods.each do |method_name|
171
+ if @monkey.singleton_class.method_defined?(method_name)
172
+ raise(PatchableClassMethodIsNotPrivateError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
173
+ end
174
+
175
+ unless @monkey.singleton_class.private_method_defined?(method_name)
176
+ raise(NoPatchableClassMethodFoundError.new(@monkey_patcher_name, monkey: @monkey, method: method_name))
177
+ end
178
+
179
+ next if ignore_arity_errors
180
+
181
+ prepatched_method = @monkey.singleton_class.instance_method(method_name)
182
+ patched_method = module_to_prepend.instance_method(method_name)
183
+ if prepatched_method.arity != patched_method.arity
184
+ raise(MismatchedClassMethodArityError.new(@monkey_patcher_name, monkey: @monkey, method: method_name, prepatched_arity: prepatched_method.arity, patched_arity: patched_method.arity))
185
+ end
186
+ end
187
+
96
188
  # If anyone asks for super_super, include it
97
189
  @include_class_super_super = true if include_super_super
98
190
  @patch_class_methods_module.module_eval(&block)
@@ -124,7 +216,7 @@ module MonkeyBars
124
216
  module_to_redefine = Module.new
125
217
  module_to_redefine.module_eval(&redefined_constant)
126
218
  module_to_redefine.constants(false).each do |const_name|
127
- unless @monkey.constants(false).include?(const_name)
219
+ unless @monkey.const_defined?(const_name, false)
128
220
  raise(PatchConstantNotFoundError.new(@monkey_patcher_name, monkey: @monkey, constant: const_name))
129
221
  end
130
222
  end
@@ -141,7 +233,7 @@ module MonkeyBars
141
233
  module_to_include = Module.new
142
234
  module_to_include.module_eval(&new_constant)
143
235
  module_to_include.constants(false).each do |const_name|
144
- if @monkey.constants(false).include?(const_name)
236
+ if @monkey.const_defined?(const_name, false)
145
237
  raise(NewConstantAlreadyExistsError.new(@monkey_patcher_name, monkey: @monkey, constant: const_name))
146
238
  end
147
239
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MonkeyBars
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/monkey_bars.rb CHANGED
@@ -31,6 +31,10 @@ module MonkeyBars
31
31
  alias_method :🐵, :patch
32
32
 
33
33
  def prepare_for_patching(monkey, version:, version_check:, patch_immediately: false, &block)
34
+ if @prepare_for_patching_performed
35
+ raise(PrepareForPatchingAlreadyPerformedError.new(@monkey_patcher_name))
36
+ end
37
+
34
38
  @monkey = monkey
35
39
  @version = version
36
40
  @version_check_block = version_check
@@ -38,6 +42,8 @@ module MonkeyBars
38
42
  yield if block_given?
39
43
 
40
44
  patch! if patch_immediately
45
+
46
+ @prepare_for_patching_performed = true
41
47
  end
42
48
 
43
49
  def patch!
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monkey_bars
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrés Rojas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-10 00:00:00.000000000 Z
11
+ date: 2026-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake