nanomachine 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|