state_machines 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c732f34387da18f4dc1c3d78cad1fbb111cc88d06d9fe3edcda912adc5e655a
4
- data.tar.gz: ef0079a89a1b0e4ddea45dd4a79e11de12740d4eb0c2aa98ad95b5ca7ae3eab8
3
+ metadata.gz: fcd04394640ccea1429d2e121a46e2c705f8c16b2987b142858ad2f151c9c287
4
+ data.tar.gz: 58cfb25ff972b1b2802730d9084d3c3b27c59a82e694646ce676e38fc661cbb9
5
5
  SHA512:
6
- metadata.gz: d71a5bd20b0de0b19e4684b4251954d0cf648e5454b57af96555372bbdee59a9ac855bb27d182e018f87ffa0525b5cd3daff5c3ce53c483f3dec4954d076a331
7
- data.tar.gz: 179e0cb6c2a31d14c4078820d254ce35e26d9b5aaa2646e4766b842127411cf49e5ad697d0a764ba79a0036bce5dc288ef113b8bef57c3cac64de8816a23f49b
6
+ metadata.gz: c9328eea4f9d8d9e57124571de54bb8f5160a044ef913d3e515bcaac933b7355ccf987c10699c71e94145f44bc1e2387342f350dcbff0100d76a5c1c06645321
7
+ data.tar.gz: 07c7bfad14f7ce103eec5d6dfa48a9117bc0a73cb5433447c4bf7b6cdea1b87b894a12ad6b85b5a0cb05d63dd68e291f980eeca40e60754c13d5b5c44a7fcf29
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg)
2
+
2
3
  # State Machines
3
4
 
4
5
  State Machines adds support for creating state machines for attributes on any Ruby class.
@@ -9,15 +10,21 @@ State Machines adds support for creating state machines for attributes on any Ru
9
10
 
10
11
  Add this line to your application's Gemfile:
11
12
 
12
- gem 'state_machines'
13
+ ```ruby
14
+ gem 'state_machines'
15
+ ```
13
16
 
14
17
  And then execute:
15
18
 
16
- $ bundle
19
+ ```sh
20
+ bundle
21
+ ```
17
22
 
18
23
  Or install it yourself as:
19
24
 
20
- $ gem install state_machines
25
+ ```sh
26
+ gem install state_machines
27
+ ```
21
28
 
22
29
  ## Usage
23
30
 
@@ -38,7 +45,7 @@ Class definition:
38
45
 
