multiflow 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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/CHANGELOG.rdoc +38 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +46 -0
- data/LICENCE +20 -0
- data/README.rdoc +131 -0
- data/Rakefile +6 -0
- data/benchmark/compare_state_machines.rb +88 -0
- data/examples/robot.rb +18 -0
- data/examples/test.rb +48 -0
- data/init.rb +1 -0
- data/lib/multiflow/event.rb +38 -0
- data/lib/multiflow/exception.rb +5 -0
- data/lib/multiflow/machine.rb +40 -0
- data/lib/multiflow/persistence/active_record.rb +35 -0
- data/lib/multiflow/persistence/none.rb +21 -0
- data/lib/multiflow/persistence.rb +29 -0
- data/lib/multiflow/railtie.rb +12 -0
- data/lib/multiflow/state.rb +31 -0
- data/lib/multiflow/transition.rb +39 -0
- data/lib/multiflow.rb +121 -0
- data/multiflow.gemspec +24 -0
- data/spec/mongoid.yml +6 -0
- data/spec/orm/activerecord_spec.rb +209 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/stateflow_spec.rb +347 -0
- metadata +131 -0
data/lib/multiflow.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
module Multiflow
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
Multiflow::Persistence.load!(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.persistence
|
12
|
+
@@persistence ||= nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.persistence=(persistence)
|
16
|
+
@@persistence = persistence
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def machines
|
21
|
+
@machines ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def stateflow(&block)
|
25
|
+
@machines ||= []
|
26
|
+
|
27
|
+
machine = Multiflow::Machine.new(&block)
|
28
|
+
@machines << machine
|
29
|
+
|
30
|
+
reader = :"machine_#{machine.state_column}"
|
31
|
+
state = :"current_#{machine.state_column}"
|
32
|
+
mach_ivar = :"@#{reader}"
|
33
|
+
state_ivar = :"@#{state}"
|
34
|
+
|
35
|
+
if respond_to?(reader)
|
36
|
+
raise ArgumentError, "Machine for #{machine.state_column} is already defined"
|
37
|
+
end
|
38
|
+
|
39
|
+
define_method(reader) do
|
40
|
+
self.class.send reader
|
41
|
+
end
|
42
|
+
|
43
|
+
define_singleton_method(reader) do
|
44
|
+
instance_variable_get(mach_ivar)
|
45
|
+
end
|
46
|
+
|
47
|
+
instance_variable_set(mach_ivar, machine)
|
48
|
+
|
49
|
+
define_method(state) do
|
50
|
+
state = instance_variable_get(state_ivar)
|
51
|
+
|
52
|
+
if state.nil?
|
53
|
+
loaded = load_from_persistence(machine)
|
54
|
+
if loaded.nil?
|
55
|
+
state = machine.initial_state
|
56
|
+
else
|
57
|
+
state = machine.states[loaded.to_sym]
|
58
|
+
end
|
59
|
+
|
60
|
+
instance_variable_set(state_ivar, state)
|
61
|
+
end
|
62
|
+
|
63
|
+
state
|
64
|
+
end
|
65
|
+
|
66
|
+
define_method("set_#{state}") do |state, options = {}|
|
67
|
+
save_to_persistence(machine, state.name.to_s, options)
|
68
|
+
instance_variable_set(state_ivar, state)
|
69
|
+
end
|
70
|
+
|
71
|
+
machine.states.values.each do |state|
|
72
|
+
state_name = state.name
|
73
|
+
|
74
|
+
define_method(:"#{state_name}?") do
|
75
|
+
state_name == send(:"current_#{machine.state_column}").name
|
76
|
+
end
|
77
|
+
|
78
|
+
if machine.create_scopes?
|
79
|
+
add_scope(machine, state)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
machine.events.keys.each do |key|
|
84
|
+
define_method(key.to_sym) do
|
85
|
+
fire_event(machine, key, :save => false)
|
86
|
+
end
|
87
|
+
|
88
|
+
define_method(:"#{key}!") do
|
89
|
+
fire_event(machine, key, :save => true)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def machines
|
96
|
+
self.class.machines
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def fire_event(machine, event_name, options = {})
|
102
|
+
event = machine.events[event_name.to_sym]
|
103
|
+
raise Multiflow::NoEventFound.new("No event matches #{event_name}") if event.nil?
|
104
|
+
|
105
|
+
current_state = send :"current_#{machine.state_column}"
|
106
|
+
|
107
|
+
state = event.fire(machine, current_state, self, options)
|
108
|
+
|
109
|
+
unless state.nil?
|
110
|
+
send :"set_current_#{machine.state_column}", state, options
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
require 'multiflow/machine'
|
116
|
+
require 'multiflow/state'
|
117
|
+
require 'multiflow/event'
|
118
|
+
require 'multiflow/transition'
|
119
|
+
require 'multiflow/persistence'
|
120
|
+
require 'multiflow/exception'
|
121
|
+
require 'multiflow/railtie' if defined?(Rails)
|
data/multiflow.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "multiflow"
|
5
|
+
s.version = "1.0.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2")
|
8
|
+
s.authors = ["Ryan Oberholzer", "Sergey Gridasov"]
|
9
|
+
s.date = "2013-03-06"
|
10
|
+
s.description = "State machine that allows dynamic transitions for business workflows"
|
11
|
+
s.email = ["ryan@platform45.com", "grindars@gmail.com"]
|
12
|
+
s.files = `git ls-files`.split($/)
|
13
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
14
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.homepage = "https://github.com/grindars/multiflow"
|
17
|
+
s.rubygems_version = "2.0.0"
|
18
|
+
s.summary = "State machine that allows dynamic transitions for business workflows"
|
19
|
+
|
20
|
+
s.add_runtime_dependency("activesupport", ">= 0")
|
21
|
+
s.add_development_dependency("rspec", ">= 2.0.0")
|
22
|
+
s.add_development_dependency("activerecord", ">= 0")
|
23
|
+
s.add_development_dependency("sqlite3-ruby", ">= 0")
|
24
|
+
end
|
data/spec/mongoid.yml
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
Multiflow.persistence = :active_record
|
5
|
+
|
6
|
+
# change this if sqlite is unavailable
|
7
|
+
dbconfig = {
|
8
|
+
:adapter => 'sqlite3',
|
9
|
+
:database => ':memory:'
|
10
|
+
}
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection(dbconfig)
|
13
|
+
ActiveRecord::Migration.verbose = false
|
14
|
+
|
15
|
+
class TestMigration < ActiveRecord::Migration
|
16
|
+
def self.up
|
17
|
+
create_table :active_record_robots, :force => true do |t|
|
18
|
+
t.column :state, :string
|
19
|
+
t.column :name, :string
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :active_record_no_scope_robots, :force => true do |t|
|
23
|
+
t.column :state, :string
|
24
|
+
t.column :name, :string
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.down
|
29
|
+
drop_table :active_record_robots
|
30
|
+
drop_table :active_record_no_scope_robots
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ActiveRecordRobot < ActiveRecord::Base
|
35
|
+
include Multiflow
|
36
|
+
|
37
|
+
stateflow do
|
38
|
+
initial :red
|
39
|
+
|
40
|
+
state :red, :green
|
41
|
+
|
42
|
+
event :change do
|
43
|
+
transitions :from => :red, :to => :green
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class ActiveRecordNoScopeRobot < ActiveRecord::Base
|
49
|
+
include Multiflow
|
50
|
+
|
51
|
+
stateflow do
|
52
|
+
create_scopes false
|
53
|
+
initial :red
|
54
|
+
|
55
|
+
state :red, :green
|
56
|
+
|
57
|
+
event :change do
|
58
|
+
transitions :from => :red, :to => :green
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ActiveRecordStateColumnSetRobot < ActiveRecord::Base
|
64
|
+
include Multiflow
|
65
|
+
|
66
|
+
stateflow do
|
67
|
+
state_column :status
|
68
|
+
initial :red
|
69
|
+
|
70
|
+
state :red, :green
|
71
|
+
|
72
|
+
event :change do
|
73
|
+
transitions :from => :red, :to => :green
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe Multiflow::Persistence::ActiveRecord do
|
79
|
+
before(:all) { TestMigration.up }
|
80
|
+
after(:all) { TestMigration.down }
|
81
|
+
after { ActiveRecordRobot.delete_all }
|
82
|
+
|
83
|
+
let(:robot) { ActiveRecordRobot.new }
|
84
|
+
|
85
|
+
describe "includes" do
|
86
|
+
it "should include current_state" do
|
87
|
+
robot.respond_to?(:current_state).should be_true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should include current_state=" do
|
91
|
+
robot.respond_to?(:set_current_state).should be_true
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should include save_to_persistence" do
|
95
|
+
robot.respond_to?(:save_to_persistence).should be_true
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should include load_from_persistence" do
|
99
|
+
robot.respond_to?(:load_from_persistence).should be_true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "bang method" do
|
104
|
+
before do
|
105
|
+
@robot = robot
|
106
|
+
@robot.state = "red"
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should call the set_current_state with save being true" do
|
110
|
+
@robot.should_receive(:set_current_state).with(@robot.machine_state.states[:green], {:save=>true})
|
111
|
+
@robot.change!
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should call the setter method for the state column" do
|
115
|
+
@robot.should_receive(:state=).with("green")
|
116
|
+
@robot.change!
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should call save after setting the state column" do
|
120
|
+
@robot.should_receive(:save!)
|
121
|
+
@robot.change!
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should save the record" do
|
125
|
+
@robot.new_record?.should be_true
|
126
|
+
@robot.change!
|
127
|
+
@robot.new_record?.should be_false
|
128
|
+
@robot.reload.state.should == "green"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "non bang method" do
|
133
|
+
before do
|
134
|
+
@robot = robot
|
135
|
+
@robot.state = "red"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should call the set_current_state with save being false" do
|
139
|
+
@robot.should_receive(:set_current_state).with(@robot.machine_state.states[:green], {:save=>false})
|
140
|
+
@robot.change
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should call the setter method for the state column" do
|
144
|
+
@robot.should_receive(:state=).with("green")
|
145
|
+
@robot.change
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should call save after setting the state column" do
|
149
|
+
@robot.should_not_receive(:save)
|
150
|
+
@robot.change
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should not save the record" do
|
154
|
+
@robot.new_record?.should be_true
|
155
|
+
@robot.change
|
156
|
+
@robot.new_record?.should be_true
|
157
|
+
@robot.state.should == "green"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
it "Make sure stateflow saves the initial state if no state is set" do
|
162
|
+
@robot3 = robot
|
163
|
+
|
164
|
+
@robot3.save
|
165
|
+
@robot3.reload
|
166
|
+
|
167
|
+
@robot3.state.should == "red"
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "load from persistence" do
|
171
|
+
before do
|
172
|
+
@robot = robot
|
173
|
+
@robot.state = "green"
|
174
|
+
@robot.name = "Bottie"
|
175
|
+
@robot.save
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should call the load_from_persistence method" do
|
179
|
+
@robot.reload
|
180
|
+
@robot.should_receive(:load_from_persistence)
|
181
|
+
|
182
|
+
@robot.current_state
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "scopes" do
|
187
|
+
it "should be added for each state" do
|
188
|
+
ActiveRecordRobot.should respond_to(:red)
|
189
|
+
ActiveRecordRobot.should respond_to(:green)
|
190
|
+
end
|
191
|
+
it "should be added for each state when the state column is not 'state'" do
|
192
|
+
ActiveRecordStateColumnSetRobot.should respond_to(:red)
|
193
|
+
ActiveRecordStateColumnSetRobot.should respond_to(:green)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should not be added for each state" do
|
197
|
+
ActiveRecordNoScopeRobot.should_not respond_to(:red)
|
198
|
+
ActiveRecordNoScopeRobot.should_not respond_to(:green)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should behave like Mongoid scopes" do
|
202
|
+
2.times { ActiveRecordRobot.create(:state => "red") }
|
203
|
+
3.times { ActiveRecordRobot.create(:state => "green") }
|
204
|
+
ActiveRecordRobot.red.count.should == 2
|
205
|
+
ActiveRecordRobot.green.count.should == 3
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,347 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
Multiflow.persistence = :none
|
4
|
+
|
5
|
+
class Robot
|
6
|
+
include Multiflow
|
7
|
+
|
8
|
+
stateflow do
|
9
|
+
initial :green
|
10
|
+
|
11
|
+
state :green, :yellow, :red
|
12
|
+
|
13
|
+
event :change_color do
|
14
|
+
transitions :from => :green, :to => :yellow
|
15
|
+
transitions :from => :yellow, :to => :red
|
16
|
+
transitions :from => :red, :to => :green
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Car
|
22
|
+
include Multiflow
|
23
|
+
|
24
|
+
stateflow do
|
25
|
+
initial :parked
|
26
|
+
|
27
|
+
state :parked do
|
28
|
+
enter do
|
29
|
+
"Entering parked"
|
30
|
+
end
|
31
|
+
|
32
|
+
exit do
|
33
|
+
"Exiting parked"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
state :driving do
|
38
|
+
enter do
|
39
|
+
"Entering parked"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
event :drive do
|
44
|
+
transitions :from => :parked, :to => :driving
|
45
|
+
end
|
46
|
+
|
47
|
+
event :park do
|
48
|
+
transitions :from => :driving, :to => :parked
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Bob
|
54
|
+
include Multiflow
|
55
|
+
|
56
|
+
stateflow do
|
57
|
+
state :yellow, :red, :purple
|
58
|
+
|
59
|
+
event :change_hair_color do
|
60
|
+
transitions :from => :purple, :to => :yellow
|
61
|
+
transitions :from => :yellow, :to => :red
|
62
|
+
transitions :from => :red, :to => :purple
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Dater
|
68
|
+
include Multiflow
|
69
|
+
|
70
|
+
stateflow do
|
71
|
+
state :single, :dating, :married
|
72
|
+
|
73
|
+
event :take_out do
|
74
|
+
transitions :from => :single, :to => :dating
|
75
|
+
end
|
76
|
+
|
77
|
+
event :gift do
|
78
|
+
transitions :from => :dating, :to => [:single, :married], :decide => :girls_mood?
|
79
|
+
end
|
80
|
+
|
81
|
+
event :blank_decision do
|
82
|
+
transitions :from => :single, :to => [:single, :married], :decide => :girls_mood?
|
83
|
+
end
|
84
|
+
|
85
|
+
event :fail do
|
86
|
+
transitions :from => :dating, :to => [:single, :married]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def girls_mood?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Priority
|
95
|
+
include Multiflow
|
96
|
+
|
97
|
+
stateflow do
|
98
|
+
initial :medium
|
99
|
+
state :low, :medium, :high
|
100
|
+
|
101
|
+
event :low do
|
102
|
+
transitions :from => any, :to => :low
|
103
|
+
end
|
104
|
+
|
105
|
+
event :medium do
|
106
|
+
transitions :from => any, :to => :medium
|
107
|
+
end
|
108
|
+
|
109
|
+
event :high do
|
110
|
+
transitions :from => any, :to => :high
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Stater
|
116
|
+
include Multiflow
|
117
|
+
|
118
|
+
stateflow do
|
119
|
+
initial :bill
|
120
|
+
|
121
|
+
state :bob, :bill
|
122
|
+
|
123
|
+
event :lolcats do
|
124
|
+
transitions :from => :bill, :to => :bob
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe Multiflow do
|
130
|
+
describe "class methods" do
|
131
|
+
it "should respond to stateflow block to setup the intial stateflow" do
|
132
|
+
Robot.should respond_to(:stateflow)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should respond to the machine attr accessor" do
|
136
|
+
Robot.should respond_to(:machine_state)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should return all active persistence layers" do
|
140
|
+
Set[*Multiflow::Persistence.active.should] == Set[:active_record, :mongo_mapper, :mongoid, :none]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "instance methods" do
|
145
|
+
before(:each) do
|
146
|
+
@r = Robot.new
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should respond to current state" do
|
150
|
+
@r.should respond_to(:current_state)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should respond to the current state setter" do
|
154
|
+
@r.should respond_to(:set_current_state)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should respond to the current machine" do
|
158
|
+
@r.should respond_to(:machine_state)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should respond to load from persistence" do
|
162
|
+
@r.should respond_to(:load_from_persistence)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should respond to save to persistence" do
|
166
|
+
@r.should respond_to(:save_to_persistence)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "initial state" do
|
171
|
+
it "should set the initial state" do
|
172
|
+
robot = Robot.new
|
173
|
+
robot.current_state.name.should == :green
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should return true for green?" do
|
177
|
+
robot = Robot.new
|
178
|
+
robot.green?.should be_true
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should return false for yellow?" do
|
182
|
+
robot = Robot.new
|
183
|
+
robot.yellow?.should be_false
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should return false for red?" do
|
187
|
+
robot = Robot.new
|
188
|
+
robot.red?.should be_false
|
189
|
+
end
|
190
|
+
|
191
|
+
it "should set the initial state to the first state set" do
|
192
|
+
bob = Bob.new
|
193
|
+
bob.current_state.name.should == :yellow
|
194
|
+
bob.yellow?.should be_true
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it "robot class should contain red, yellow and green states" do
|
199
|
+
robot = Robot.new
|
200
|
+
robot.machine_state.states.keys.should include(:red, :yellow, :green)
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "firing events" do
|
204
|
+
let(:robot) { Robot.new }
|
205
|
+
let(:event) { :change_color }
|
206
|
+
subject { robot }
|
207
|
+
|
208
|
+
it "should raise an exception if the event does not exist" do
|
209
|
+
lambda { robot.send(:fire_event, robot.machine_state, :fake) }.should raise_error(Multiflow::NoEventFound)
|
210
|
+
end
|
211
|
+
|
212
|
+
shared_examples_for "an entity supporting state changes" do
|
213
|
+
context "when firing" do
|
214
|
+
after(:each) { subject.send(event_method) }
|
215
|
+
it "should call the fire method on event" do
|
216
|
+
subject.machine_state.events[event].should_receive(:fire)
|
217
|
+
end
|
218
|
+
|
219
|
+
it "should call the fire_event method" do
|
220
|
+
subject.should_receive(:fire_event).with(subject.machine_state, event, {:save=>persisted})
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should update the current state" do
|
225
|
+
subject.current_state.name.should == :green
|
226
|
+
subject.send(event_method)
|
227
|
+
subject.current_state.name.should == :yellow
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "bang event" do
|
232
|
+
let(:event_method) { :change_color! }
|
233
|
+
let(:persisted) { true }
|
234
|
+
it_should_behave_like "an entity supporting state changes"
|
235
|
+
end
|
236
|
+
|
237
|
+
describe "non-bang event" do
|
238
|
+
let(:event_method) { :change_color }
|
239
|
+
let(:persisted) { false }
|
240
|
+
it_should_behave_like "an entity supporting state changes"
|
241
|
+
end
|
242
|
+
|
243
|
+
describe "before filters" do
|
244
|
+
before(:each) do
|
245
|
+
@car = Car.new
|
246
|
+
end
|
247
|
+
|
248
|
+
it "should call the exit state before filter on the exiting old state" do
|
249
|
+
@car.machine_state.states[:parked].should_receive(:execute_action).with(:exit, @car)
|
250
|
+
@car.drive!
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should call the enter state before filter on the entering new state" do
|
254
|
+
@car.machine_state.states[:driving].should_receive(:execute_action).with(:enter, @car)
|
255
|
+
@car.drive!
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
describe "persistence" do
|
261
|
+
it "should attempt to persist the new state and the name should be a string" do
|
262
|
+
robot = Robot.new
|
263
|
+
robot.should_receive(:save_to_persistence).with(robot.machine_state, "yellow", {:save=>true})
|
264
|
+
robot.change_color!
|
265
|
+
end
|
266
|
+
|
267
|
+
it "should attempt to read the initial state from the persistence" do
|
268
|
+
robot = Robot.new
|
269
|
+
|
270
|
+
def robot.load_from_persistence(machine)
|
271
|
+
:red
|
272
|
+
end
|
273
|
+
|
274
|
+
robot.current_state.name.should == :red
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
describe "dynamic to transitions" do
|
279
|
+
it "should raise an error without any decide argument" do
|
280
|
+
date = Dater.new
|
281
|
+
|
282
|
+
def date.load_from_persistence(machine)
|
283
|
+
:dating
|
284
|
+
end
|
285
|
+
|
286
|
+
lambda { date.fail! }.should raise_error(Multiflow::IncorrectTransition)
|
287
|
+
end
|
288
|
+
|
289
|
+
it "should raise an error if the decision method does not return a valid state" do
|
290
|
+
date = Dater.new
|
291
|
+
|
292
|
+
def date.girls_mood?
|
293
|
+
:lol
|
294
|
+
end
|
295
|
+
|
296
|
+
lambda { date.blank_decision! }.should raise_error(Multiflow::NoStateFound, "Decision did not return a state that was set in the 'to' argument")
|
297
|
+
end
|
298
|
+
|
299
|
+
it "should raise an error if the decision method returns blank/nil" do
|
300
|
+
date = Dater.new
|
301
|
+
|
302
|
+
def date.girls_mood?
|
303
|
+
end
|
304
|
+
|
305
|
+
lambda { date.blank_decision! }.should raise_error(Multiflow::NoStateFound, "Decision did not return a state that was set in the 'to' argument")
|
306
|
+
end
|
307
|
+
|
308
|
+
it "should calculate the decide block or method and transition to the correct state" do
|
309
|
+
date = Dater.new
|
310
|
+
|
311
|
+
def date.load_from_persistence(machine)
|
312
|
+
:dating
|
313
|
+
end
|
314
|
+
|
315
|
+
date.current_state.name.should == :dating
|
316
|
+
|
317
|
+
def date.girls_mood?
|
318
|
+
:single
|
319
|
+
end
|
320
|
+
|
321
|
+
date.gift!
|
322
|
+
|
323
|
+
date.current_state.name.should == :single
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe "transitions from any" do
|
328
|
+
it "should properly change state" do
|
329
|
+
priority = Priority.new
|
330
|
+
priority.low!
|
331
|
+
priority.should be_low
|
332
|
+
priority.medium!
|
333
|
+
priority.should be_medium
|
334
|
+
priority.high!
|
335
|
+
priority.should be_high
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
describe 'sub-classes of Multiflow\'d classes' do
|
340
|
+
subject { Class.new(Stater) }
|
341
|
+
|
342
|
+
it 'shouldn\'t raise an exception' do
|
343
|
+
expect { subject.new.lolcats! }.not_to raise_exception
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
end
|