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 +4 -4
- data/README.md +30 -5
- data/lib/state_machines/branch.rb +21 -7
- data/lib/state_machines/callback.rb +1 -1
- data/lib/state_machines/eval_helpers.rb +123 -32
- data/lib/state_machines/event.rb +12 -10
- data/lib/state_machines/machine.rb +2 -2
- data/lib/state_machines/state.rb +8 -2
- data/lib/state_machines/transition.rb +14 -5
- data/lib/state_machines/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fcd04394640ccea1429d2e121a46e2c705f8c16b2987b142858ad2f151c9c287
|
4
|
+
data.tar.gz: 58cfb25ff972b1b2802730d9084d3c3b27c59a82e694646ce676e38fc661cbb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9328eea4f9d8d9e57124571de54bb8f5160a044ef913d3e515bcaac933b7355ccf987c10699c71e94145f44bc1e2387342f350dcbff0100d76a5c1c06645321
|
7
|
+
data.tar.gz: 07c7bfad14f7ce103eec5d6dfa48a9117bc0a73cb5433447c4bf7b6cdea1b87b894a12ad6b85b5a0cb05d63dd68e291f980eeca40e60754c13d5b5c44a7fcf29
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|

|
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
|
-
|
13
|
+
```ruby
|
14
|
+
gem 'state_machines'
|
15
|
+
```
|
13
16
|
|
14
17
|
And then execute:
|
15
18
|
|
16
|
-
|
19
|
+
```sh
|
20
|
+
bundle
|
21
|
+
```
|
17
22
|
|
18
23
|
Or install it yourself as:
|
19
24
|
|
20
|
-
|
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]
|
184
|
-
|
185
|
-
|
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
|
@@ -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
|
-
|
74
|
+
in Symbol => sym
|
60
75
|
klass = (class << object; self; end)
|
61
|
-
args = [] if (klass.method_defined?(
|
62
|
-
object.send(
|
63
|
-
|
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 =
|
80
|
+
arity = proc.arity
|
66
81
|
# Handle blocks for Procs
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
101
|
+
proc.call(*args, **)
|
83
102
|
|
84
|
-
|
103
|
+
in Method => meth
|
85
104
|
args.unshift(object)
|
86
|
-
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
|
-
|
94
|
-
|
112
|
+
meth.call(*args, **, &block)
|
113
|
+
in String => str
|
95
114
|
# Input validation for string evaluation
|
96
|
-
validate_eval_string(
|
115
|
+
validate_eval_string(str)
|
97
116
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
data/lib/state_machines/event.rb
CHANGED
@@ -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
|
-
|
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.
|
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 |*
|
562
|
-
block.call((scope == :instance ? self.class : self).state_machine(name), self, *
|
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
|
data/lib/state_machines/state.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
157
|
-
# transition.perform(false)
|
158
|
-
# transition.perform(
|
159
|
-
# transition.perform(Time.now
|
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 =
|
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
|