state_machines 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/state-machines/state_machines.svg?branch=master)](https://travis-ci.org/state-machines/state_machines)
|
2
|
-
[![Code Climate](https://codeclimate.com/github/state-machines/state_machines.
|
2
|
+
[![Code Climate](https://codeclimate.com/github/state-machines/state_machines.svg)](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'
|