state_machines 0.4.0 → 0.5.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/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Changelog.md +7 -1
- data/README.md +11 -9
- data/lib/state_machines/extensions.rb +1 -1
- data/lib/state_machines/integrations.rb +1 -5
- data/lib/state_machines/integrations/base.rb +2 -5
- data/lib/state_machines/machine.rb +14 -10
- data/lib/state_machines/path_collection.rb +3 -3
- data/lib/state_machines/state.rb +21 -16
- data/lib/state_machines/state_collection.rb +1 -1
- data/lib/state_machines/transition.rb +1 -1
- data/lib/state_machines/transition_collection.rb +2 -2
- data/lib/state_machines/version.rb +1 -1
- data/state_machines.gemspec +1 -2
- data/test/files/integrations/vehicle.rb +1 -1
- data/test/files/models/driver.rb +13 -0
- data/test/files/models/motorcycle.rb +5 -0
- data/test/functional/driver_default_nonstandard_test.rb +13 -0
- data/test/functional/motorcycle_test.rb +6 -0
- data/test/unit/event/event_with_matching_disabled_transitions_test.rb +1 -1
- data/test/unit/event/event_with_transition_with_nil_to_state_test.rb +2 -2
- data/test/unit/integrations/integration_matcher_test.rb +4 -2
- data/test/unit/machine/machine_with_custom_integration_test.rb +2 -2
- data/test/unit/transition_collection/transition_collection_empty_with_block_test.rb +1 -1
- data/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb +10 -14
- data/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb +14 -10
- metadata +9 -426
- data/test/unit/branch/branch_with_multiple_on_requirements_test.rb +0 -20
- data/test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '029961fc61666a778298a5b6e5b5b31802b8710f'
|
4
|
+
data.tar.gz: d9e00ff04cbffbaf522e82b40e76aeb9c9ab3564
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52f6b57620f17c661585a1bccf2340d46be80366f2a7de609c41033ff46024cc62fc17814279267b26de65974612f149c0a057b47f2396e45f842c249742c9cd
|
7
|
+
data.tar.gz: b8bbe1923532edffd936139272782d6053b0ab8f1b338eaf61f805fbbc2a3b1c284bd523c35a375fc40fecb6f4948cb4aabcfc72c0fda5ded46959557a20b1cc
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
state_machines
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/.travis.yml
CHANGED
data/Changelog.md
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
|
1
|
+
## 0.5.0
|
2
|
+
|
3
|
+
* Fix states being evaluated with wrong `owner_class` context
|
4
|
+
|
5
|
+
* Fixed state machine false duplication
|
6
|
+
|
7
|
+
* Fixed inconsistent use of :use_transactions
|
2
8
|
|
3
9
|
* Namespaced integrations are not registered by default anymore
|
4
10
|
|
data/README.md
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
[](https://travis-ci.org/state-machines/state_machines)
|
2
|
-
[](https://codeclimate.com/github/state-machines/state_machines)
|
3
3
|
# State Machines
|
4
4
|
|
5
5
|
State Machines adds support for creating state machines for attributes on any Ruby class.
|
6
6
|
|
7
|
+
*Please note that multiple integrations are available for [Active Model](https://github.com/state-machines/state_machines-activemodel), [Active Record](https://github.com/state-machines/state_machines-activerecord), [Mongoid](https://github.com/state-machines/state_machines-mongoid) and more in the [State Machines organisation](https://github.com/state-machines).* If you want to save state in your database, **you need one of these additional integrations**.
|
8
|
+
|
7
9
|
## Installation
|
8
10
|
|
9
11
|
Add this line to your application's Gemfile:
|
@@ -40,10 +42,10 @@ class Vehicle
|
|
40
42
|
attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
|
41
43
|
|
42
44
|
state_machine :state, initial: :parked do
|
43
|
-
before_transition parked:
|
44
|
-
|
45
|
+
before_transition parked: any - :parked, do: :put_on_seatbelt
|
46
|
+
|
45
47
|
after_transition on: :crash, do: :tow
|
46
|
-
after_transition on: :repair,
|
48
|
+
after_transition on: :repair, do: :fix
|
47
49
|
after_transition any => :parked do |vehicle, transition|
|
48
50
|
vehicle.seatbelt_on = false
|
49
51
|
end
|
@@ -83,7 +85,7 @@ class Vehicle
|
|
83
85
|
event :repair do
|
84
86
|
# The first transition that matches the state and passes its conditions
|
85
87
|
# will be used
|
86
|
-
transition stalled: parked, unless: :auto_shop_busy
|
88
|
+
transition stalled: :parked, unless: :auto_shop_busy
|
87
89
|
transition stalled: same
|
88
90
|
end
|
89
91
|
|
@@ -217,7 +219,7 @@ vehicle.fire_events(:shift_down, :enable_alarm) # => true
|
|
217
219
|
vehicle.state_name # => :first_gear
|
218
220
|
vehicle.alarm_state_name # => :active
|
219
221
|
|
220
|
-
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:
|
222
|
+
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm
|
221
223
|
|
222
224
|
# Human-friendly names can be accessed for states/events
|
223
225
|
Vehicle.human_state_name(:first_gear) # => "first gear"
|
@@ -400,8 +402,8 @@ For example, transitions and callbacks can be defined like so:
|
|
400
402
|
class Vehicle
|
401
403
|
state_machine initial: :parked do
|
402
404
|
before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
|
403
|
-
after_transition to: :parked do |transition|
|
404
|
-
|
405
|
+
after_transition to: :parked do |vehicle, transition|
|
406
|
+
vehicle.seatbelt = 'off'
|
405
407
|
end
|
406
408
|
|
407
409
|
event :ignite do
|
@@ -425,7 +427,7 @@ class Vehicle
|
|
425
427
|
...
|
426
428
|
|
427
429
|
state :parked do
|
428
|
-
transition to
|
430
|
+
transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
|
429
431
|
|
430
432
|
def speed
|
431
433
|
0
|
@@ -133,7 +133,7 @@ module StateMachines
|
|
133
133
|
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
134
134
|
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
135
135
|
#
|
136
|
-
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::
|
136
|
+
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm
|
137
137
|
def fire_events!(*events)
|
138
138
|
run_action = [true, false].include?(events.last) ? events.pop : true
|
139
139
|
fire_events(*(events + [run_action])) || fail(StateMachines::InvalidParallelTransition.new(self, events))
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'set'
|
2
|
-
|
3
1
|
module StateMachines
|
4
2
|
# Integrations allow state machines to take advantage of features within the
|
5
3
|
# context of a particular library. This is currently most useful with
|
@@ -54,7 +52,6 @@ module StateMachines
|
|
54
52
|
|
55
53
|
alias_method :list, :integrations
|
56
54
|
|
57
|
-
|
58
55
|
# Attempts to find an integration that matches the given class. This will
|
59
56
|
# look through all of the built-in integrations under the StateMachines::Integrations
|
60
57
|
# namespace and find one that successfully matches the class.
|
@@ -86,7 +83,7 @@ module StateMachines
|
|
86
83
|
# == Examples
|
87
84
|
#
|
88
85
|
# StateMachines::Integrations.match_ancestors([]) # => nil
|
89
|
-
# StateMachines::Integrations.match_ancestors([
|
86
|
+
# StateMachines::Integrations.match_ancestors([ActiveRecord::Base]) # => StateMachines::Integrations::ActiveModel
|
90
87
|
def match_ancestors(ancestors)
|
91
88
|
integrations.detect { |integration| integration.matches_ancestors?(ancestors) }
|
92
89
|
end
|
@@ -103,7 +100,6 @@ module StateMachines
|
|
103
100
|
integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
|
104
101
|
end
|
105
102
|
|
106
|
-
|
107
103
|
private
|
108
104
|
|
109
105
|
def add(integration)
|
@@ -24,20 +24,17 @@ module StateMachines
|
|
24
24
|
|
25
25
|
# Whether the integration should be used for the given class.
|
26
26
|
def matches?(klass)
|
27
|
-
|
27
|
+
matching_ancestors.any? { |ancestor| klass <= ancestor }
|
28
28
|
end
|
29
29
|
|
30
30
|
# Whether the integration should be used for the given list of ancestors.
|
31
31
|
def matches_ancestors?(ancestors)
|
32
32
|
(ancestors & matching_ancestors).any?
|
33
33
|
end
|
34
|
-
|
35
34
|
end
|
36
35
|
|
37
|
-
extend ClassMethods
|
38
|
-
|
39
36
|
def self.included(base) #:nodoc:
|
40
|
-
base.
|
37
|
+
base.extend ClassMethods
|
41
38
|
end
|
42
39
|
end
|
43
40
|
end
|
@@ -419,7 +419,11 @@ module StateMachines
|
|
419
419
|
name = args.first || :state
|
420
420
|
|
421
421
|
# Find an existing machine
|
422
|
-
|
422
|
+
machine = owner_class.respond_to?(:state_machines) &&
|
423
|
+
(args.first && owner_class.state_machines[name] || !args.first &&
|
424
|
+
owner_class.state_machines.values.first) || nil
|
425
|
+
|
426
|
+
if machine
|
423
427
|
# Only create a new copy if changes are being made to the machine in
|
424
428
|
# a subclass
|
425
429
|
if machine.owner_class != owner_class && (options.any? || block_given?)
|
@@ -2046,14 +2050,12 @@ module StateMachines
|
|
2046
2050
|
# the method and is further along in the ancestor chain than this
|
2047
2051
|
# machine's helper module.
|
2048
2052
|
def owner_class_ancestor_has_method?(scope, method)
|
2053
|
+
return false unless owner_class_has_method?(scope, method)
|
2054
|
+
|
2049
2055
|
superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
|
2050
2056
|
|
2051
2057
|
if scope == :class
|
2052
|
-
|
2053
|
-
current = (
|
2054
|
-
class << owner_class;
|
2055
|
-
self;
|
2056
|
-
end)
|
2058
|
+
current = owner_class.singleton_class
|
2057
2059
|
superclass = superclasses.first
|
2058
2060
|
else
|
2059
2061
|
current = owner_class
|
@@ -2068,14 +2070,16 @@ module StateMachines
|
|
2068
2070
|
|
2069
2071
|
# Search for for the first ancestor that defined this method
|
2070
2072
|
ancestors.detect do |ancestor|
|
2071
|
-
ancestor = (
|
2072
|
-
class << ancestor;
|
2073
|
-
self;
|
2074
|
-
end) if scope == :class && ancestor.is_a?(Class)
|
2073
|
+
ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
|
2075
2074
|
ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
|
2076
2075
|
end
|
2077
2076
|
end
|
2078
2077
|
|
2078
|
+
def owner_class_has_method?(scope, method)
|
2079
|
+
target = scope == :class ? owner_class.singleton_class : owner_class
|
2080
|
+
target.method_defined?(method) || target.private_method_defined?(method)
|
2081
|
+
end
|
2082
|
+
|
2079
2083
|
# Adds helper methods for accessing naming information about states and
|
2080
2084
|
# events on the owner class
|
2081
2085
|
def define_name_helpers
|
@@ -45,7 +45,7 @@ module StateMachines
|
|
45
45
|
#
|
46
46
|
# paths.from_states # => [:parked, :idling, :first_gear, ...]
|
47
47
|
def from_states
|
48
|
-
|
48
|
+
flat_map(&:from_states).uniq
|
49
49
|
end
|
50
50
|
|
51
51
|
# Lists all of the states that can be transitioned to through the paths in
|
@@ -55,7 +55,7 @@ module StateMachines
|
|
55
55
|
#
|
56
56
|
# paths.to_states # => [:idling, :first_gear, :second_gear, ...]
|
57
57
|
def to_states
|
58
|
-
|
58
|
+
flat_map(&:to_states).uniq
|
59
59
|
end
|
60
60
|
|
61
61
|
# Lists all of the events that can be fired through the paths in this
|
@@ -65,7 +65,7 @@ module StateMachines
|
|
65
65
|
#
|
66
66
|
# paths.events # => [:park, :ignite, :shift_up, ...]
|
67
67
|
def events
|
68
|
-
|
68
|
+
flat_map(&:events).uniq
|
69
69
|
end
|
70
70
|
|
71
71
|
private
|
data/lib/state_machines/state.rb
CHANGED
@@ -2,7 +2,7 @@ module StateMachines
|
|
2
2
|
# A state defines a value that an attribute can be in after being transitioned
|
3
3
|
# 0 or more times. States can represent a value of any type in Ruby, though
|
4
4
|
# the most common (and default) type is String.
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# In addition to defining the machine's value, a state can also define a
|
7
7
|
# behavioral context for an object when that object is in the state. See
|
8
8
|
# StateMachines::Machine#state for more information about how state-driven
|
@@ -10,7 +10,7 @@ module StateMachines
|
|
10
10
|
class State
|
11
11
|
|
12
12
|
# The state machine for which this state is defined
|
13
|
-
|
13
|
+
attr_reader :machine
|
14
14
|
|
15
15
|
# The unique identifier for the state used in event and callback definitions
|
16
16
|
attr_reader :name
|
@@ -38,7 +38,7 @@ module StateMachines
|
|
38
38
|
attr_accessor :matcher
|
39
39
|
|
40
40
|
# Creates a new state within the context of the given machine.
|
41
|
-
#
|
41
|
+
#
|
42
42
|
# Configuration options:
|
43
43
|
# * <tt>:initial</tt> - Whether this state is the beginning state for the
|
44
44
|
# machine. Default is false.
|
@@ -86,6 +86,11 @@ module StateMachines
|
|
86
86
|
@context = StateContext.new(self)
|
87
87
|
end
|
88
88
|
|
89
|
+
def machine=(machine)
|
90
|
+
@machine = machine
|
91
|
+
@context = StateContext.new(self)
|
92
|
+
end
|
93
|
+
|
89
94
|
# Determines whether there are any states that can be transitioned to from
|
90
95
|
# this state. If there are none, then this state is considered *final*.
|
91
96
|
# Any objects in a final state will remain so forever given the current
|
@@ -107,15 +112,15 @@ module StateMachines
|
|
107
112
|
end
|
108
113
|
|
109
114
|
# Generates a human-readable description of this state's name / value:
|
110
|
-
#
|
115
|
+
#
|
111
116
|
# For example,
|
112
|
-
#
|
117
|
+
#
|
113
118
|
# State.new(machine, :parked).description # => "parked"
|
114
119
|
# State.new(machine, :parked, :value => :parked).description # => "parked"
|
115
120
|
# State.new(machine, :parked, :value => nil).description # => "parked (nil)"
|
116
121
|
# State.new(machine, :parked, :value => 1).description # => "parked (1)"
|
117
122
|
# State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
|
118
|
-
#
|
123
|
+
#
|
119
124
|
# Configuration options:
|
120
125
|
# * <tt>:human_name</tt> - Whether to use this state's human name in the
|
121
126
|
# description or just the internal name
|
@@ -129,9 +134,9 @@ module StateMachines
|
|
129
134
|
# The value that represents this state. This will optionally evaluate the
|
130
135
|
# original block if it's a lambda block. Otherwise, the static value is
|
131
136
|
# returned.
|
132
|
-
#
|
137
|
+
#
|
133
138
|
# For example,
|
134
|
-
#
|
139
|
+
#
|
135
140
|
# State.new(machine, :parked, :value => 1).value # => 1
|
136
141
|
# State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
|
137
142
|
# State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
|
@@ -152,14 +157,14 @@ module StateMachines
|
|
152
157
|
# Determines whether this state matches the given value. If no matcher is
|
153
158
|
# configured, then this will check whether the values are equivalent.
|
154
159
|
# Otherwise, the matcher will determine the result.
|
155
|
-
#
|
160
|
+
#
|
156
161
|
# For example,
|
157
|
-
#
|
162
|
+
#
|
158
163
|
# # Without a matcher
|
159
164
|
# state = State.new(machine, :parked, :value => 1)
|
160
165
|
# state.matches?(1) # => true
|
161
166
|
# state.matches?(2) # => false
|
162
|
-
#
|
167
|
+
#
|
163
168
|
# # With a matcher
|
164
169
|
# state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
|
165
170
|
# state.matches?(nil) # => false
|
@@ -170,7 +175,7 @@ module StateMachines
|
|
170
175
|
|
171
176
|
# Defines a context for the state which will be enabled on instances of
|
172
177
|
# the owner class when the machine is in this state.
|
173
|
-
#
|
178
|
+
#
|
174
179
|
# This can be called multiple times. Each time a new context is created,
|
175
180
|
# a new module will be included in the owner class.
|
176
181
|
def context(&block)
|
@@ -184,7 +189,7 @@ module StateMachines
|
|
184
189
|
new_methods = context_methods.to_a.select { |(name, method)| old_methods[name] != method }
|
185
190
|
|
186
191
|
# Alias new methods so that the only execute when the object is in this state
|
187
|
-
new_methods.each do |(method_name,
|
192
|
+
new_methods.each do |(method_name, _method)|
|
188
193
|
context_name = context_name_for(method_name)
|
189
194
|
context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
|
190
195
|
alias_method :"#{context_name}", :#{method_name}
|
@@ -208,7 +213,7 @@ module StateMachines
|
|
208
213
|
|
209
214
|
# Calls a method defined in this state's context on the given object. All
|
210
215
|
# arguments and any block will be passed into the method defined.
|
211
|
-
#
|
216
|
+
#
|
212
217
|
# If the method has never been defined for this state, then a NoMethodError
|
213
218
|
# will be raised.
|
214
219
|
def call(object, method, *args, &block)
|
@@ -239,9 +244,9 @@ module StateMachines
|
|
239
244
|
end
|
240
245
|
|
241
246
|
# Generates a nicely formatted description of this state's contents.
|
242
|
-
#
|
247
|
+
#
|
243
248
|
# For example,
|
244
|
-
#
|
249
|
+
#
|
245
250
|
# state = StateMachines::State.new(machine, :parked, :value => 1, :initial => true)
|
246
251
|
# state # => #<StateMachines::State name=:parked value=1 initial=true context=[]>
|
247
252
|
def inspect
|
@@ -93,7 +93,7 @@ module StateMachines
|
|
93
93
|
|
94
94
|
machine.events.each { |event| order += event.known_states }
|
95
95
|
order += select { |state| state.context_methods.any? }.map { |state| state.name }
|
96
|
-
order += keys(:name) - machine.callbacks.values.flatten.
|
96
|
+
order += keys(:name) - machine.callbacks.values.flatten.flat_map(&:known_states)
|
97
97
|
order += keys(:name)
|
98
98
|
|
99
99
|
order.uniq!
|
@@ -168,7 +168,7 @@ module StateMachines
|
|
168
168
|
def catch_exceptions
|
169
169
|
begin
|
170
170
|
yield
|
171
|
-
rescue
|
171
|
+
rescue
|
172
172
|
rollback
|
173
173
|
raise
|
174
174
|
end
|
@@ -210,7 +210,7 @@ module StateMachines
|
|
210
210
|
# Rollback only if exceptions occur during before callbacks
|
211
211
|
begin
|
212
212
|
super
|
213
|
-
rescue
|
213
|
+
rescue
|
214
214
|
rollback unless @before_run
|
215
215
|
@success = nil # mimics ActiveRecord.save behavior on rollback
|
216
216
|
raise
|
data/state_machines.gemspec
CHANGED
@@ -12,9 +12,8 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.homepage = 'https://github.com/state-machines/state_machines'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
|
15
|
-
spec.required_ruby_version = '>=
|
15
|
+
spec.required_ruby_version = '>= 2.0.0'
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
|
-
spec.test_files = spec.files.grep(/^test\//)
|
18
17
|
spec.require_paths = ['lib']
|
19
18
|
|
20
19
|
spec.add_development_dependency 'bundler', '>= 1.7.6'
|