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 +4 -4
- data/README.md +28 -0
- data/docs/llm-usage.md +59 -0
- data/lib/monkey_bars/errors.rb +48 -0
- data/lib/monkey_bars/validation.rb +92 -0
- data/lib/monkey_bars/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b35082ce948e0728f731d785776d4ac9bf43cffe2cde650caf8c4433ef7cf7ff
|
|
4
|
+
data.tar.gz: bac9b0bf1d3da1ceb52a167ad0289192d66e9a815bf05dda126a7d62f771aa4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/monkey_bars/errors.rb
CHANGED
|
@@ -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)
|
data/lib/monkey_bars/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-02-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|