39
46
  ```ruby
40
47
  class Vehicle
41
- attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
48
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy, :parking_meter_number
42
49
 
43
50
  state_machine :state, initial: :parked do
44
51
  before_transition parked: any - :parked, do: :put_on_seatbelt
@@ -61,6 +68,18 @@ class Vehicle
61
68
  transition [:idling, :first_gear] => :parked
62
69
  end
63
70
 
71
+ before_transition on: :park do |vehicle, transition|
72
+ # If using Rails:
73
+ # options = transition.args.extract_options!
74
+
75
+ options = transition.args.last.is_a?(Hash) ? transition.args.pop : {}
76
+ meter_number = options[:meter_number]
77
+
78
+ unless meter_number.nil?
79
+ vehicle.parking_meter_number = meter_number
80
+ end
81
+ end
82
+
64
83
  event :ignite do
65
84
  transition stalled: same, parked: :idling
66
85
  end
@@ -130,6 +149,7 @@ class Vehicle
130
149
  @seatbelt_on = false
131
150
  @time_used = 0
132
151
  @auto_shop_busy = true
152
+ @parking_meter_number = nil
133
153
  super() # NOTE: This *must* be called, otherwise states won't get initialized
134
154
  end
135
155
 
@@ -200,6 +220,11 @@ vehicle.park! # => StateMachines:InvalidTransition: Cannot tra
200
220
  vehicle.state?(:parked) # => false
201
221
  vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
202
222
 
223
+ # Transition callbacks can receive arguments
224
+ vehicle.park(meter_number: '12345') # => true
225
+ vehicle.parked? # => true
226
+ vehicle.parking_meter_number # => "12345"
227
+
203
228
  # Namespaced machines have uniquely-generated methods
204
229
  vehicle.alarm_state # => 1
205
230
  vehicle.alarm_state_name # => :active
@@ -790,7 +815,7 @@ For RSpec testing, use the custom RSpec matchers:
790
815
 
791
816
  ## Contributing
792
817
 
793
- 1. Fork it ( https://github.com/state-machines/state_machines/fork )
818
+ 1. Fork it ( <https://github.com/state-machines/state_machines/fork> )
794
819
  2. Create your feature branch (`git checkout -b my-new-feature`)
795
820
  3. Commit your changes (`git commit -am 'Add some feature'`)
796
821
  4. Push to the branch (`git push origin my-new-feature`)
@@ -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
@@ -193,7 +193,7 @@ module StateMachines
193
193
  else
194
194
  @methods.each do |method|
195
195
  result = evaluate_method(object, method, *args)
196
- throw :halt if @terminator && @terminator.call(result)
196
+ throw :halt if @terminator&.call(result)
197
197
  end
198
198
  end
199
199
  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
@@ -131,12 +131,14 @@ module StateMachines
131
131
  # specified, then this will match any to state.
132
132
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
133
133
  # conditionals defined for each one. Default is true.
134
- def transition_for(object, requirements = {})
134
+ #
135
+ # Event arguments are passed to guard conditions if they accept multiple parameters.
136
+ def transition_for(object, requirements = {}, *event_args)
135
137
  StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
136
138
  requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
137
139
 
138
140
  branches.each do |branch|
139
- next unless (match = branch.match(object, requirements))
141
+ next unless (match = branch.match(object, requirements, event_args))
140
142
 
141
143
  # Branch allows for the transition to occur
142
144
  from = requirements[:from]
@@ -161,13 +163,13 @@ module StateMachines
161
163
  #
162
164
  # Any additional arguments are passed to the StateMachines::Transition#perform
163
165
  # instance method.
164
- def fire(object, *)
166
+ def fire(object, *event_args)
165
167
  machine.reset(object)
166
168
 
167
- if (transition = transition_for(object))
168
- transition.perform(*)
169
+ if (transition = transition_for(object, {}, *event_args))
170
+ transition.perform(*event_args)
169
171
  else
170
- on_failure(object, *)
172
+ on_failure(object, *event_args)
171
173
  false
172
174
  end
173
175
  end
@@ -204,13 +206,13 @@ module StateMachines
204
206
  # event.transition all - :idling => :parked, :idling => same
205
207
  # event # => #<StateMachines::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
206
208
  def inspect
207
- transitions = branches.map do |branch|
209
+ transitions = branches.flat_map do |branch|
208
210
  branch.state_requirements.map do |state_requirement|
209
211
  "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
210
- end * ', '
211
- end
212
+ end
213
+ end.join(', ')
212
214
 
213
- "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
215
+ "#<#{self.class} name=#{name.inspect} transitions=[#{transitions}]>"
214
216
  end
215
217
 
216
218
  protected
@@ -558,8 +558,8 @@ module StateMachines
558
558
  else
559
559
  name = self.name
560
560
  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)
561
+ define_method(method) do |*args, **kwargs|
562
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *args, **kwargs)
563
563
  end
564
564
  end
565
565
  end
@@ -289,9 +289,9 @@ module StateMachines
289
289
  predicate_method = "#{qualified_name}?"
290
290
 
291
291
  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."
292
+ warn_about_method_conflict(predicate_method, machine.owner_class.ancestors.first)
293
293
  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."
294
+ warn_about_method_conflict(predicate_method, machine.owner_class)
295
295
  else
296
296
  machine.define_helper(:instance, predicate_method) do |machine, object|
297
297
  machine.states.matches?(object, name)
@@ -303,5 +303,11 @@ module StateMachines
303
303
  def context_name_for(method)
304
304
  :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
305
305
  end
306
+
307
+ def warn_about_method_conflict(method, defined_in)
308
+ return if StateMachines::Machine.ignore_method_conflicts
309
+
310
+ warn "Instance method #{method.inspect} is already defined in #{defined_in.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
311
+ end
306
312
  end
307
313
  end
@@ -153,12 +153,21 @@ module StateMachines
153
153
  #
154
154
  # vehicle = Vehicle.new
155
155
  # transition = StateMachines::Transition.new(vehicle, machine, :ignite, :parked, :idling)
156
- # transition.perform # => Runs the +save+ action after setting the state attribute
157
- # transition.perform(false) # => Only sets the state attribute
158
- # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
159
- # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
156
+ # transition.perform # => Runs the +save+ action after setting the state attribute
157
+ # transition.perform(false) # => Only sets the state attribute
158
+ # transition.perform(run_action: false) # => Only sets the state attribute
159
+ # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
160
+ # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
161
+ # transition.perform(Time.now, run_action: false) # => Passes in additional arguments and only sets the state attribute
160
162
  def perform(*args)
161
- run_action = [true, false].include?(args.last) ? args.pop : true
163
+ run_action = true
164
+
165
+ if [true, false].include?(args.last)
166
+ run_action = args.pop
167
+ elsif args.last.is_a?(Hash) && args.last.key?(:run_action)
168
+ run_action = args.last.delete(:run_action)
169
+ end
170
+
162
171
  self.args = args
163
172
 
164
173
  # Run the transition
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StateMachines
4
- VERSION = '0.30.0'
4
+ VERSION = '0.31.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih