state_machines 0.30.0 → 0.40.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.
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby Engine Compatibility Check
4
+ # The async gem requires native extensions and Fiber scheduler support
5
+ # which are not available on JRuby or TruffleRuby
6
+ if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'truffleruby'
7
+ raise LoadError, <<~ERROR
8
+ StateMachines::AsyncMode is not available on #{RUBY_ENGINE}.
9
+
10
+ The async gem requires native extensions (io-event) and Fiber scheduler support
11
+ which are not implemented in #{RUBY_ENGINE}. AsyncMode is only supported on:
12
+
13
+ • MRI Ruby (CRuby) 3.2+
14
+ • Other Ruby engines with full Fiber scheduler support
15
+
16
+ If you need async support on #{RUBY_ENGINE}, consider using:
17
+ • java.util.concurrent classes (JRuby)
18
+ • Native threading libraries for your platform
19
+ • Or stick with synchronous state machines
20
+ ERROR
21
+ end
22
+
23
+ # Load required gems with version constraints
24
+ gem 'async', '>= 2.25.0'
25
+ gem 'concurrent-ruby', '>= 1.3.5' # Security is not negotiable - enterprise-grade thread safety required
26
+
27
+ require 'async'
28
+ require 'concurrent-ruby'
29
+
30
+ # Load all async mode components
31
+ require_relative 'async_mode/thread_safe_state'
32
+ require_relative 'async_mode/async_events'
33
+ require_relative 'async_mode/async_event_extensions'
34
+ require_relative 'async_mode/async_machine'
35
+ require_relative 'async_mode/async_transition_collection'
36
+
37
+ module StateMachines
38
+ # AsyncMode provides asynchronous state machine capabilities using the async gem
39
+ # This module enables concurrent, thread-safe state operations for high-performance applications
40
+ #
41
+ # @example Basic usage
42
+ # class AutonomousDrone < StarfleetShip
43
+ # state_machine :teleporter_status, async: true do
44
+ # event :power_up do
45
+ # transition offline: :charging
46
+ # end
47
+ # end
48
+ # end
49
+ #
50
+ # drone = AutonomousDrone.new
51
+ # Async do
52
+ # result = drone.fire_event_async(:power_up) # => true
53
+ # task = drone.power_up_async! # => Async::Task
54
+ # end
55
+ #
56
+ module AsyncMode
57
+ # All components are loaded from separate files:
58
+ # - ThreadSafeState: Mutex-based thread safety
59
+ # - AsyncEvents: Async event firing methods
60
+ # - AsyncEventExtensions: Event method generation
61
+ # - AsyncMachine: Machine-level async capabilities
62
+ # - AsyncTransitionCollection: Concurrent transition execution
63
+ end
64
+ end
@@ -108,16 +108,18 @@ module StateMachines
108
108
  # * <tt>:guard</tt> - Whether to guard matches with the if/unless
109
109
  # conditionals defined for this branch. Default is true.
110
110
  #
111
+ # Event arguments are passed to guard conditions if they accept multiple parameters.
112
+ #
111
113
  # == Examples
112
114
  #
113
115
  # branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite)
114
116
  #
115
117
  # branch.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
116
118
  # branch.match(object, :on => :park) # => nil
117
- def match(object, query = {})
119
+ def match(object, query = {}, event_args = [])
118
120
  StateMachines::OptionsValidator.assert_valid_keys!(query, :from, :to, :on, :guard)
119
121
 
120
- return unless (match = match_query(query)) && matches_conditions?(object, query)
122
+ return unless (match = match_query(query)) && matches_conditions?(object, query, event_args)
121
123
 
122
124
  match
123
125
  end
@@ -178,11 +180,23 @@ module StateMachines
178
180
  end
179
181
 
180
182
  # Verifies that the conditionals for this branch evaluate to true for the
181
- # given object
182
- def matches_conditions?(object, query)
183
- query[:guard] == false ||
184
- (Array(if_condition).all? { |condition| evaluate_method(object, condition) } &&
185
- !Array(unless_condition).any? { |condition| evaluate_method(object, condition) })
183
+ # given object. Event arguments are passed to guards that accept multiple parameters.
184
+ def matches_conditions?(object, query, event_args = [])
185
+ case [query[:guard], if_condition, unless_condition]
186
+ in [false, _, _]
187
+ true
188
+ in [_, nil, nil]
189
+ true
190
+ in [_, if_conds, nil] if if_conds
191
+ Array(if_conds).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
192
+ in [_, nil, unless_conds] if unless_conds
193
+ Array(unless_conds).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
194
+ in [_, if_conds, unless_conds] if if_conds || unless_conds
195
+ Array(if_conds).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) } &&
196
+ Array(unless_conds).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
197
+ else
198
+ true
199
+ end
186
200
  end
187
201
  end
188
202
  end
@@ -177,7 +177,8 @@ module StateMachines
177
177
  # order. The callback will only halt if the resulting value from the
178
178
  # method passes the terminator.
179
179
  def run_methods(object, context = {}, index = 0, *args, &block)
180
- if type == :around
180
+ case type
181
+ when :around
181
182
  current_method = @methods[index]
182
183
  if current_method
183
184
  yielded = false
@@ -193,7 +194,7 @@ module StateMachines
193
194
  else
194
195
  @methods.each do |method|
195
196
  result = evaluate_method(object, method, *args)
196
- throw :halt if @terminator && @terminator.call(result)
197
+ throw :halt if @terminator&.call(result)
197
198
  end
198
199
  end
199
200
  end
@@ -47,6 +47,11 @@ module StateMachines
47
47
  # the method defines additional arguments other than the object context,
48
48
  # then all arguments are required.
49
49
  #
50
+ # For guard conditions in state machines, event arguments can be passed
51
+ # automatically based on the guard's arity:
52
+ # - Guards with arity 1 receive only the object (backward compatible)
53
+ # - Guards with arity -1 or > 1 receive object + event arguments
54
+ #
50
55
  # For example,
51
56
  #
52
57
  # person = Person.new('John Smith')
@@ -54,18 +59,30 @@ module StateMachines
54
59
  # evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
55
60
  # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
56
61
  # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
62
+ #
63
+ # With event arguments for guards:
64
+ #
65
+ # # Single parameter guard (backward compatible)
66
+ # guard = lambda {|obj| obj.valid? }
67
+ # evaluate_method_with_event_args(object, guard, [arg1, arg2]) # => calls guard.call(object)
68
+ #
69
+ # # Multi-parameter guard (receives event args)
70
+ # guard = lambda {|obj, *args| obj.valid? && args[0] == :force }
71
+ # evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object, :force)
57
72
  def evaluate_method(object, method, *args, **, &block)
58
73
  case method
59
- when Symbol
74
+ in Symbol => sym
60
75
  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
76
+ args = [] if (klass.method_defined?(sym) || klass.private_method_defined?(sym)) && object.method(sym).arity.zero?
77
+ object.send(sym, *args, **, &block)
78
+ in Proc => proc
64
79
  args.unshift(object)
65
- arity = method.arity
80
+ arity = proc.arity
66
81
  # Handle blocks for Procs
67
- if block_given? && arity != 0
68
- if [1, 2].include?(arity)
82
+ case [block_given?, arity]
83
+ in [true, arity] if arity != 0
84
+ case arity
85
+ in 1 | 2
69
86
  # Force the block to be either the only argument or the second one
70
87
  # after the object (may mean additional arguments get discarded)
71
88
  args = args[0, arity - 1] + [block]
@@ -73,52 +90,126 @@ module StateMachines
73
90
  # insert the block to the end of the args
74
91
  args << block
75
92
  end
76
- elsif [0, 1].include?(arity)
93
+ in [_, 0 | 1]
77
94
  # These method types are only called with 0, 1, or n arguments
78
95
  args = args[0, arity]
96
+ else
97
+ # No changes needed for other cases
79
98
  end
80
99
 
81
100
  # Call the Proc with the arguments
82
- method.call(*args, **)
101
+ proc.call(*args, **)
83
102
 
84
- when Method
103
+ in Method => meth
85
104
  args.unshift(object)
86
- arity = method.arity
105
+ arity = meth.arity
87
106
 
88
107
  # Methods handle blocks via &block, not as arguments
89
108
  # Only limit arguments if necessary based on arity
90
109
  args = args[0, arity] if [0, 1].include?(arity)
