very_tiny_state_machine 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +51 -0
- data/Rakefile +51 -0
- data/lib/very_tiny_state_machine.rb +215 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/very_tiny_state_machine_spec.rb +205 -0
- data/very_tiny_state_machine.gemspec +62 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 40cb0d3afb2f9c64b8eb616492132d2c45d424f0
|
4
|
+
data.tar.gz: 7ccefb4f23c017273a13c5b696bdbf10b2978db6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1c95e4e56f4c9da12c9bf07cf007060e885f7201b646ea0c5d604a949d51f27b8684ac7f202bcdaf9507afa808f17b6264e6e45d1db524089a24a43a44e28e05
|
7
|
+
data.tar.gz: 26e7aeba3f817d853f9f6bce6a4695f6a76a0f00c728b4b1a8a7d2e3f3d3932f6f3cc7254ae43afea8a0c5b60eeebc1a4ffe8f53fdf382208aa19b9b3ecf096f
|
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 WeTransfer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# very_tiny_state_machine
|
2
|
+
|
3
|
+
For when the others are not tiny enough.
|
4
|
+
|
5
|
+
The entire state machine lives in a separate variable, and does not pollute the class or the module of the caller.
|
6
|
+
The state machine has the ability to dispatch callbacks when states are switched, and the callbacks
|
7
|
+
are dispatched to the given object as messages.
|
8
|
+
|
9
|
+
@automaton = VeryTinyStateMachine.new(:initialized, self)
|
10
|
+
@automaton.permit_state :processing, :closing, :closed
|
11
|
+
@automaton.permit_transition :initialized => :processing, :processing => :closing
|
12
|
+
@automaton.permit_transition :closing => :closed
|
13
|
+
|
14
|
+
# Then, lower down the code
|
15
|
+
@automaton.transition! :processing
|
16
|
+
|
17
|
+
The object supplied as the optional second argument will receive messages when states are switched around,
|
18
|
+
in the following order (using the state machine from the previous example):
|
19
|
+
|
20
|
+
# self.leaving_initialized_state
|
21
|
+
# self.entering_processing_state
|
22
|
+
# self.transitioning_from_initialized_to_processing_state
|
23
|
+
# ..the state variable is switched here
|
24
|
+
# self.after_transitioning_from_initialized_to_processing_state
|
25
|
+
# self.after_leaving_initialized_state
|
26
|
+
# self.after_entering_processing_state
|
27
|
+
|
28
|
+
You can see in which state the machine is in:
|
29
|
+
|
30
|
+
@automaton.in_state?(:processing) #=> true
|
31
|
+
@automaton.in_state?(:initialized) #=> false
|
32
|
+
|
33
|
+
and state machine has your back if you want to do something invalid:
|
34
|
+
|
35
|
+
@automaton.transition :initialized # Will raise TinyStateMachine::InvalidFlow
|
36
|
+
@automaton.transition :something_odd # Will raise TinyStateMachine::UnknownState
|
37
|
+
|
38
|
+
## Contributing to very_tiny_state_machine
|
39
|
+
|
40
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
41
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
42
|
+
* Fork the project.
|
43
|
+
* Start a feature/bugfix branch.
|
44
|
+
* Commit and push until you are happy with your contribution.
|
45
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
46
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
47
|
+
|
48
|
+
## Copyright
|
49
|
+
|
50
|
+
Copyright (c) 2016 WeTransfer. See LICENSE.txt for further details.
|
51
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
require_relative 'lib/very_tiny_state_machine'
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
17
|
+
gem.version = VeryTinyStateMachine::VERSION
|
18
|
+
gem.name = "very_tiny_state_machine"
|
19
|
+
gem.homepage = "http://github.com/WeTransfer/very_tiny_state_machine"
|
20
|
+
gem.license = "MIT"
|
21
|
+
gem.description = %Q{You wouldn't beleive how tiny it is}
|
22
|
+
gem.summary = %Q{A minuscule state machine for storing state of interesting objects}
|
23
|
+
gem.email = "me@julik.nl"
|
24
|
+
gem.authors = ["Julik Tarkhanov"]
|
25
|
+
# dependencies defined in Gemfile
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rspec/core'
|
30
|
+
require 'rspec/core/rake_task'
|
31
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
32
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Code coverage detail"
|
36
|
+
task :simplecov do
|
37
|
+
ENV['COVERAGE'] = "true"
|
38
|
+
Rake::Task['spec'].execute
|
39
|
+
end
|
40
|
+
|
41
|
+
task :default => :spec
|
42
|
+
|
43
|
+
require 'rdoc/task'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "very_tiny_state_machine #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
# A mini state machine object that can be used to track a state flow.
|
5
|
+
#
|
6
|
+
# The entire state machine lives in a separate variable, and does not pollute the
|
7
|
+
# class or the module of the caller.
|
8
|
+
# The state machine has the ability to dispatch callbacks when states are switched, the callbacks
|
9
|
+
# are dispatched to the given object.
|
10
|
+
#
|
11
|
+
# @automaton = VeryTinyStateMachine.new(:initialized, self)
|
12
|
+
# @automaton.permit_state :processing, :closing, :closed
|
13
|
+
# @automaton.permit_transition :initialized => :processing, :processing => :closing
|
14
|
+
# @automaton.permit_transition :closing => :closed
|
15
|
+
#
|
16
|
+
# # Then, lower down the code
|
17
|
+
# @automaton.transition! :processing
|
18
|
+
#
|
19
|
+
# # This switches the internal state of the machine, and dispatches the following method
|
20
|
+
# # calls on the object given as the second argument to the constructor, in the following order:
|
21
|
+
#
|
22
|
+
# # self.leaving_initialized_state
|
23
|
+
# # self.entering_processing_state
|
24
|
+
# # self.transitioning_from_initialized_to_processing_state
|
25
|
+
# # ..the state variable is switched here
|
26
|
+
# # self.after_transitioning_from_initialized_to_processing_state
|
27
|
+
# # self.after_leaving_initialized_state
|
28
|
+
# # self.after_entering_processing_state
|
29
|
+
#
|
30
|
+
# @automaton.transition :initialized # Will raise TinyStateMachine::InvalidFlow
|
31
|
+
# @automaton.transition :something_odd # Will raise TinyStateMachine::UnknownState
|
32
|
+
#
|
33
|
+
# @automaton.in_state?(:processing) #=> true
|
34
|
+
# @automaton.in_state?(:initialized) #=> false
|
35
|
+
class VeryTinyStateMachine
|
36
|
+
VERSION = '1.0.0'
|
37
|
+
|
38
|
+
InvalidFlow = Class.new(StandardError) # Gets raised when an impossible transition gets requested
|
39
|
+
UnknownState = Class.new(StandardError) # Gets raised when an unknown state gets requested
|
40
|
+
|
41
|
+
# Initialize a new TinyStateMachine, with the initial state and the object that will receive callbacks.
|
42
|
+
#
|
43
|
+
# @param initial_state[#to_sym] the initial state of the state machine
|
44
|
+
# @param object_handling_callbacks[#send, #respond_to?] the callback handler that will receive transition notifications
|
45
|
+
def initialize(initial_state, object_handling_callbacks = nil)
|
46
|
+
@mutex = Mutex.new
|
47
|
+
@state = initial_state.to_sym
|
48
|
+
@flow = [@state]
|
49
|
+
@permitted_states = Set.new([initial_state])
|
50
|
+
@permitted_transitions = Set.new
|
51
|
+
@callbacks_via = object_handling_callbacks
|
52
|
+
end
|
53
|
+
|
54
|
+
# Permit a single state or multiple states
|
55
|
+
#
|
56
|
+
# @param states [Array] states to permit
|
57
|
+
# @return [Set] the Set of states added to permitted states as the result of the call
|
58
|
+
def permit_state(*states)
|
59
|
+
states_to_permit = Set.new(states.map(&:to_sym))
|
60
|
+
will_be_added = states_to_permit - @permitted_states
|
61
|
+
@permitted_states += states_to_permit
|
62
|
+
will_be_added
|
63
|
+
end
|
64
|
+
|
65
|
+
# Permit a transition from one state to another. If you need to add multiple transitions
|
66
|
+
# from the same state, just call the method multiple times:
|
67
|
+
#
|
68
|
+
# @machine.permit_transition :initialized => :failed, :running => :closed
|
69
|
+
# @machine.permit_transition :initialized => :running
|
70
|
+
#
|
71
|
+
# @param from_to_hash[Hash] the transitions to allow
|
72
|
+
# @return [Array] the list of states added to permitted states
|
73
|
+
def permit_transition(from_to_hash)
|
74
|
+
transitions_to_permit = Set.new
|
75
|
+
from_to_hash.each_pair do | from_state, to_state |
|
76
|
+
raise UnknownState, from_state unless @permitted_states.include?(from_state.to_sym)
|
77
|
+
raise UnknownState, to_state unless @permitted_states.include?(to_state.to_sym)
|
78
|
+
transitions_to_permit << {from_state.to_sym => to_state.to_sym}
|
79
|
+
end
|
80
|
+
additions = transitions_to_permit - @permitted_transitions
|
81
|
+
@permitted_transitions += transitions_to_permit
|
82
|
+
additions
|
83
|
+
end
|
84
|
+
|
85
|
+
# Tells whether the state is known to this state machine
|
86
|
+
#
|
87
|
+
# @param state[Symbol,String] the state to check for
|
88
|
+
# @return [Boolean] whether the state is known
|
89
|
+
def known?(state)
|
90
|
+
@permitted_states.include?(state.to_sym)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Tells whether a transition is permitted to the given state.
|
94
|
+
#
|
95
|
+
# @param to_state[Symbol,String] state to transition to
|
96
|
+
# @return [Boolean] whether the state can be transitioned to
|
97
|
+
def may_transition_to?(to_state)
|
98
|
+
to_state = to_state.to_sym
|
99
|
+
transition = {@state => to_state.to_sym}
|
100
|
+
@permitted_states.include?(to_state) && @permitted_transitions.include?(transition)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Tells whether the state machine is in a given state at the moment
|
104
|
+
#
|
105
|
+
# @param requisite_state [Symbol,String] name of the state to check for
|
106
|
+
# @return [Boolean] whether the machine is in that state currently
|
107
|
+
def in_state?(requisite_state)
|
108
|
+
@state == requisite_state.to_sym
|
109
|
+
end
|
110
|
+
|
111
|
+
# Ensure the machine is in a given state, and if it isn't raise an InvalidFlow
|
112
|
+
#
|
113
|
+
# @param requisite_state[#to_sym] the state to verify
|
114
|
+
# @raise InvalidFlow
|
115
|
+
# @return [TrueClass] true if the machine is in the requisite state
|
116
|
+
def expect!(requisite_state)
|
117
|
+
unless requisite_state.to_sym == @state
|
118
|
+
raise InvalidFlow, "Must be in #{requisite_state.inspect} state, but was in #{@state.inspect}"
|
119
|
+
end
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
# Transition to a given state. Will raise an InvalidFlow exception if the transition is impossible.
|
124
|
+
# Additionally, if you want to transition to a state that is already activated, an InvalidFlow will
|
125
|
+
# be raised if you did not permit this transition explicitly. If you want to transition to a state OR
|
126
|
+
# stay in it if it is already active use {TinyStateMachine#transition_or_maintain!}
|
127
|
+
#
|
128
|
+
#
|
129
|
+
# During transitions the before callbacks will be called on the @callbacks_via instance variable. If you are
|
130
|
+
# transitioning from "initialized" to "processing" for instance, the following callbacks will be dispatched:
|
131
|
+
#
|
132
|
+
# * leaving_initialized_state
|
133
|
+
# * entering_processing_state
|
134
|
+
# * transitioning_from_initialized_to_processing_state
|
135
|
+
# ..the state variable is switched here
|
136
|
+
# * after_transitioning_from_initialized_to_processing_state
|
137
|
+
# * after_leaving_initialized_state
|
138
|
+
# * after_entering_processing_state
|
139
|
+
#
|
140
|
+
# The return value of the callbacks does not matter.
|
141
|
+
#
|
142
|
+
# @param new_state[#to_sym] the state to transition to.
|
143
|
+
# @return [Symbol] the state that the machine has just left
|
144
|
+
# @raise InvalidFlow
|
145
|
+
def transition!(new_state)
|
146
|
+
new_state = new_state.to_sym
|
147
|
+
|
148
|
+
raise UnknownState, new_state.inspect unless known?(new_state)
|
149
|
+
if may_transition_to?(new_state)
|
150
|
+
@mutex.synchronize do
|
151
|
+
dispatch_callbacks_before_transition(new_state)
|
152
|
+
previous = @state
|
153
|
+
@state = new_state.to_sym
|
154
|
+
@flow << new_state.to_sym
|
155
|
+
dispatch_callbacks_after_transition(previous)
|
156
|
+
previous
|
157
|
+
end
|
158
|
+
else
|
159
|
+
raise InvalidFlow,
|
160
|
+
"Cannot change states from #{@state} to #{new_state} (flow so far: #{@flow.join(' > ')})"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Transition to a given state. If the machine already is in that state, do nothing.
|
165
|
+
# If the transition has to happen (the requested state is different than the current)
|
166
|
+
# transition! will be called instead.
|
167
|
+
#
|
168
|
+
# @see TinyStateMachine#transition!
|
169
|
+
# @param new_state[Symbol,String] the state to transition to.
|
170
|
+
# @raise InvalidFlow
|
171
|
+
# @return [void]
|
172
|
+
def transition_or_maintain!(new_state)
|
173
|
+
return if in_state?(new_state)
|
174
|
+
transition! new_state
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns the flow of the transitions the machine went through so far
|
178
|
+
#
|
179
|
+
# @return [Array] the array of states
|
180
|
+
def flow_so_far
|
181
|
+
@flow.dup
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def dispatch_callbacks_after_transition(from)
|
187
|
+
to = @state
|
188
|
+
if @callbacks_via.respond_to?("after_transitioning_from_#{from}_to_#{to}_state", also_protected_and_private=true)
|
189
|
+
@callbacks_via.send("after_transitioning_from_#{from}_to_#{to}_state")
|
190
|
+
end
|
191
|
+
|
192
|
+
if @callbacks_via.respond_to?("after_leaving_#{from}_state", also_protected_and_private=true)
|
193
|
+
@callbacks_via.send("after_leaving_#{from}_state")
|
194
|
+
end
|
195
|
+
|
196
|
+
if @callbacks_via.respond_to?("after_entering_#{to}_state", also_protected_and_private=true)
|
197
|
+
@callbacks_via.send("after_entering_#{to}_state")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def dispatch_callbacks_before_transition(to)
|
202
|
+
from = @state
|
203
|
+
if @callbacks_via.respond_to?("leaving_#{from}_state", also_protected_and_private=true)
|
204
|
+
@callbacks_via.send("leaving_#{from}_state")
|
205
|
+
end
|
206
|
+
|
207
|
+
if @callbacks_via.respond_to?("entering_#{to}_state", also_protected_and_private=true)
|
208
|
+
@callbacks_via.send("entering_#{to}_state")
|
209
|
+
end
|
210
|
+
|
211
|
+
if @callbacks_via.respond_to?("transitioning_from_#{from}_to_#{to}", also_protected_and_private=true)
|
212
|
+
@callbacks_via.send("transitioning_from_#{from}_to_#{to}")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start
|
7
|
+
|
8
|
+
require 'very_tiny_state_machine'
|
9
|
+
|
10
|
+
RSpec.configure { |config| config.order = 'random' }
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe VeryTinyStateMachine do
|
4
|
+
describe '#initialize' do
|
5
|
+
it 'creates a state machine and sets its initial state' do
|
6
|
+
machine = described_class.new(:started)
|
7
|
+
expect(machine).to be_known(:started)
|
8
|
+
expect(machine).to be_in_state(:started)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'accepts a second argument' do
|
12
|
+
acceptor = double('Callbacks')
|
13
|
+
machine = described_class.new(:started, acceptor)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#permit_state' do
|
18
|
+
it 'makes the state known to the state machine' do
|
19
|
+
machine = described_class.new(:started)
|
20
|
+
|
21
|
+
first_result_of_permission = machine.permit_state :closed
|
22
|
+
second_result_of_permission = machine.permit_state :closed, :open
|
23
|
+
|
24
|
+
expect(first_result_of_permission).to eq(Set.new([:closed]))
|
25
|
+
expect(second_result_of_permission).to eq(Set.new([:open]))
|
26
|
+
|
27
|
+
expect(machine).to be_known(:started)
|
28
|
+
expect(machine).to be_known(:closed)
|
29
|
+
expect(machine).to be_known(:open)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'does not permit transitions to the newly added state by default' do
|
33
|
+
machine = described_class.new(:started)
|
34
|
+
machine.permit_state :running
|
35
|
+
|
36
|
+
expect(machine).not_to be_may_transition_to(:running)
|
37
|
+
|
38
|
+
expect {
|
39
|
+
machine.transition! :running
|
40
|
+
}.to raise_error(described_class::InvalidFlow, /Cannot change states from started to running/)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#permit_transition' do
|
45
|
+
it 'raises on an unknown state specified as source' do
|
46
|
+
machine = described_class.new(:started)
|
47
|
+
expect {
|
48
|
+
machine.permit_transition :unknown => :started
|
49
|
+
}.to raise_error(VeryTinyStateMachine::UnknownState)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'raises on an unknwon state specified as destination' do
|
53
|
+
machine = described_class.new(:started)
|
54
|
+
expect {
|
55
|
+
machine.permit_transition :started => :unknown
|
56
|
+
}.to raise_error(VeryTinyStateMachine::UnknownState)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'returns a Set of transitions permitted after the call' do
|
60
|
+
machine = described_class.new(:started)
|
61
|
+
machine.permit_state :running
|
62
|
+
|
63
|
+
result = machine.permit_transition :started => :running
|
64
|
+
|
65
|
+
expect(result).to be_kind_of(Set)
|
66
|
+
expect(result).to eq(Set.new([{:started => :running}]))
|
67
|
+
|
68
|
+
adding_second_time = machine.permit_transition :started => :running
|
69
|
+
expect(adding_second_time).to be_kind_of(Set)
|
70
|
+
expect(adding_second_time).to be_empty
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'is able to perform the transition after it has been defined' do
|
74
|
+
machine = described_class.new(:started)
|
75
|
+
machine.permit_state :running
|
76
|
+
machine.permit_transition :started => :running
|
77
|
+
machine.transition! :running
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'allows the transition from a state to itself only explicitly' do
|
81
|
+
machine = described_class.new(:started)
|
82
|
+
expect {
|
83
|
+
machine.transition! :started
|
84
|
+
}.to raise_error(described_class::InvalidFlow)
|
85
|
+
|
86
|
+
machine.permit_transition :started => :started
|
87
|
+
machine.transition! :started
|
88
|
+
expect(machine.flow_so_far).to eq([:started, :started])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#flow_so_far' do
|
93
|
+
it 'records the flow' do
|
94
|
+
machine = described_class.new(:started)
|
95
|
+
machine.permit_state :running, :stopped
|
96
|
+
machine.permit_transition :started => :running, :running => :stopped, :stopped => :started
|
97
|
+
|
98
|
+
machine.transition! :running
|
99
|
+
machine.transition! :stopped
|
100
|
+
machine.transition! :started
|
101
|
+
|
102
|
+
flow = machine.flow_so_far
|
103
|
+
expect(flow).to eq([:started, :running, :stopped, :started])
|
104
|
+
|
105
|
+
flow << nil
|
106
|
+
expect(flow).not_to eq(machine.flow_so_far), "The flow returned should not link to the mutable array in the machine"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#transition!' do
|
111
|
+
it 'returns the previous state the object was in' do
|
112
|
+
machine = described_class.new(:started)
|
113
|
+
machine.permit_state :running
|
114
|
+
machine.permit_transition :started => :running
|
115
|
+
transitioned_from = machine.transition! :running
|
116
|
+
expect(transitioned_from).to eq(:started)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'sends all of the callbacks if the object responds to them' do
|
120
|
+
fake_acceptor = double('Callback handler')
|
121
|
+
allow(fake_acceptor).to receive(:respond_to?) {|method_name, honor_private_and_public|
|
122
|
+
expect(honor_private_and_public).to eq(true)
|
123
|
+
true
|
124
|
+
}
|
125
|
+
|
126
|
+
machine = described_class.new(:started, fake_acceptor)
|
127
|
+
machine.permit_state :running, :stopped
|
128
|
+
machine.permit_transition :started => :running, :running => :stopped, :stopped => :started
|
129
|
+
|
130
|
+
expect(fake_acceptor).to receive(:leaving_started_state)
|
131
|
+
expect(fake_acceptor).to receive(:entering_running_state)
|
132
|
+
expect(fake_acceptor).to receive(:transitioning_from_started_to_running)
|
133
|
+
expect(fake_acceptor).to receive(:after_transitioning_from_started_to_running_state)
|
134
|
+
expect(fake_acceptor).to receive(:after_leaving_started_state)
|
135
|
+
expect(fake_acceptor).to receive(:after_entering_running_state)
|
136
|
+
|
137
|
+
machine.transition! :running
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'dispatches callbacks to private methods as well' do
|
141
|
+
acceptor = Class.new do
|
142
|
+
def called?
|
143
|
+
@entered_state
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
def entering_running_state
|
148
|
+
@entered_state = true
|
149
|
+
end
|
150
|
+
end.new
|
151
|
+
|
152
|
+
machine = described_class.new(:started, acceptor)
|
153
|
+
machine.permit_state :running, :stopped
|
154
|
+
machine.permit_transition :started => :running, :running => :stopped, :stopped => :started
|
155
|
+
|
156
|
+
machine.transition! :running
|
157
|
+
expect(acceptor).to be_called
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'does not send the messages to an acceptor that does not respond to those messages' do
|
161
|
+
fake_acceptor = double('Callback handler')
|
162
|
+
allow(fake_acceptor).to receive(:respond_to?) {|method_name, honor_private_and_public|
|
163
|
+
expect(honor_private_and_public).to eq(true)
|
164
|
+
false
|
165
|
+
}
|
166
|
+
|
167
|
+
machine = described_class.new(:started, fake_acceptor)
|
168
|
+
machine.permit_state :running, :stopped
|
169
|
+
machine.permit_transition :started => :running, :running => :stopped, :stopped => :started
|
170
|
+
|
171
|
+
machine.transition! :running
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe '#transition_or_maintain!' do
|
176
|
+
it 'does not perform any transitions if the object is already in the requisite state' do
|
177
|
+
machine = described_class.new(:perfect)
|
178
|
+
machine.transition_or_maintain! :perfect
|
179
|
+
expect(machine.flow_so_far).to eq([:perfect])
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'does perform a transition if the object is not in the requisite state' do
|
183
|
+
machine = described_class.new(:perfect)
|
184
|
+
machine.permit_state :perfect, :improving
|
185
|
+
machine.permit_transition :perfect => :improving, :improving => :perfect
|
186
|
+
|
187
|
+
machine.transition_or_maintain! :improving
|
188
|
+
expect(machine.flow_so_far).to eq([:perfect, :improving])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#expect!' do
|
193
|
+
it 'returns true when the machine is in the requisite state' do
|
194
|
+
machine = described_class.new(:started)
|
195
|
+
expect(machine.expect!(:started)).to eq(true)
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'raises an exception if the machine is not in that state' do
|
199
|
+
machine = described_class.new(:started)
|
200
|
+
expect {
|
201
|
+
machine.expect!(:running)
|
202
|
+
}.to raise_error(VeryTinyStateMachine::InvalidFlow, 'Must be in :running state, but was in :started')
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: very_tiny_state_machine 1.0.0 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "very_tiny_state_machine"
|
9
|
+
s.version = "1.0.0"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Julik Tarkhanov"]
|
14
|
+
s.date = "2016-01-14"
|
15
|
+
s.description = "You wouldn't beleive how tiny it is"
|
16
|
+
s.email = "me@julik.nl"
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".rspec",
|
24
|
+
"Gemfile",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"lib/very_tiny_state_machine.rb",
|
29
|
+
"spec/spec_helper.rb",
|
30
|
+
"spec/very_tiny_state_machine_spec.rb",
|
31
|
+
"very_tiny_state_machine.gemspec"
|
32
|
+
]
|
33
|
+
s.homepage = "http://github.com/WeTransfer/very_tiny_state_machine"
|
34
|
+
s.licenses = ["MIT"]
|
35
|
+
s.rubygems_version = "2.2.2"
|
36
|
+
s.summary = "A minuscule state machine for storing state of interesting objects"
|
37
|
+
|
38
|
+
if s.respond_to? :specification_version then
|
39
|
+
s.specification_version = 4
|
40
|
+
|
41
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
42
|
+
s.add_development_dependency(%q<rspec>, ["~> 3.2.0"])
|
43
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
44
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
45
|
+
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
46
|
+
s.add_development_dependency(%q<simplecov>, ["~> 0.10"])
|
47
|
+
else
|
48
|
+
s.add_dependency(%q<rspec>, ["~> 3.2.0"])
|
49
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
50
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
51
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
52
|
+
s.add_dependency(%q<simplecov>, ["~> 0.10"])
|
53
|
+
end
|
54
|
+
else
|
55
|
+
s.add_dependency(%q<rspec>, ["~> 3.2.0"])
|
56
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
57
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
58
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
59
|
+
s.add_dependency(%q<simplecov>, ["~> 0.10"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: very_tiny_state_machine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julik Tarkhanov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rdoc
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.12'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.12'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: jeweler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.0.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.0.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.10'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.10'
|
83
|
+
description: You wouldn't beleive how tiny it is
|
84
|
+
email: me@julik.nl
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files:
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
files:
|
91
|
+
- ".document"
|
92
|
+
- ".rspec"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/very_tiny_state_machine.rb
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
- spec/very_tiny_state_machine_spec.rb
|
100
|
+
- very_tiny_state_machine.gemspec
|
101
|
+
homepage: http://github.com/WeTransfer/very_tiny_state_machine
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.2.2
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: A minuscule state machine for storing state of interesting objects
|
125
|
+
test_files: []
|