nanomachine 1.0.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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +5 -0
- data/README.md +76 -0
- data/Rakefile +34 -0
- data/lib/nanomachine.rb +183 -0
- data/lib/nanomachine/version.rb +4 -0
- data/nanomachine.gemspec +52 -0
- data/spec/nano_machine_spec.rb +163 -0
- data/spec/spec_helper.rb +2 -0
- metadata +136 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Nanomachine
|
2
|
+
|
3
|
+
A really tiny state machine for ruby. No events, only accepted transitions and transition callbacks.
|
4
|
+
|
5
|
+
The difference between Nanomachine, and otherwise known Micromachine (https://rubygems.org/gems/micromachine) is that
|
6
|
+
Micromachine transitions to new states in response to events; multiple events can transition between the two same states.
|
7
|
+
Nanomachine, on the other hand, does not care about events, and only needs the state you want to be in after successful
|
8
|
+
transition.
|
9
|
+
|
10
|
+
Nanomachine can be used in any ruby project, and have no runtime dependencies.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Install the gem:
|
15
|
+
|
16
|
+
```shell
|
17
|
+
gem install nanomachine
|
18
|
+
```
|
19
|
+
|
20
|
+
or add it to your Gemfile, if you are using [Bundler][]:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem "nanomachine", "~> 1.0"
|
24
|
+
```
|
25
|
+
|
26
|
+
[Bundler]: http://gembundler.com/
|
27
|
+
|
28
|
+
## Example
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
state_machine = Nanomachine.new("unpublished") do |fsm|
|
32
|
+
fsm.transition("published", %w[unpublished processing removed])
|
33
|
+
fsm.transition("unpublished", %w[published processing removed])
|
34
|
+
fsm.transition("processing", %w[published unpublished])
|
35
|
+
fsm.transition("removed", []) # defined for being explicit
|
36
|
+
|
37
|
+
fsm.on_transition(:to => "processing") do |(previous_state, _), id|
|
38
|
+
Worker.schedule(id, previous_state)
|
39
|
+
end
|
40
|
+
|
41
|
+
fsm.on_transition do |(from_state, to_state)|
|
42
|
+
update_column(:state, to_state)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
if state_machine.transition_to("published")
|
47
|
+
puts "Publish success!"
|
48
|
+
else
|
49
|
+
puts "Publish failure! We’re in #{state_machine.state}."
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
## License
|
54
|
+
|
55
|
+
Copyright (c) 2012 Ivan Navarrete and Kim Burgestrand
|
56
|
+
|
57
|
+
MIT License
|
58
|
+
|
59
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
60
|
+
a copy of this software and associated documentation files (the
|
61
|
+
"Software"), to deal in the Software without restriction, including
|
62
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
63
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
64
|
+
permit persons to whom the Software is furnished to do so, subject to
|
65
|
+
the following conditions:
|
66
|
+
|
67
|
+
The above copyright notice and this permission notice shall be
|
68
|
+
included in all copies or substantial portions of the Software.
|
69
|
+
|
70
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
71
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
72
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
73
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
74
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
75
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
76
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
rescue LoadError
|
4
|
+
# bundler not required
|
5
|
+
end
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "yard"
|
9
|
+
YARD::Rake::YardocTask.new("yard:doc") do |task|
|
10
|
+
task.options = ["--no-stats"]
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "List undocumented methods and constants"
|
14
|
+
task "yard:stats" do
|
15
|
+
YARD::CLI::Stats.run("--list-undoc")
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Generate documentation and show documentation stats"
|
19
|
+
task :yard => ["yard:doc", "yard:stats"]
|
20
|
+
rescue LoadError
|
21
|
+
puts "WARN: YARD not available. You may install documentation dependencies via bundler."
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "Start an IRB session with Nanomachine loaded"
|
25
|
+
task :console do
|
26
|
+
exec "irb", "-Ilib", "-rnanomachine"
|
27
|
+
end
|
28
|
+
|
29
|
+
require "rspec/core/rake_task"
|
30
|
+
RSpec::Core::RakeTask.new do |spec|
|
31
|
+
spec.ruby_opts = ["-W"]
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => :spec
|
data/lib/nanomachine.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require "nanomachine/version"
|
2
|
+
require "set"
|
3
|
+
|
4
|
+
# A minimal state machine where you transition between states, instead
|
5
|
+
# of transition by input symbols or events.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# state_machine = Nanomachine.new("unpublished") do |fsm|
|
9
|
+
# fsm.transition("published", %w[unpublished processing removed])
|
10
|
+
# fsm.transition("unpublished", %w[published processing removed])
|
11
|
+
# fsm.transition("processing", %w[published unpublished])
|
12
|
+
# fsm.transition("removed", []) # defined for being explicit
|
13
|
+
#
|
14
|
+
# fsm.on_transition do |(from_state, to_state)|
|
15
|
+
# update_column(:state, to_state)
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# if state_machine.transition_to("published")
|
20
|
+
# puts "Publish success!"
|
21
|
+
# else
|
22
|
+
# puts "Publish failure! We’re in #{state_machine.state}."
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
#
|
26
|
+
class Nanomachine
|
27
|
+
# Raised when a transition cannot be performed.
|
28
|
+
InvalidTransitionError = Class.new(StandardError)
|
29
|
+
|
30
|
+
# Raised when a given state cannot be accepted.
|
31
|
+
InvalidStateError = Class.new(StandardError)
|
32
|
+
|
33
|
+
# Construct a Nanomachine with an initial state.
|
34
|
+
#
|
35
|
+
# @example initialization with a block
|
36
|
+
# machine = Nanomachine.new("initial") do |fsm|
|
37
|
+
# fsm.transition("initial", %w[green orange])
|
38
|
+
# fsm.transition("green", %w[orange error])
|
39
|
+
# fsm.transition("orange", %w[green error])
|
40
|
+
# # error is a dead state, no transition out of it
|
41
|
+
# # so not necessary to define the transitions for it
|
42
|
+
#
|
43
|
+
# fsm.on_transition(to: "error") do |(from_state, to_state), message|
|
44
|
+
# notifier.notify_error(message)
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# fsm.on_transition do |(from_state, to_state)|
|
48
|
+
# object.update_state(to_state)
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @param [#to_s] initial_state state the machine is in after initialization
|
53
|
+
# @raise [InvalidStateError] if initial state is nil
|
54
|
+
# @yield [self] yields the machine for easy definition of states
|
55
|
+
# @yieldparam [Nanomachine] self
|
56
|
+
def initialize(initial_state)
|
57
|
+
if initial_state.nil?
|
58
|
+
raise InvalidStateError, "initial state cannot be nil"
|
59
|
+
end
|
60
|
+
|
61
|
+
@state = initial_state.to_s
|
62
|
+
@transitions = Hash.new(Set.new)
|
63
|
+
@callbacks = Hash.new { |h, k| h[k] = [] }
|
64
|
+
yield self if block_given?
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [String] current state of the state machine.
|
68
|
+
attr_reader :state
|
69
|
+
|
70
|
+
# @example
|
71
|
+
# {"initial"=>#<Set: {"green", "orange"}>,
|
72
|
+
# "green"=>#<Set: {"orange", "error"}>,
|
73
|
+
# "orange"=>#<Set: {"green", "error"}>}
|
74
|
+
#
|
75
|
+
# @return [Hash<String, Set>] mapping of state to possible transition targets
|
76
|
+
attr_reader :transitions
|
77
|
+
|
78
|
+
# Define possible state transitions from the source state.
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# fsm.transition("green", %w[orange red])
|
82
|
+
# fsm.transition("orange", %w[red])
|
83
|
+
# fsm.transition(:error, [:nowhere])
|
84
|
+
#
|
85
|
+
# @param [#to_s] from
|
86
|
+
# @param [#each] to each target state must respond to #to_s
|
87
|
+
def transition(from, to)
|
88
|
+
transitions[from.to_s] = Set.new(to).map!(&:to_s)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Define a callback to be executed on transition.
|
92
|
+
#
|
93
|
+
# @example callback executed on any transition
|
94
|
+
# fsm.on_transition do |(from_state, to_state), *args, &block|
|
95
|
+
# # executed on any transition
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# @example callback executed on transition from a given state only
|
99
|
+
# fsm.on_transition(from: "green") do |(from_state, to_state), *args, &block|
|
100
|
+
# # executed only on transitions *from* green state
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# @example callback executed on transition to a given state only
|
104
|
+
# fsm.on_transition(to: "green") do |(from_state, to_state), *args, &block|
|
105
|
+
# # executed only on transitions *to* green state
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# @example callback executed on transition between two states only
|
109
|
+
# fsm.on_transition(from: "green", to: "red") do |(from_state, to_state), *args, &block|
|
110
|
+
# # executed only on transitions between green and red
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# @param [Hash] options constraint on when callback is to be executed
|
114
|
+
# @option options [#to_s, nil] :from (nil) only match when transitioning from the given state, nil for any
|
115
|
+
# @option options [#to_s, nil] :to (nil) only match when transitioning to the given state, nil for any
|
116
|
+
# @yield [transition, *args, &block] transition states (from, to), and parameters given to {#transition_to} on transition
|
117
|
+
# @yieldparam [Array<from_state, to_state>] transition
|
118
|
+
# @yieldparam *args arguments passed to {#transition_to}
|
119
|
+
# @yieldparam &block block passed to {#transition_to}
|
120
|
+
# @raise [ArgumentError] when given unknown options
|
121
|
+
# @raise [LocalJumpError] when no callback block is supplied
|
122
|
+
def on_transition(options = {}, &block)
|
123
|
+
unless block_given?
|
124
|
+
raise LocalJumpError, "no block given"
|
125
|
+
end
|
126
|
+
|
127
|
+
from = options.delete(:from)
|
128
|
+
from &&= from.to_s
|
129
|
+
|
130
|
+
to = options.delete(:to)
|
131
|
+
to &&= to.to_s
|
132
|
+
|
133
|
+
unless options.empty?
|
134
|
+
raise ArgumentError, "unknown options: #{options.keys.join(", ")}"
|
135
|
+
end
|
136
|
+
|
137
|
+
@callbacks[[from, to]] << block
|
138
|
+
end
|
139
|
+
|
140
|
+
# Transition the state machine from the current state to a target state.
|
141
|
+
#
|
142
|
+
# @example transition to error state with a message given to any callbacks
|
143
|
+
# if previous_state = fsm.transition_to("error", "something went really wrong")
|
144
|
+
# puts "Transition from #{previous_state} to #{fsm.state} successful!"
|
145
|
+
# else
|
146
|
+
# puts "Transition failed."
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# @param [#to_s] other_state new state to transition to
|
150
|
+
# @param args any number of arguments, passed to callbacks defined with {#on_transition}
|
151
|
+
# @param block passed to callbacks defined with {#on_transition}
|
152
|
+
# @return [String, false] state the machine was in before transition, or false if transition is not allowed
|
153
|
+
def transition_to(other_state, *args, &block)
|
154
|
+
other_state &&= other_state.to_s
|
155
|
+
if transitions[state].include?(other_state)
|
156
|
+
previous_state, @state = @state, other_state
|
157
|
+
[[nil, nil], [previous_state, nil], [nil, @state], [previous_state, @state]].each do |combo|
|
158
|
+
@callbacks[combo].each do |callback|
|
159
|
+
callback.call([previous_state, @state], *args, &block)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
previous_state
|
163
|
+
else
|
164
|
+
false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Same as {#transition_to}, but raises an error if the transition is not allowed.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# fsm.transition_to!("bogus state") # => InvalidTransitionError
|
172
|
+
#
|
173
|
+
# @param (see #transition_to)
|
174
|
+
# @return [String] the state the state machine was in before transition
|
175
|
+
# @raise [InvalidTransitionError] if the state machine cannot transition from current state to target state
|
176
|
+
def transition_to!(other_state)
|
177
|
+
if previous_state = transition_to(other_state)
|
178
|
+
previous_state
|
179
|
+
else
|
180
|
+
raise InvalidTransitionError, "cannot transition from #{state.inspect} to #{other_state.inspect}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/nanomachine.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "nanomachine/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "nanomachine"
|
8
|
+
gem.summary = "A really tiny state machine for ruby. No events, only acceptable transitions and transition callbacks."
|
9
|
+
gem.description = <<-DESCRIPTION.gsub(/^ */, "")
|
10
|
+
A really tiny state machine for ruby. No events, only accepted transitions and transition callbacks.
|
11
|
+
|
12
|
+
The difference between Nanomachine, and otherwise known Micromachine (https://rubygems.org/gems/micromachine) is that
|
13
|
+
Micromachine transitions to new states in response to events; multiple events can transition between the two same states.
|
14
|
+
Nanomachine, on the other hand, does not care about events, and only needs the state you want to be in after successful
|
15
|
+
transition.
|
16
|
+
|
17
|
+
Nanomachine can be used in any ruby project, and have no runtime dependencies.
|
18
|
+
|
19
|
+
Example:
|
20
|
+
state_machine = Nanomachine.new("unpublished") do |fsm|
|
21
|
+
fsm.transition("published", %w[unpublished processing removed])
|
22
|
+
fsm.transition("unpublished", %w[published processing removed])
|
23
|
+
fsm.transition("processing", %w[published unpublished])
|
24
|
+
fsm.transition("removed", []) # defined for being explicit
|
25
|
+
|
26
|
+
fsm.on_transition do |(from_state, to_state)|
|
27
|
+
update_column(:state, to_state)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if state_machine.transition_to("published")
|
32
|
+
puts "Publish success!"
|
33
|
+
else
|
34
|
+
puts "Publish failure! We’re in \#{state_machine.state}."
|
35
|
+
end
|
36
|
+
DESCRIPTION
|
37
|
+
|
38
|
+
gem.version = Nanomachine::VERSION
|
39
|
+
|
40
|
+
gem.homepage = "https://github.com/elabs/nanomachine"
|
41
|
+
gem.authors = ["Ivan Navarrete", "Kim Burgestrand"]
|
42
|
+
gem.email = ["crzivn@gmail.com", "kim@burgestrand.se"]
|
43
|
+
gem.license = "MIT License"
|
44
|
+
|
45
|
+
gem.files = `git ls-files`.split($/)
|
46
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
47
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
48
|
+
gem.require_paths = ["lib"]
|
49
|
+
|
50
|
+
gem.add_development_dependency "rake"
|
51
|
+
gem.add_development_dependency "rspec", "~> 2.0"
|
52
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
describe "Nanomachine state machine" do
|
2
|
+
before do
|
3
|
+
@callbacks = []
|
4
|
+
end
|
5
|
+
|
6
|
+
let(:fsm) do
|
7
|
+
Nanomachine.new("A") do |m|
|
8
|
+
m.transition("A", %w[B C E X])
|
9
|
+
m.transition("B", %w[A])
|
10
|
+
m.transition(:C, [:A, :D])
|
11
|
+
m.transition("D", %w[])
|
12
|
+
m.transition("E", %w[B C])
|
13
|
+
|
14
|
+
m.on_transition(:to => "B") do |*args, &block|
|
15
|
+
@callbacks << [:to, args, block]
|
16
|
+
end
|
17
|
+
|
18
|
+
m.on_transition(:from => "A") do |*args, &block|
|
19
|
+
@callbacks << [:from, args, block]
|
20
|
+
end
|
21
|
+
|
22
|
+
m.on_transition(:from => "A", :to => "B") do |*args, &block|
|
23
|
+
@callbacks << [:from_to, args, block]
|
24
|
+
end
|
25
|
+
|
26
|
+
m.on_transition(:from => "E", :to => "C") do |*args, &block|
|
27
|
+
@callbacks << [:from_to_e, args, block]
|
28
|
+
end
|
29
|
+
|
30
|
+
m.on_transition do |*args, &block|
|
31
|
+
@callbacks << [:any, args, block]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "VERSION" do
|
37
|
+
specify { Nanomachine::VERSION.should be_a String }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#initialize" do
|
41
|
+
it "raises an error if given an invalid initial state" do
|
42
|
+
expect { Nanomachine.new(nil) }.to raise_error(Nanomachine::InvalidStateError, /initial state/)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#state" do
|
47
|
+
it "returns the current state" do
|
48
|
+
fsm.state.should eq("A")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#transitions" do
|
53
|
+
it "returns the available transitions" do
|
54
|
+
fsm.transitions.should eq({"A" => Set.new(%w[B C E X]),
|
55
|
+
"B" => Set.new(%w[A]),
|
56
|
+
"C" => Set.new(%w[A D]),
|
57
|
+
"D" => Set.new(),
|
58
|
+
"E" => Set.new(%w[B C])})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#on_transition" do
|
63
|
+
it "raises an error when given unknown options" do
|
64
|
+
expect { fsm.on_transition(:bad_option => "foo") { } }.to raise_error(ArgumentError, /bad_option/)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "raises an error when given no block" do
|
68
|
+
expect { fsm.on_transition }.to raise_error(LocalJumpError, /no block given/)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#transition_to" do
|
73
|
+
it "transitions to the new state" do
|
74
|
+
expect { fsm.transition_to("B") }.to change { fsm.state }.from("A").to("B")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "does not transition when the transition is undefined" do
|
78
|
+
fsm.transition_to("X")
|
79
|
+
expect { fsm.transition_to("A").should be_false }.to_not change { fsm.state }
|
80
|
+
end
|
81
|
+
|
82
|
+
it "returns the previous state" do
|
83
|
+
fsm.transition_to("B").should eq("A")
|
84
|
+
end
|
85
|
+
|
86
|
+
it "returns false if transition failed" do
|
87
|
+
fsm.transition_to("D").should be_false
|
88
|
+
end
|
89
|
+
|
90
|
+
context "callbacks" do
|
91
|
+
it "executes all callbacks in the correct order, most generic first" do
|
92
|
+
block = proc {}
|
93
|
+
args = [1, [3, 4]]
|
94
|
+
|
95
|
+
fsm.transition_to("B", *args, &block)
|
96
|
+
|
97
|
+
@callbacks.should eq([
|
98
|
+
[:any, [["A", "B"], 1, [3, 4]], block],
|
99
|
+
[:from, [["A", "B"], 1, [3, 4]], block],
|
100
|
+
[:to, [["A", "B"], 1, [3, 4]], block],
|
101
|
+
[:from_to, [["A", "B"], 1, [3, 4]], block]
|
102
|
+
])
|
103
|
+
end
|
104
|
+
|
105
|
+
it "executes callbacks reacting to any transition" do
|
106
|
+
fsm.transition_to("C")
|
107
|
+
@callbacks.clear
|
108
|
+
fsm.transition_to("D")
|
109
|
+
|
110
|
+
@callbacks.should eq([
|
111
|
+
[:any, [["C", "D"]], nil],
|
112
|
+
])
|
113
|
+
end
|
114
|
+
|
115
|
+
it "executes callbacks for the from-transition" do
|
116
|
+
fsm.transition_to("C")
|
117
|
+
|
118
|
+
@callbacks.should eq([
|
119
|
+
[:any, [["A", "C"]], nil],
|
120
|
+
[:from, [["A", "C"]], nil],
|
121
|
+
])
|
122
|
+
end
|
123
|
+
|
124
|
+
it "executes callbacks for the to-transition" do
|
125
|
+
fsm.transition_to("E")
|
126
|
+
@callbacks.clear
|
127
|
+
fsm.transition_to("B")
|
128
|
+
|
129
|
+
@callbacks.should eq([
|
130
|
+
[:any, [["E", "B"]], nil],
|
131
|
+
[:to, [["E", "B"]], nil],
|
132
|
+
])
|
133
|
+
end
|
134
|
+
|
135
|
+
it "executes callbacks for the from-to-transition" do
|
136
|
+
fsm.transition_to("E")
|
137
|
+
@callbacks.clear
|
138
|
+
fsm.transition_to("C")
|
139
|
+
|
140
|
+
@callbacks.should eq([
|
141
|
+
[:any, [["E", "C"]], nil],
|
142
|
+
[:from_to_e, [["E", "C"]], nil],
|
143
|
+
])
|
144
|
+
end
|
145
|
+
|
146
|
+
it "executes no callbacks on failed transitions" do
|
147
|
+
fsm.transition_to("D").should be_false
|
148
|
+
@callbacks.should be_empty
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "#transition_to!" do
|
154
|
+
it "raises an error on an invalid transition" do
|
155
|
+
fsm.should_receive(:transition_to).and_return(false)
|
156
|
+
expect { fsm.transition_to!("D") }.to raise_error(Nanomachine::InvalidTransitionError, /cannot transition/)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "returns the previous state on success" do
|
160
|
+
fsm.transition_to!("B").should eq "A"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nanomachine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ivan Navarrete
|
9
|
+
- Kim Burgestrand
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-11-15 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: &2170833800 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2170833800
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rspec
|
28
|
+
requirement: &2170833240 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2170833240
|
37
|
+
description: ! 'A really tiny state machine for ruby. No events, only accepted transitions
|
38
|
+
and transition callbacks.
|
39
|
+
|
40
|
+
|
41
|
+
The difference between Nanomachine, and otherwise known Micromachine (https://rubygems.org/gems/micromachine)
|
42
|
+
is that
|
43
|
+
|
44
|
+
Micromachine transitions to new states in response to events; multiple events can
|
45
|
+
transition between the two same states.
|
46
|
+
|
47
|
+
Nanomachine, on the other hand, does not care about events, and only needs the state
|
48
|
+
you want to be in after successful
|
49
|
+
|
50
|
+
transition.
|
51
|
+
|
52
|
+
|
53
|
+
Nanomachine can be used in any ruby project, and have no runtime dependencies.
|
54
|
+
|
55
|
+
|
56
|
+
Example:
|
57
|
+
|
58
|
+
state_machine = Nanomachine.new("unpublished") do |fsm|
|
59
|
+
|
60
|
+
fsm.transition("published", %w[unpublished processing removed])
|
61
|
+
|
62
|
+
fsm.transition("unpublished", %w[published processing removed])
|
63
|
+
|
64
|
+
fsm.transition("processing", %w[published unpublished])
|
65
|
+
|
66
|
+
fsm.transition("removed", []) # defined for being explicit
|
67
|
+
|
68
|
+
|
69
|
+
fsm.on_transition do |(from_state, to_state)|
|
70
|
+
|
71
|
+
update_column(:state, to_state)
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
if state_machine.transition_to("published")
|
79
|
+
|
80
|
+
puts "Publish success!"
|
81
|
+
|
82
|
+
else
|
83
|
+
|
84
|
+
puts "Publish failure! We’re in #{state_machine.state}."
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
'
|
89
|
+
email:
|
90
|
+
- crzivn@gmail.com
|
91
|
+
- kim@burgestrand.se
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- .gitignore
|
97
|
+
- .rspec
|
98
|
+
- .travis.yml
|
99
|
+
- Gemfile
|
100
|
+
- README.md
|
101
|
+
- Rakefile
|
102
|
+
- lib/nanomachine.rb
|
103
|
+
- lib/nanomachine/version.rb
|
104
|
+
- nanomachine.gemspec
|
105
|
+
- spec/nano_machine_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
homepage: https://github.com/elabs/nanomachine
|
108
|
+
licenses:
|
109
|
+
- MIT License
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.8.10
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: A really tiny state machine for ruby. No events, only acceptable transitions
|
132
|
+
and transition callbacks.
|
133
|
+
test_files:
|
134
|
+
- spec/nano_machine_spec.rb
|
135
|
+
- spec/spec_helper.rb
|
136
|
+
has_rdoc:
|