state_machines 0.20.0 → 0.30.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 +124 -13
- data/lib/state_machines/branch.rb +12 -13
- data/lib/state_machines/callback.rb +11 -12
- data/lib/state_machines/core.rb +0 -1
- data/lib/state_machines/error.rb +5 -4
- data/lib/state_machines/eval_helpers.rb +83 -45
- data/lib/state_machines/event.rb +23 -26
- data/lib/state_machines/event_collection.rb +4 -5
- data/lib/state_machines/extensions.rb +5 -5
- data/lib/state_machines/helper_module.rb +1 -1
- data/lib/state_machines/integrations/base.rb +1 -1
- data/lib/state_machines/integrations.rb +11 -14
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +25 -11
- data/lib/state_machines/machine/configuration.rb +124 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +73 -617
- data/lib/state_machines/machine_collection.rb +18 -14
- data/lib/state_machines/macro_methods.rb +2 -2
- data/lib/state_machines/matcher.rb +6 -6
- data/lib/state_machines/matcher_helpers.rb +1 -1
- data/lib/state_machines/node_collection.rb +18 -17
- data/lib/state_machines/path.rb +2 -4
- data/lib/state_machines/path_collection.rb +2 -3
- data/lib/state_machines/state.rb +6 -5
- data/lib/state_machines/state_collection.rb +3 -3
- data/lib/state_machines/state_context.rb +6 -7
- data/lib/state_machines/stdio_renderer.rb +16 -16
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +290 -27
- data/lib/state_machines/transition.rb +43 -41
- data/lib/state_machines/transition_collection.rb +22 -25
- data/lib/state_machines/version.rb +1 -1
- metadata +23 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c732f34387da18f4dc1c3d78cad1fbb111cc88d06d9fe3edcda912adc5e655a
|
4
|
+
data.tar.gz: ef0079a89a1b0e4ddea45dd4a79e11de12740d4eb0c2aa98ad95b5ca7ae3eab8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d71a5bd20b0de0b19e4684b4251954d0cf648e5454b57af96555372bbdee59a9ac855bb27d182e018f87ffa0525b5cd3daff5c3ce53c483f3dec4954d076a331
|
7
|
+
data.tar.gz: 179e0cb6c2a31d14c4078820d254ce35e26d9b5aaa2646e4766b842127411cf49e5ad697d0a764ba79a0036bce5dc288ef113b8bef57c3cac64de8816a23f49b
|
data/README.md
CHANGED
@@ -42,7 +42,7 @@ class Vehicle
|
|
42
42
|
|
43
43
|
state_machine :state, initial: :parked do
|
44
44
|
before_transition parked: any - :parked, do: :put_on_seatbelt
|
45
|
-
|
45
|
+
|
46
46
|
after_transition on: :crash, do: :tow
|
47
47
|
after_transition on: :repair, do: :fix
|
48
48
|
after_transition any => :parked do |vehicle, transition|
|
@@ -256,30 +256,36 @@ vehicle.state_name # => :parked
|
|
256
256
|
|
257
257
|
## Testing
|
258
258
|
|
259
|
-
State Machines provides
|
259
|
+
State Machines provides an optional `TestHelper` module with assertion methods to make testing state machines easier and more expressive.
|
260
|
+
|
261
|
+
**Note: TestHelper is not required by default** - you must explicitly require it in your test files.
|
260
262
|
|
261
263
|
### Setup
|
262
264
|
|
263
|
-
|
265
|
+
First, require the test helper module, then include it in your test class:
|
264
266
|
|
265
267
|
```ruby
|
266
268
|
# For Minitest
|
269
|
+
require 'state_machines/test_helper'
|
270
|
+
|
267
271
|
class VehicleTest < Minitest::Test
|
268
272
|
include StateMachines::TestHelper
|
269
|
-
|
273
|
+
|
270
274
|
def test_initial_state
|
271
275
|
vehicle = Vehicle.new
|
272
|
-
|
276
|
+
assert_sm_state vehicle, :parked
|
273
277
|
end
|
274
278
|
end
|
275
279
|
|
276
|
-
# For RSpec
|
280
|
+
# For RSpec
|
281
|
+
require 'state_machines/test_helper'
|
282
|
+
|
277
283
|
RSpec.describe Vehicle do
|
278
284
|
include StateMachines::TestHelper
|
279
|
-
|
285
|
+
|
280
286
|
it "starts in parked state" do
|
281
287
|
vehicle = Vehicle.new
|
282
|
-
|
288
|
+
assert_sm_state vehicle, :parked
|
283
289
|
end
|
284
290
|
end
|
285
291
|
```
|
@@ -292,10 +298,17 @@ The TestHelper provides both basic assertions and comprehensive state machine-sp
|
|
292
298
|
|
293
299
|
```ruby
|
294
300
|
vehicle = Vehicle.new
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
301
|
+
|
302
|
+
# New standardized API (all methods prefixed with assert_sm_)
|
303
|
+
assert_sm_state(vehicle, :parked) # Uses default :state machine
|
304
|
+
assert_sm_state(vehicle, :parked, machine_name: :status) # Specify machine explicitly
|
305
|
+
assert_sm_can_transition(vehicle, :ignite) # Test transition capability
|
306
|
+
assert_sm_cannot_transition(vehicle, :shift_up) # Test transition restriction
|
307
|
+
assert_sm_transition(vehicle, :ignite, :idling) # Test actual transition
|
308
|
+
|
309
|
+
# Multi-FSM examples
|
310
|
+
assert_sm_state(vehicle, :inactive, machine_name: :insurance_state) # Test insurance state
|
311
|
+
assert_sm_can_transition(vehicle, :buy_insurance, machine_name: :insurance_state)
|
299
312
|
```
|
300
313
|
|
301
314
|
#### Extended State Machine Assertions
|
@@ -308,7 +321,7 @@ vehicle = Vehicle.new
|
|
308
321
|
assert_sm_states_list machine, [:parked, :idling, :stalled]
|
309
322
|
assert_sm_initial_state machine, :parked
|
310
323
|
|
311
|
-
# Event behavior
|
324
|
+
# Event behavior
|
312
325
|
assert_sm_event_triggers vehicle, :ignite
|
313
326
|
refute_sm_event_triggers vehicle, :shift_up
|
314
327
|
assert_sm_event_raises_error vehicle, :invalid_event, StateMachines::InvalidTransition
|
@@ -317,8 +330,106 @@ assert_sm_event_raises_error vehicle, :invalid_event, StateMachines::InvalidTran
|
|
317
330
|
assert_sm_state_persisted record, expected: :active
|
318
331
|
```
|
319
332
|
|
333
|
+
#### Indirect Event Testing
|
334
|
+
|
335
|
+
Test that methods trigger state machine events indirectly:
|
336
|
+
|
337
|
+
```ruby
|
338
|
+
# Minitest style
|
339
|
+
vehicle = Vehicle.new
|
340
|
+
vehicle.ignite # Put in idling state
|
341
|
+
|
342
|
+
# Test that a custom method triggers a specific event
|
343
|
+
assert_sm_triggers_event(vehicle, :crash) do
|
344
|
+
vehicle.redline # Custom method that calls crash! internally
|
345
|
+
end
|
346
|
+
|
347
|
+
# Test multiple events
|
348
|
+
assert_sm_triggers_event(vehicle, [:crash, :emergency]) do
|
349
|
+
vehicle.emergency_stop
|
350
|
+
end
|
351
|
+
|
352
|
+
# Test on specific state machine (multi-FSM support)
|
353
|
+
assert_sm_triggers_event(vehicle, :disable, machine_name: :alarm) do
|
354
|
+
vehicle.turn_off_alarm
|
355
|
+
end
|
356
|
+
```
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
# RSpec style (coming soon with proper matcher support)
|
360
|
+
RSpec.describe Vehicle do
|
361
|
+
include StateMachines::TestHelper
|
362
|
+
|
363
|
+
it "triggers crash when redlining" do
|
364
|
+
vehicle = Vehicle.new
|
365
|
+
vehicle.ignite
|
366
|
+
|
367
|
+
expect_to_trigger_event(vehicle, :crash) do
|
368
|
+
vehicle.redline
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
#### Callback Definition Testing (TDD Support)
|
375
|
+
|
376
|
+
Verify that callbacks are properly defined in your state machine:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
# Test after_transition callbacks
|
380
|
+
assert_after_transition(Vehicle, on: :crash, do: :tow)
|
381
|
+
assert_after_transition(Vehicle, from: :stalled, to: :parked, do: :log_repair)
|
382
|
+
|
383
|
+
# Test before_transition callbacks
|
384
|
+
assert_before_transition(Vehicle, from: :parked, do: :put_on_seatbelt)
|
385
|
+
assert_before_transition(Vehicle, on: :ignite, if: :seatbelt_on?)
|
386
|
+
|
387
|
+
# Works with machine instances too
|
388
|
+
machine = Vehicle.state_machine(:state)
|
389
|
+
assert_after_transition(machine, on: :crash, do: :tow)
|
390
|
+
```
|
391
|
+
|
392
|
+
#### Multiple State Machine Support
|
393
|
+
|
394
|
+
The TestHelper fully supports objects with multiple state machines:
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
# Example: StarfleetShip with 3 state machines
|
398
|
+
ship = StarfleetShip.new
|
399
|
+
|
400
|
+
# Test states on different machines
|
401
|
+
assert_sm_state(ship, :docked, machine_name: :status) # Main ship status
|
402
|
+
assert_sm_state(ship, :down, machine_name: :shields) # Shield system
|
403
|
+
assert_sm_state(ship, :standby, machine_name: :weapons) # Weapons system
|
404
|
+
|
405
|
+
# Test transitions on specific machines
|
406
|
+
assert_sm_transition(ship, :undock, :impulse, machine_name: :status)
|
407
|
+
assert_sm_transition(ship, :raise_shields, :up, machine_name: :shields)
|
408
|
+
assert_sm_transition(ship, :arm_weapons, :armed, machine_name: :weapons)
|
409
|
+
|
410
|
+
# Test event triggering across multiple machines
|
411
|
+
assert_sm_triggers_event(ship, :red_alert, machine_name: :status) do
|
412
|
+
ship.engage_combat_mode # Custom method affecting multiple systems
|
413
|
+
end
|
414
|
+
|
415
|
+
assert_sm_triggers_event(ship, :raise_shields, machine_name: :shields) do
|
416
|
+
ship.engage_combat_mode
|
417
|
+
end
|
418
|
+
|
419
|
+
# Test callback definitions on specific machines
|
420
|
+
shields_machine = StarfleetShip.state_machine(:shields)
|
421
|
+
assert_before_transition(shields_machine, from: :down, to: :up, do: :power_up_shields)
|
422
|
+
|
423
|
+
# Test persistence across multiple machines
|
424
|
+
assert_sm_state_persisted(ship, "impulse", :status)
|
425
|
+
assert_sm_state_persisted(ship, "up", :shields)
|
426
|
+
assert_sm_state_persisted(ship, "armed", :weapons)
|
427
|
+
```
|
428
|
+
|
320
429
|
The test helper works with both Minitest and RSpec, automatically detecting your testing framework.
|
321
430
|
|
431
|
+
**Note:** All methods use consistent keyword arguments with `machine_name:` as the last parameter, making the API intuitive and Grep-friendly.
|
432
|
+
|
322
433
|
## Additional Topics
|
323
434
|
|
324
435
|
### Explicit vs. Implicit Event Transitions
|
@@ -8,7 +8,6 @@ module StateMachines
|
|
8
8
|
# state of the transition match, in addition to if/unless conditionals for
|
9
9
|
# an object's state.
|
10
10
|
class Branch
|
11
|
-
|
12
11
|
include EvalHelpers
|
13
12
|
|
14
13
|
# The condition that must be met on an object
|
@@ -31,7 +30,7 @@ module StateMachines
|
|
31
30
|
attr_reader :known_states
|
32
31
|
|
33
32
|
# Creates a new branch
|
34
|
-
def initialize(options = {})
|
33
|
+
def initialize(options = {}) # :nodoc:
|
35
34
|
# Build conditionals
|
36
35
|
@if_condition = options.delete(:if)
|
37
36
|
@unless_condition = options.delete(:unless)
|
@@ -39,9 +38,9 @@ module StateMachines
|
|
39
38
|
# Build event requirement
|
40
39
|
@event_requirement = build_matcher(options, :on, :except_on)
|
41
40
|
|
42
|
-
if (options.keys - [
|
41
|
+
if (options.keys - %i[from to on except_from except_to except_on]).empty?
|
43
42
|
# Explicit from/to requirements specified
|
44
|
-
@state_requirements = [{from: build_matcher(options, :from, :except_from), to: build_matcher(options, :to, :except_to)}]
|
43
|
+
@state_requirements = [{ from: build_matcher(options, :from, :except_from), to: build_matcher(options, :to, :except_to) }]
|
45
44
|
else
|
46
45
|
# Separate out the event requirement
|
47
46
|
options.delete(:on)
|
@@ -51,7 +50,7 @@ module StateMachines
|
|
51
50
|
@state_requirements = options.collect do |from, to|
|
52
51
|
from = WhitelistMatcher.new(from) unless from.is_a?(Matcher)
|
53
52
|
to = WhitelistMatcher.new(to) unless to.is_a?(Matcher)
|
54
|
-
{from: from, to: to}
|
53
|
+
{ from: from, to: to }
|
55
54
|
end
|
56
55
|
end
|
57
56
|
|
@@ -59,7 +58,7 @@ module StateMachines
|
|
59
58
|
# on the priority in which tracked states should be added.
|
60
59
|
@known_states = []
|
61
60
|
@state_requirements.each do |state_requirement|
|
62
|
-
[
|
61
|
+
%i[from to].each { |option| @known_states |= state_requirement[option].values }
|
63
62
|
end
|
64
63
|
end
|
65
64
|
|
@@ -118,16 +117,16 @@ module StateMachines
|
|
118
117
|
def match(object, query = {})
|
119
118
|
StateMachines::OptionsValidator.assert_valid_keys!(query, :from, :to, :on, :guard)
|
120
119
|
|
121
|
-
|
122
|
-
|
123
|
-
|
120
|
+
return unless (match = match_query(query)) && matches_conditions?(object, query)
|
121
|
+
|
122
|
+
match
|
124
123
|
end
|
125
124
|
|
126
125
|
def draw(graph, event, valid_states, io = $stdout)
|
127
126
|
machine.renderer.draw_branch(self, graph, event, valid_states, io)
|
128
127
|
end
|
129
128
|
|
130
|
-
|
129
|
+
protected
|
131
130
|
|
132
131
|
# Builds a matcher strategy to use for the given options. If neither a
|
133
132
|
# whitelist nor a blacklist option is specified, then an AllMatcher is
|
@@ -168,7 +167,7 @@ module StateMachines
|
|
168
167
|
# matching requirement is found, then it is returned.
|
169
168
|
def match_states(query)
|
170
169
|
state_requirements.detect do |state_requirement|
|
171
|
-
[
|
170
|
+
%i[from to].all? { |option| matches_requirement?(query, option, state_requirement[option]) }
|
172
171
|
end
|
173
172
|
end
|
174
173
|
|
@@ -182,8 +181,8 @@ module StateMachines
|
|
182
181
|
# given object
|
183
182
|
def matches_conditions?(object, query)
|
184
183
|
query[:guard] == false ||
|
185
|
-
|
186
|
-
|
184
|
+
(Array(if_condition).all? { |condition| evaluate_method(object, condition) } &&
|
185
|
+
!Array(unless_condition).any? { |condition| evaluate_method(object, condition) })
|
187
186
|
end
|
188
187
|
end
|
189
188
|
end
|
@@ -124,7 +124,7 @@ module StateMachines
|
|
124
124
|
# callback can be found in their attribute definitions.
|
125
125
|
def initialize(type, *args, &block)
|
126
126
|
@type = type
|
127
|
-
raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless [
|
127
|
+
raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless %i[before after around failure].include?(type)
|
128
128
|
|
129
129
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
130
130
|
@methods = args
|
@@ -132,7 +132,7 @@ module StateMachines
|
|
132
132
|
@methods << block if block_given?
|
133
133
|
raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any?
|
134
134
|
|
135
|
-
options = {bind_to_object: self.class.bind_to_object, terminator: self.class.terminator}.merge(options)
|
135
|
+
options = { bind_to_object: self.class.bind_to_object, terminator: self.class.terminator }.merge(options)
|
136
136
|
|
137
137
|
# Proxy lambda blocks so that they're bound to the object
|
138
138
|
bind_to_object = options.delete(:bind_to_object)
|
@@ -156,16 +156,16 @@ module StateMachines
|
|
156
156
|
#
|
157
157
|
# If a terminator has been configured and it matches the result from the
|
158
158
|
# evaluated method, then the callback chain should be halted.
|
159
|
-
def call(object, context = {},
|
159
|
+
def call(object, context = {}, *, &)
|
160
160
|
if @branch.matches?(object, context)
|
161
|
-
run_methods(object, context, 0,
|
161
|
+
run_methods(object, context, 0, *, &)
|
162
162
|
true
|
163
163
|
else
|
164
164
|
false
|
165
165
|
end
|
166
166
|
end
|
167
167
|
|
168
|
-
|
168
|
+
private
|
169
169
|
|
170
170
|
# Runs all of the methods configured for this callback.
|
171
171
|
#
|
@@ -179,7 +179,7 @@ module StateMachines
|
|
179
179
|
def run_methods(object, context = {}, index = 0, *args, &block)
|
180
180
|
if type == :around
|
181
181
|
current_method = @methods[index]
|
182
|
-
if
|
182
|
+
if current_method
|
183
183
|
yielded = false
|
184
184
|
evaluate_method(object, current_method, *args) do
|
185
185
|
yielded = true
|
@@ -187,8 +187,8 @@ module StateMachines
|
|
187
187
|
end
|
188
188
|
|
189
189
|
throw :halt unless yielded
|
190
|
-
|
191
|
-
yield
|
190
|
+
elsif block_given?
|
191
|
+
yield
|
192
192
|
end
|
193
193
|
else
|
194
194
|
@methods.each do |method|
|
@@ -206,13 +206,12 @@ module StateMachines
|
|
206
206
|
arity += 1 if arity >= 0 # Make sure the object gets passed
|
207
207
|
arity += 1 if arity == 1 && type == :around # Make sure the block gets passed
|
208
208
|
|
209
|
-
method =
|
210
|
-
|
209
|
+
method = ->(object, *args) { object.instance_exec(*args, &block) }
|
211
210
|
|
212
211
|
# Proxy arity to the original block
|
213
212
|
(
|
214
|
-
class << method
|
215
|
-
self
|
213
|
+
class << method
|
214
|
+
self
|
216
215
|
end).class_eval do
|
217
216
|
define_method(:arity) { arity }
|
218
217
|
end
|
data/lib/state_machines/core.rb
CHANGED
data/lib/state_machines/error.rb
CHANGED
@@ -6,7 +6,7 @@ module StateMachines
|
|
6
6
|
# The object that failed
|
7
7
|
attr_reader :object
|
8
8
|
|
9
|
-
def initialize(object, message = nil)
|
9
|
+
def initialize(object, message = nil) # :nodoc:
|
10
10
|
@object = object
|
11
11
|
|
12
12
|
super(message)
|
@@ -49,12 +49,13 @@ module StateMachines
|
|
49
49
|
# The event that was attempted to be run
|
50
50
|
attr_reader :event
|
51
51
|
|
52
|
-
def initialize(object, event_name)
|
52
|
+
def initialize(object, event_name) # :nodoc:
|
53
53
|
@event = event_name
|
54
54
|
|
55
55
|
super(object, "#{event.inspect} is an unknown state machine event")
|
56
56
|
end
|
57
57
|
end
|
58
|
+
|
58
59
|
# An invalid transition was attempted
|
59
60
|
class InvalidTransition < Error
|
60
61
|
# The machine attempting to be transitioned
|
@@ -63,7 +64,7 @@ module StateMachines
|
|
63
64
|
# The current state value for the machine
|
64
65
|
attr_reader :from
|
65
66
|
|
66
|
-
def initialize(object, machine, event)
|
67
|
+
def initialize(object, machine, event) # :nodoc:
|
67
68
|
@machine = machine
|
68
69
|
@from_state = machine.states.match!(object)
|
69
70
|
@from = machine.read(object, :state)
|
@@ -101,7 +102,7 @@ module StateMachines
|
|
101
102
|
# The set of events that failed the transition(s)
|
102
103
|
attr_reader :events
|
103
104
|
|
104
|
-
def initialize(object, events)
|
105
|
+
def initialize(object, events) # :nodoc:
|
105
106
|
@events = events
|
106
107
|
|
107
108
|
super(object, "Cannot run events in parallel: #{events * ', '}")
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'syntax_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# Provides a set of helper methods for evaluating methods within the context
|
5
7
|
# of an object.
|
@@ -52,64 +54,100 @@ module StateMachines
|
|
52
54
|
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
53
55
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
54
56
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
|
55
|
-
def evaluate_method(object, method, *args,
|
57
|
+
def evaluate_method(object, method, *args, **, &block)
|
56
58
|
case method
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
else
|
71
|
-
# Tack the block to the end of the args
|
72
|
-
args << block
|
73
|
-
end
|
59
|
+
when Symbol
|
60
|
+
klass = (class << object; self; end)
|
61
|
+
args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
|
62
|
+
object.send(method, *args, **, &block)
|
63
|
+
when Proc
|
64
|
+
args.unshift(object)
|
65
|
+
arity = method.arity
|
66
|
+
# Handle blocks for Procs
|
67
|
+
if block_given? && arity != 0
|
68
|
+
if [1, 2].include?(arity)
|
69
|
+
# Force the block to be either the only argument or the second one
|
70
|
+
# after the object (may mean additional arguments get discarded)
|
71
|
+
args = args[0, arity - 1] + [block]
|
74
72
|
else
|
75
|
-
#
|
76
|
-
args
|
73
|
+
# insert the block to the end of the args
|
74
|
+
args << block
|
77
75
|
end
|
76
|
+
elsif [0, 1].include?(arity)
|
77
|
+
# These method types are only called with 0, 1, or n arguments
|
78
|
+
args = args[0, arity]
|
79
|
+
end
|
78
80
|
|
79
81
|
# Call the Proc with the arguments
|
80
|
-
method.call(*args, **
|
82
|
+
method.call(*args, **)
|
83
|
+
|
84
|
+
when Method
|
85
|
+
args.unshift(object)
|
86
|
+
arity = method.arity
|
81
87
|
|
82
|
-
|
83
|
-
|
84
|
-
|
88
|
+
# Methods handle blocks via &block, not as arguments
|
89
|
+
# Only limit arguments if necessary based on arity
|
90
|
+
args = args[0, arity] if [0, 1].include?(arity)
|
85
91
|
|
86
|
-
|
87
|
-
|
88
|
-
|
92
|
+
# Call the Method with the arguments and pass the block
|
93
|
+
method.call(*args, **, &block)
|
94
|
+
when String
|
95
|
+
# Input validation for string evaluation
|
96
|
+
validate_eval_string(method)
|
89
97
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
eigen = class << object; self; end
|
99
|
-
eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
98
|
+
if block_given?
|
99
|
+
if StateMachines::Transition.pause_supported?
|
100
|
+
eval(method, object.instance_eval { binding }, &block)
|
101
|
+
else
|
102
|
+
# Support for JRuby and Truffle Ruby, which don't support binding blocks
|
103
|
+
# Need to check with @headius, if jruby 10 does now.
|
104
|
+
eigen = class << object; self; end
|
105
|
+
eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
100
106
|
def __temp_eval_method__(*args, &b)
|
101
107
|
#{method}
|
102
108
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
else
|
109
|
-
eval(method, object.instance_eval { binding })
|
109
|
+
RUBY
|
110
|
+
result = object.__temp_eval_method__(*args, &block)
|
111
|
+
eigen.send(:remove_method, :__temp_eval_method__)
|
112
|
+
result
|
110
113
|
end
|
111
114
|
else
|
112
|
-
|
115
|
+
eval(method, object.instance_eval { binding })
|
116
|
+
end
|
117
|
+
else
|
118
|
+
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Validates string input before eval to prevent code injection
|
125
|
+
# This is a basic safety check - not foolproof security
|
126
|
+
def validate_eval_string(method_string)
|
127
|
+
# Check for obviously dangerous patterns
|
128
|
+
dangerous_patterns = [
|
129
|
+
/`.*`/, # Backticks (shell execution)
|
130
|
+
/system\s*\(/, # System calls
|
131
|
+
/exec\s*\(/, # Exec calls
|
132
|
+
/eval\s*\(/, # Nested eval
|
133
|
+
/require\s+['"]/, # Require statements
|
134
|
+
/load\s+['"]/, # Load statements
|
135
|
+
/File\./, # File operations
|
136
|
+
/IO\./, # IO operations
|
137
|
+
/Dir\./, # Directory operations
|
138
|
+
/Kernel\./ # Kernel operations
|
139
|
+
]
|
140
|
+
|
141
|
+
dangerous_patterns.each do |pattern|
|
142
|
+
raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Basic syntax validation - but allow yield since it's valid in block context
|
146
|
+
begin
|
147
|
+
test_code = method_string.include?('yield') ? "def dummy_method; #{method_string}; end" : method_string
|
148
|
+
SyntaxValidator.validate!(test_code, '(eval)')
|
149
|
+
rescue SyntaxError => e
|
150
|
+
raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}"
|
113
151
|
end
|
114
152
|
end
|
115
153
|
end
|