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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -13
  3. data/lib/state_machines/branch.rb +12 -13
  4. data/lib/state_machines/callback.rb +11 -12
  5. data/lib/state_machines/core.rb +0 -1
  6. data/lib/state_machines/error.rb +5 -4
  7. data/lib/state_machines/eval_helpers.rb +83 -45
  8. data/lib/state_machines/event.rb +23 -26
  9. data/lib/state_machines/event_collection.rb +4 -5
  10. data/lib/state_machines/extensions.rb +5 -5
  11. data/lib/state_machines/helper_module.rb +1 -1
  12. data/lib/state_machines/integrations/base.rb +1 -1
  13. data/lib/state_machines/integrations.rb +11 -14
  14. data/lib/state_machines/machine/action_hooks.rb +53 -0
  15. data/lib/state_machines/machine/callbacks.rb +59 -0
  16. data/lib/state_machines/machine/class_methods.rb +25 -11
  17. data/lib/state_machines/machine/configuration.rb +124 -0
  18. data/lib/state_machines/machine/event_methods.rb +59 -0
  19. data/lib/state_machines/machine/helper_generators.rb +125 -0
  20. data/lib/state_machines/machine/integration.rb +70 -0
  21. data/lib/state_machines/machine/parsing.rb +77 -0
  22. data/lib/state_machines/machine/rendering.rb +17 -0
  23. data/lib/state_machines/machine/scoping.rb +44 -0
  24. data/lib/state_machines/machine/state_methods.rb +101 -0
  25. data/lib/state_machines/machine/utilities.rb +85 -0
  26. data/lib/state_machines/machine/validation.rb +39 -0
  27. data/lib/state_machines/machine.rb +73 -617
  28. data/lib/state_machines/machine_collection.rb +18 -14
  29. data/lib/state_machines/macro_methods.rb +2 -2
  30. data/lib/state_machines/matcher.rb +6 -6
  31. data/lib/state_machines/matcher_helpers.rb +1 -1
  32. data/lib/state_machines/node_collection.rb +18 -17
  33. data/lib/state_machines/path.rb +2 -4
  34. data/lib/state_machines/path_collection.rb +2 -3
  35. data/lib/state_machines/state.rb +6 -5
  36. data/lib/state_machines/state_collection.rb +3 -3
  37. data/lib/state_machines/state_context.rb +6 -7
  38. data/lib/state_machines/stdio_renderer.rb +16 -16
  39. data/lib/state_machines/syntax_validator.rb +57 -0
  40. data/lib/state_machines/test_helper.rb +290 -27
  41. data/lib/state_machines/transition.rb +43 -41
  42. data/lib/state_machines/transition_collection.rb +22 -25
  43. data/lib/state_machines/version.rb +1 -1
  44. metadata +23 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5aa5d105c78cb53f15f42e8662dd7f8e0d0d5f8807b88dcbd8b4201f13718384
4
- data.tar.gz: d761cedbe052c5c8829626e0875f54dce064f7156162119b4a040594b439068c
3
+ metadata.gz: 5c732f34387da18f4dc1c3d78cad1fbb111cc88d06d9fe3edcda912adc5e655a
4
+ data.tar.gz: ef0079a89a1b0e4ddea45dd4a79e11de12740d4eb0c2aa98ad95b5ca7ae3eab8
5
5
  SHA512:
6
- metadata.gz: 37398c9ca5f2de7413dfb7a8a467984d0fc4d47f8367ea0747fec6d9c1eaca5a7c3439f37988c02dd8ce27622a9a9e54cf5cc0b2d84404f00f92a862c168bb23
7
- data.tar.gz: 159022534eb3c308bc2f2c8e960f111053631d3e10adafc9ae8156c95a26165f48690c682229a577bdfecf918b335ebe5b6d57e82e095c924585ad8d11b9d1ee
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 a `TestHelper` module with assertion methods to make testing state machines easier and more expressive.
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
- Include the test helper in your test class:
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
- assert_state vehicle, :state, :parked
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
- assert_state vehicle, :state, :parked
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
- assert_state vehicle, :state, :parked
296
- assert_can_transition vehicle, :ignite
297
- assert_cannot_transition vehicle, :shift_up
298
- assert_transition vehicle, :ignite, :state, :idling
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 = {}) #:nodoc:
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 - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
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
- [:from, :to].each { |option| @known_states |= state_requirement[option].values }
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
- if (match = match_query(query)) && matches_conditions?(object, query)
122
- match
123
- end
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
- protected
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
- [:from, :to].all? { |option| matches_requirement?(query, option, state_requirement[option]) }
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
- Array(if_condition).all? { |condition| evaluate_method(object, condition) } &&
186
- !Array(unless_condition).any? { |condition| evaluate_method(object, condition) }
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 [:before, :after, :around, :failure].include?(type)
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 = {}, *args, &block)
159
+ def call(object, context = {}, *, &)
160
160
  if @branch.matches?(object, context)
161
- run_methods(object, context, 0, *args, &block)
161
+ run_methods(object, context, 0, *, &)
162
162
  true
163
163
  else
164
164
  false
165
165
  end
166
166
  end
167
167
 
168
- private
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 current_method
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
- else
191
- yield if block_given?
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 = lambda { |object, *args| object.instance_exec(*args, &block) }
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
@@ -24,7 +24,6 @@ require 'state_machines/transition_collection'
24
24
  require 'state_machines/branch'
25
25
 
26
26
  require 'state_machines/helper_module'
27
- require 'state_machines/state'
28
27
  require 'state_machines/callback'
29
28
  require 'state_machines/node_collection'
30
29
 
@@ -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) #:nodoc:
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) #:nodoc:
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) #:nodoc:
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) #:nodoc:
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, **kwargs, &block)
57
+ def evaluate_method(object, method, *args, **, &block)
56
58
  case method
57
- when Symbol
58
- klass = (class << object; self; end)
59
- args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
60
- object.send(method, *args, **kwargs, &block)
61
- when Proc
62
- args.unshift(object)
63
- arity = method.arity
64
- # Handle blocks for Procs
65
- if block_given? && arity != 0
66
- if [1, 2].include?(arity)
67
- # Force the block to be either the only argument or the 2nd one
68
- # after the object (may mean additional arguments get discarded)
69
- args = args[0, arity - 1] + [block]
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
- # These method types are only called with 0, 1, or n arguments
76
- args = args[0, arity] if [0, 1].include?(arity)
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, **kwargs)
82
+ method.call(*args, **)
83
+
84
+ when Method
85
+ args.unshift(object)
86
+ arity = method.arity
81
87
 
82
- when Method
83
- args.unshift(object)
84
- arity = method.arity
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
- # Methods handle blocks via &block, not as arguments
87
- # Only limit arguments if necessary based on arity
88
- args = args[0, arity] if [0, 1].include?(arity)
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
- # Call the Method with the arguments and pass the block
91
- method.call(*args, **kwargs, &block)
92
- when String
93
- if block_given?
94
- if StateMachines::Transition.pause_supported?
95
- eval(method, object.instance_eval { binding }, &block)
96
- else
97
- # Support for JRuby and Truffle Ruby, which don't support binding blocks
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
- RUBY
104
- result = object.__temp_eval_method__(*args, &block)
105
- eigen.send(:remove_method, :__temp_eval_method__)
106
- result
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
- raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
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