91
110
 
92
111
  # Call the Method with the arguments and pass the block
93
- method.call(*args, **, &block)
94
- when String
112
+ meth.call(*args, **, &block)
113
+ in String => str
95
114
  # Input validation for string evaluation
96
- validate_eval_string(method)
115
+ validate_eval_string(str)
97
116
 
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
106
- def __temp_eval_method__(*args, &b)
107
- #{method}
108
- end
109
- RUBY
110
- result = object.__temp_eval_method__(*args, &block)
111
- eigen.send(:remove_method, :__temp_eval_method__)
112
- result
113
- end
114
- else
115
- eval(method, object.instance_eval { binding })
117
+ case [block_given?, StateMachines::Transition.pause_supported?]
118
+ in [true, true]
119
+ eval(str, object.instance_eval { binding }, &block)
120
+ in [true, false]
121
+ # Support for JRuby and Truffle Ruby, which don't support binding blocks
122
+ # Need to check with @headius, if jruby 10 does now.
123
+ eigen = class << object; self; end
124
+ eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
125
+ def __temp_eval_method__(*args, &b)
126
+ #{str}
127
+ end
128
+ RUBY
129
+ result = object.__temp_eval_method__(*args, &block)
130
+ eigen.send(:remove_method, :__temp_eval_method__)
131
+ result
132
+ in [false, _]
133
+ eval(str, object.instance_eval { binding })
116
134
  end
117
135
  else
118
136
  raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
119
137
  end
120
138
  end
121
139
 
140
+ # Evaluates a guard method with support for event arguments passed to transitions.
141
+ # This method uses arity detection to determine whether to pass event arguments
142
+ # to the guard, ensuring backward compatibility.
143
+ #
144
+ # == Parameters
145
+ # * object - The object context to evaluate within
146
+ # * method - The guard method/proc to evaluate
147
+ # * event_args - Array of arguments passed to the event (optional)
148
+ #
149
+ # == Arity-based behavior
150
+ # * Arity 1: Only passes the object (backward compatible)
151
+ # * Arity -1 or > 1: Passes object + event arguments
152
+ #
153
+ # == Examples
154
+ #
155
+ # # Backward compatible single-parameter guard
156
+ # guard = lambda {|obj| obj.valid? }
157
+ # evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object)
158
+ #
159
+ # # New multi-parameter guard receiving event args
160
+ # guard = lambda {|obj, *args| obj.valid? && args[0] != :skip }
161
+ # evaluate_method_with_event_args(object, guard, [:skip]) # => calls guard.call(object, :skip)
162
+ def evaluate_method_with_event_args(object, method, event_args = [])
163
+ case method
164
+ in Symbol
165
+ # Symbol methods currently don't support event arguments
166
+ # This maintains backward compatibility
167
+ evaluate_method(object, method)
168
+ in Proc => proc
169
+ arity = proc.arity
170
+
171
+ # Arity-based decision for backward compatibility using pattern matching
172
+ case arity
173
+ in 0
174
+ proc.call
175
+ in 1
176
+ proc.call(object)
177
+ in -1
178
+ # Splat parameters: object + all event args
179
+ proc.call(object, *event_args)
180
+ in arity if arity > 1
181
+ # Explicit parameters: object + limited event args
182
+ args_needed = arity - 1 # Subtract 1 for the object parameter
183
+ proc.call(object, *event_args[0, args_needed])
184
+ else
185
+ # Negative arity other than -1 (unlikely but handle gracefully)
186
+ proc.call(object, *event_args)
187
+ end
188
+ in Method => meth
189
+ arity = meth.arity
190
+
191
+ case arity
192
+ in 0
193
+ meth.call
194
+ in 1
195
+ meth.call(object)
196
+ in -1
197
+ meth.call(object, *event_args)
198
+ in arity if arity > 1
199
+ args_needed = arity - 1
200
+ meth.call(object, *event_args[0, args_needed])
201
+ else
202
+ meth.call(object, *event_args)
203
+ end
204
+ in String
205
+ # String evaluation doesn't support event arguments for security
206
+ evaluate_method(object, method)
207
+ else
208
+ # Fall back to standard evaluation
209
+ evaluate_method(object, method)
210
+ end
211
+ end
212
+
122
213
  private
123
214
 
124
215
  # Validates string input before eval to prevent code injection
@@ -35,15 +35,17 @@ module StateMachines
35
35
  # * <tt>:human_name</tt> - The human-readable version of this event's name
36
36
  def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
37
37
  # Handle both old hash style and new kwargs style for backward compatibility
38
- if options.is_a?(Hash)
38
+ case options
39
+ in Hash
39
40
  # Old style: initialize(machine, name, {human_name: 'Custom Name'})
40
41
  StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
41
42
  human_name = options[:human_name]
42
- else
43
+ in nil
43
44
  # New style: initialize(machine, name, human_name: 'Custom Name')
44
- raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
45
-
46
45
  StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
46
+ else
47
+ # Handle unexpected options
48
+ raise ArgumentError, "Unexpected positional argument in Event initialize: #{options.inspect}"
47
49
  end
48
50
 
49
51
  @machine = machine
@@ -131,12 +133,14 @@ module StateMachines
131
133
  # specified, then this will match any to state.
132
134
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
133
135
  # conditionals defined for each one. Default is true.
134
- def transition_for(object, requirements = {})
136
+ #
137
+ # Event arguments are passed to guard conditions if they accept multiple parameters.
138
+ def transition_for(object, requirements = {}, *event_args)
135
139
  StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
136
140
  requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
137
141
 
138
142
  branches.each do |branch|
139
- next unless (match = branch.match(object, requirements))
143
+ next unless (match = branch.match(object, requirements, event_args))
140
144
 
141
145
  # Branch allows for the transition to occur
142
146
  from = requirements[:from]
@@ -161,13 +165,13 @@ module StateMachines
161
165
  #
162
166
  # Any additional arguments are passed to the StateMachines::Transition#perform
163
167
  # instance method.
164
- def fire(object, *)
168
+ def fire(object, *event_args)
165
169
  machine.reset(object)
166
170
 
167
- if (transition = transition_for(object))
168
- transition.perform(*)
171
+ if (transition = transition_for(object, {}, *event_args))
172
+ transition.perform(*event_args)
169
173
  else
170
- on_failure(object, *)
174
+ on_failure(object, *event_args)
171
175
  false
172
176
  end
173
177
  end
@@ -204,13 +208,13 @@ module StateMachines
204
208
  # event.transition all - :idling => :parked, :idling => same
205
209
  # event # => #<StateMachines::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
206
210
  def inspect
207
- transitions = branches.map do |branch|
211
+ transitions = branches.flat_map do |branch|
208
212
  branch.state_requirements.map do |state_requirement|
209
213
  "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
210
- end * ', '
211
- end
214
+ end
215
+ end.join(', ')
212
216
 
213
- "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
217
+ "#<#{self.class} name=#{name.inspect} transitions=[#{transitions}]>"
214
218
  end
215
219
 
216
220
  protected
@@ -117,19 +117,27 @@ module StateMachines
117
117
  return unless machine.action
118
118
 
119
119
  # TODO, simplify
120
- machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
121
- if event = self[event_name.to_sym, :name]
122
- event.transition_for(object) || begin
123
- # No valid transition: invalidate
124
- machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
125
- false
126
- end
127
- else
128
- # Event is unknown: invalidate
129
- machine.invalidate(object, :event, :invalid) if invalidate
130
- false
131
- end
132
- end
120
+ # First try the regular event_transition
121
+ transition = machine.read(object, :event_transition)
122
+
123
+ # If not found and we have stored transitions by machine (issue #91)
124
+ if !transition && (transitions_by_machine = object.instance_variable_get(:@_state_machine_event_transitions))
125
+ transition = transitions_by_machine[machine.name]
126
+ end
127
+
128
+ transition || if event_name = machine.read(object, :event)
129
+ if event = self[event_name.to_sym, :name]
130
+ event.transition_for(object) || begin
131
+ # No valid transition: invalidate
132
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
133
+ false
134
+ end
135
+ else
136
+ # Event is unknown: invalidate
137
+ machine.invalidate(object, :event, :invalid) if invalidate
138
+ false
139
+ end
140
+ end
133
141
  end
134
142
 
135
143
  private
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file provides optional async extensions for the Machine class.
4
+ # It should only be loaded when async functionality is explicitly requested.
5
+
6
+ module StateMachines
7
+ class Machine
8
+ # AsyncMode extensions for the Machine class
9
+ # Provides async-aware methods while maintaining backward compatibility
10
+ module AsyncExtensions
11
+ # Instance methods added to Machine for async support
12
+
13
+ # Configure this specific machine instance for async mode
14
+ #
15
+ # Example:
16
+ # class Vehicle
17
+ # state_machine initial: :parked do
18
+ # configure_async_mode! # Enable async for this machine
19
+ #
20
+ # event :ignite do
21
+ # transition parked: :idling
22
+ # end
23
+ # end
24
+ # end
25
+ def configure_async_mode!(enabled = true)
26
+ if enabled
27
+ begin
28
+ require 'state_machines/async_mode'
29
+ @async_mode_enabled = true
30
+
31
+ owner_class.include(StateMachines::AsyncMode::ThreadSafeState)
32
+ owner_class.include(StateMachines::AsyncMode::AsyncEvents)
33
+ self.extend(StateMachines::AsyncMode::AsyncMachine)
34
+
35
+ # Extend events to generate async versions
36
+ events.each do |event|
37
+ event.extend(StateMachines::AsyncMode::AsyncEventExtensions)
38
+ end
39
+ rescue LoadError => e
40
+ # Fallback to sync mode with warning (only once per class)
41
+ unless owner_class.instance_variable_get(:@async_fallback_warned)
42
+ warn <<~WARNING
43
+ ⚠️ #{owner_class.name}: Async mode requested but not available on #{RUBY_ENGINE}.
44
+
45
+ #{e.message}
46
+
47
+ ⚠️ Falling back to synchronous mode. Results may be unpredictable due to engine limitations.
48
+ For production async support, use MRI Ruby (CRuby) 3.2+
49
+ WARNING
50
+ owner_class.instance_variable_set(:@async_fallback_warned, true)
51
+ end
52
+
53
+ @async_mode_enabled = false
54
+ end
55
+ else
56
+ @async_mode_enabled = false
57
+ end
58
+
59
+ self
60
+ end
61
+
62
+ # Check if this specific machine instance has async mode enabled
63
+ def async_mode_enabled?
64
+ @async_mode_enabled || false
65
+ end
66
+
67
+ # Thread-safe version of state reading
68
+ def read_safely(object, attribute, ivar = false)
69
+ object.read_state_safely(self, attribute, ivar)
70
+ end
71
+
72
+ # Thread-safe version of state writing
73
+ def write_safely(object, attribute, value, ivar = false)
74
+ object.write_state_safely(self, attribute, value, ivar)
75
+ end
76
+
77
+ # Thread-safe callback execution for async operations
78
+ def run_callbacks_safely(type, object, context, transition)
79
+ object.state_machine_mutex.with_write_lock do
80
+ callbacks[type].each { |callback| callback.call(object, context, transition) }
81
+ end
82
+ end
83
+ end
84
+
85
+ # Include async extensions by default (but only load AsyncMode when requested)
86
+ include AsyncExtensions
87
+ end
88
+ end
@@ -30,6 +30,10 @@ module StateMachines
30
30
  machine = machine.clone
31
31
  machine.initial_state = options[:initial] if options.include?(:initial)
32
32
  machine.owner_class = owner_class
33
+ # Configure async mode if requested in options
34
+ if options.include?(:async)
35
+ machine.configure_async_mode!(options[:async])
36
+ end
33
37
  end
34
38
 
35
39
  # Evaluate DSL
@@ -6,7 +6,7 @@ module StateMachines
6
6
  # Initializes a new state machine with the given configuration.
7
7
  def initialize(owner_class, *args, &)
8
8
  options = args.last.is_a?(Hash) ? args.pop : {}
9
- StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
9
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions, :async)
10
10
 
11
11
  # Find an integration that matches this machine's owner class
12
12
  @integration = if options.include?(:integration)
@@ -35,6 +35,8 @@ module StateMachines
35
35
  @use_transactions = options[:use_transactions]
36
36
  @initialize_state = options[:initialize]
37
37
  @action_hook_defined = false
38
+ @async_requested = options[:async]
39
+
38
40
  self.owner_class = owner_class
39
41
 
40
42
  # Merge with sibling machine configurations
@@ -47,6 +49,12 @@ module StateMachines
47
49
 
48
50
  # Evaluate DSL
49
51
  instance_eval(&) if block_given?
52
+
53
+ # Configure async mode if requested, after owner_class is set and DSL is evaluated
54
+ if @async_requested
55
+ configure_async_mode!(true)
56
+ end
57
+
50
58
  self.initial_state = options[:initial] unless sibling_machines.any?
51
59
  end
52
60
 
@@ -61,6 +69,8 @@ module StateMachines
61
69
  @states = @states.dup
62
70
  @states.machine = self
63
71
  @callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
72
+ @async_requested = orig.instance_variable_get(:@async_requested)
73
+ @async_mode_enabled = orig.instance_variable_get(:@async_mode_enabled)
64
74
  end
65
75
 
66
76
  # Sets the class which is the owner of this state machine. Any methods
@@ -14,6 +14,7 @@ require_relative 'machine/event_methods'
14
14
  require_relative 'machine/callbacks'
15
15
  require_relative 'machine/rendering'
16
16
  require_relative 'machine/integration'
17
+ require_relative 'machine/async_extensions'
17
18
  require_relative 'syntax_validator'
18
19
 
19
20
  module StateMachines
@@ -558,8 +559,8 @@ module StateMachines
558
559
  else
559
560
  name = self.name
560
561
  helper_module.class_eval do
561
- define_method(method) do |*block_args, **block_kwargs|
562
- block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args, **block_kwargs)
562
+ define_method(method) do |*args, **kwargs|
563
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *args, **kwargs)
563
564
  end
564
565
  end
565
566
  end
@@ -55,7 +55,8 @@ module StateMachines
55
55
  # * <tt>:human_name</tt> - The human-readable version of this state's name
56
56
  def initialize(machine, name, options = nil, initial: false, value: :__not_provided__, cache: nil, if: nil, human_name: nil, **extra_options) # :nodoc:
57
57
  # Handle both old hash style and new kwargs style for backward compatibility
58
- if options.is_a?(Hash)
58
+ case options
59
+ in Hash
59
60
  # Old style: initialize(machine, name, {initial: true, value: 'foo'})
60
61
  StateMachines::OptionsValidator.assert_valid_keys!(options, :initial, :value, :cache, :if, :human_name)
61
62
  initial = options.fetch(:initial, false)
@@ -63,13 +64,13 @@ module StateMachines
63
64
  cache = options[:cache]
64
65
  if_condition = options[:if]
65
66
  human_name = options[:human_name]
66
- else
67
+ in nil
67
68
  # New style: initialize(machine, name, initial: true, value: 'foo')
68
- # options parameter should be nil in this case
69
- raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
70
-
71
69
  StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty?
72
70
  if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
71
+ else
72
+ # Handle unexpected options
73
+ raise ArgumentError, "Unexpected positional argument in State initialize: #{options.inspect}"
73
74
  end
74
75
 
75
76
  @machine = machine
@@ -289,9 +290,9 @@ module StateMachines
289
290
  predicate_method = "#{qualified_name}?"
290
291
 
291
292
  if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method)
292
- warn "Instance method #{predicate_method.inspect} is already defined in #{machine.owner_class.ancestors.first.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
293
+ warn_about_method_conflict(predicate_method, machine.owner_class.ancestors.first)
293
294
  elsif machine.send(:owner_class_has_method?, :instance, predicate_method)
294
- warn "Instance method #{predicate_method.inspect} is already defined in #{machine.owner_class.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
295
+ warn_about_method_conflict(predicate_method, machine.owner_class)
295
296
  else
296
297
  machine.define_helper(:instance, predicate_method) do |machine, object|
297
298
  machine.states.matches?(object, name)
@@ -303,5 +304,11 @@ module StateMachines
303
304
  def context_name_for(method)
304
305
  :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
305
306
  end
307
+
308
+ def warn_about_method_conflict(method, defined_in)
309
+ return if StateMachines::Machine.ignore_method_conflicts
310
+
311
+ warn "Instance method #{method.inspect} is already defined in #{defined_in.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
312
+ end
306
313
  end
307
314
  end