monkey_bars 0.1.2 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d42e4b3ee72dbd54a294b38ad21d21a43a08c3c941b0a9dab17bcdb4508f227f
4
- data.tar.gz: 47f08f29eb06feb4b73d70679aef3769d4cab20ee3d32b08561522d869e27feb
3
+ metadata.gz: b35082ce948e0728f731d785776d4ac9bf43cffe2cde650caf8c4433ef7cf7ff
4
+ data.tar.gz: bac9b0bf1d3da1ceb52a167ad0289192d66e9a815bf05dda126a7d62f771aa4a
5
5
  SHA512:
6
- metadata.gz: 0fceae1976fc70683a2aa5633ed612c5b207ddefe66d01666fee6950130d888070bcdfe3cd47af0c3be24cea92ef8167cfeb67ae8d7379eb1b9e0961555fc3bb
7
- data.tar.gz: e6dff3e7bc72d25a1fec7b903b022aad32c5440c1228b6c8c9409158aee9523a2fff23c41691a31b2e0e95c1f3cb48937df92e088f42b0aab124433d21a0b289
6
+ metadata.gz: f4e70ba31cae555f6ad5766602f9d21fdbf04b79614d338b28ea6f75c6f6ce65f1760d45d0de6c6f35d8e49a66144757cbbc396b5a18dc0b4ccd7bdc31ab7ea8
7
+ data.tar.gz: c6cff1ac6df1ba73080973a262a7812ff104ec1e93b5bcfbebbce7d1a1727ce20558d11000dc402bf42765f3fbd7fa829f8ce9a34d671c83fe9dd38470ffcd45
data/README.md CHANGED
@@ -96,6 +96,30 @@ You can call any of these helpers multiple times; MonkeyBars will combine them.
96
96
  This is helpful when you want to ignore some arity check errors (see below) for
97
97
  some methods but not others.
98
98
 
99
+ When patching existing methods, visibility must match the target method exactly.
100
+ If the target is public, keep the patch method public. If the target is
101
+ protected/private, mark the patch method as protected/private in the block.
102
+
103
+ ```ruby
104
+ patch_instance_methods do
105
+ private
106
+
107
+ def internal_token
108
+ "#{super}-patched"
109
+ end
110
+ end
111
+ ```
112
+
113
+ ```ruby
114
+ patch_instance_methods do
115
+ def internal_token(*args)
116
+ super(*args)
117
+ end
118
+
119
+ protected :internal_token
120
+ end
121
+ ```
122
+
99
123
  ### Method arity checks
100
124
 
101
125
  When patching existing methods, arity must match by default. You can opt out:
@@ -209,6 +233,10 @@ MonkeyBars raises specific errors to keep patches safe and explicit:
209
233
  - `MonkeyBars::NoPatchableClassMethodFoundError`
210
234
  - `MonkeyBars::MismatchedInstanceMethodArityError`
211
235
  - `MonkeyBars::MismatchedClassMethodArityError`
236
+ - `MonkeyBars::PatchableInstanceMethodIsPrivateError`
237
+ - `MonkeyBars::PatchableInstanceMethodIsNotPrivateError`
238
+ - `MonkeyBars::PatchableClassMethodIsPrivateError`
239
+ - `MonkeyBars::PatchableClassMethodIsNotPrivateError`
212
240
  - `MonkeyBars::NewInstanceMethodAlreadyExistsError`
213
241
  - `MonkeyBars::NewClassMethodAlreadyExistsError`
214
242
  - `MonkeyBars::PatchConstantNotFoundError`
data/docs/llm-usage.md CHANGED
@@ -86,6 +86,14 @@ Overrides existing instance methods via `prepend`.
86
86
  - Error: `MonkeyBars::NoPatchableInstanceMethodFoundError` if method does not exist.
87
87
  - Error: `MonkeyBars::MismatchedInstanceMethodArityError` when arity differs,
88
88
  unless `ignore_arity_errors: true` is set.
89
+ - Error: `MonkeyBars::PatchableInstanceMethodIsPrivateError` when the target
90
+ method is private but the patch method is defined as public/protected.
91
+ - Error: `MonkeyBars::PatchableInstanceMethodIsNotPrivateError` when the target
92
+ method is public/protected but the patch method is marked `private`.
93
+ - Error: `MonkeyBars::PatchableInstanceMethodIsProtectedError` when the target
94
+ method is protected but the patch method is defined as public/private.
95
+ - Error: `MonkeyBars::PatchableInstanceMethodIsNotProtectedError` when the target
96
+ method is public/private but the patch method is marked `protected`.
89
97
  - `include_super_super: true` makes `#super_super` available for these methods.
90
98
 
91
99
  #### `new_class_methods(&block)`
@@ -101,6 +109,14 @@ Overrides existing class methods via `singleton_class.prepend`.
101
109
  - Error: `MonkeyBars::NoPatchableClassMethodFoundError` if method does not exist.
102
110
  - Error: `MonkeyBars::MismatchedClassMethodArityError` when arity differs,
103
111
  unless `ignore_arity_errors: true` is set.
112
+ - Error: `MonkeyBars::PatchableClassMethodIsPrivateError` when the target class
113
+ method is private but the patch method is defined as public/protected.
114
+ - Error: `MonkeyBars::PatchableClassMethodIsNotPrivateError` when the target
115
+ class method is public/protected but the patch method is marked `private`.
116
+ - Error: `MonkeyBars::PatchableClassMethodIsProtectedError` when the target class
117
+ method is protected but the patch method is defined as public/private.
118
+ - Error: `MonkeyBars::PatchableClassMethodIsNotProtectedError` when the target
119
+ class method is public/private but the patch method is marked `protected`.
104
120
  - `include_super_super: true` makes `#super_super` available for these methods.
105
121
 
106
122
  #### `patch_constants(&block)`
@@ -140,6 +156,24 @@ class FixPatch
140
156
  end
141
157
  ```
142
158
 
159
+ ### Patch a protected method
160
+
161
+ ```ruby
162
+ class ProtectedPatch
163
+ extend MonkeyBars
164
+
165
+ patch(SomeLibrary, version: "1.0.0", version_check: -> { SomeLibrary::VERSION }) do
166
+ patch_instance_methods do
167
+ def internal_token(value)
168
+ super(value) + "-patched"
169
+ end
170
+
171
+ protected :internal_token
172
+ end
173
+ end
174
+ end
175
+ ```
176
+
143
177
  ### Add new class method and constant
144
178
 
145
179
  ```ruby
@@ -200,8 +234,31 @@ Use this section when the patch fails. The message usually suggests the fix.
200
234
  Update the version string or the version check.
201
235
  - `MonkeyBars::NoPatchableInstanceMethodFoundError`: method does not exist.
202
236
  Move it to `new_instance_methods` or fix the method name.
237
+ - `MonkeyBars::PatchableInstanceMethodIsPrivateError`: target method is private
238
+ but patch method is public/protected. Mark the patch method as `private`.
239
+ - `MonkeyBars::PatchableInstanceMethodIsNotPrivateError`: target method is
240
+ public/protected but patch method is private. Remove `private` in the patch
241
+ block or patch a private target.
242
+ - `MonkeyBars::PatchableInstanceMethodIsProtectedError`: target method is
243
+ protected but patch method is public/private. Mark the patch method as
244
+ `protected`.
245
+ - `MonkeyBars::PatchableInstanceMethodIsNotProtectedError`: target method is
246
+ public/private but patch method is protected. Remove `protected` in the patch
247
+ block or patch a protected target.
203
248
  - `MonkeyBars::NoPatchableClassMethodFoundError`: method does not exist.
204
249
  Move it to `new_class_methods` or fix the method name.
250
+ - `MonkeyBars::PatchableClassMethodIsPrivateError`: target class method is
251
+ private but patch method is public/protected. Mark the patch method as
252
+ `private`.
253
+ - `MonkeyBars::PatchableClassMethodIsNotPrivateError`: target class method is
254
+ public/protected but patch method is private. Remove `private` in the patch
255
+ block or patch a private target.
256
+ - `MonkeyBars::PatchableClassMethodIsProtectedError`: target class method is
257
+ protected but patch method is public/private. Mark the patch method as
258
+ `protected`.
259
+ - `MonkeyBars::PatchableClassMethodIsNotProtectedError`: target class method is
260
+ public/private but patch method is protected. Remove `protected` in the patch
261
+ block or patch a protected target.
205
262
  - `MonkeyBars::MismatchedInstanceMethodArityError`: arity differs.
206
263
  Match the signature or set `ignore_arity_errors: true`.
207
264
  - `MonkeyBars::MismatchedClassMethodArityError`: arity differs.
@@ -223,6 +280,8 @@ Use this section when the patch fails. The message usually suggests the fix.
223
280
  - Prefer `patch(...)` for immediate application unless delayed patching is required.
224
281
  - Use `patch_*` helpers for existing methods/constants and `new_*` for new ones.
225
282
  - Keep patched method arity identical to the original unless explicitly allowed.
283
+ - Keep patched method visibility aligned exactly with the target
284
+ (`public`/`protected`/`private`).
226
285
  - Add targeted tests around the patched behavior; avoid testing internal details.
227
286
 
228
287
  ## 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")
@@ -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_instance_methods.include?(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_instance_methods.include?(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)
@@ -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.0"
5
5
  end
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.0
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-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake