edge-state-machine 0.0.1

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.
@@ -0,0 +1,248 @@
1
+ require 'active_record_helper'
2
+
3
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
4
+
5
+ class TrafficLight < ActiveRecord::Base
6
+ include ActiveRecord::EdgeStateMachine
7
+
8
+ state_machine do
9
+ state :off
10
+
11
+ state :red
12
+ state :green
13
+ state :yellow
14
+
15
+ event :red_on do
16
+ transitions :to => :red, :from => [:yellow]
17
+ end
18
+
19
+ event :green_on do
20
+ transitions :to => :green, :from => [:red]
21
+ end
22
+
23
+ event :yellow_on do
24
+ transitions :to => :yellow, :from => [:green]
25
+ end
26
+
27
+ event :reset do
28
+ transitions :to => :red, :from => [:off]
29
+ end
30
+ end
31
+ end
32
+
33
+ class ProtectedTrafficLight < TrafficLight
34
+ attr_protected :state
35
+ end
36
+
37
+ class ValidatingTrafficLight < TrafficLight
38
+ validate {|t| errors.add(:base, 'This TrafficLight will never validate after creation') unless t.new_record? }
39
+ end
40
+
41
+ class ConditionalValidatingTrafficLight < TrafficLight
42
+ validates :name, :presence => true, :length => { :within => 20..40 }, :confirmation => true, :if => :red?
43
+ end
44
+
45
+
46
+ class Order < ActiveRecord::Base
47
+ include ActiveRecord::EdgeStateMachine
48
+
49
+ state_machine do
50
+ state :opened
51
+ state :placed
52
+ state :paid
53
+ state :prepared
54
+ state :delivered
55
+ state :cancelled
56
+
57
+ # no timestamp col is being specified here - should be ignored
58
+ event :place do
59
+ transitions :from => :opened, :to => :placed
60
+ end
61
+
62
+ # should set paid_at timestamp
63
+ event :pay, :timestamp => true do
64
+ transitions :from => :placed, :to => :paid
65
+ end
66
+
67
+ # should set prepared_on
68
+ event :prepare, :timestamp => true do
69
+ transitions :from => :paid, :to => :prepared
70
+ end
71
+
72
+ # should set dispatched_at
73
+ event :deliver, :timestamp => "dispatched_at" do
74
+ transitions :from => :prepared, :to => :delivered
75
+ end
76
+
77
+ # should set cancellation_date
78
+ event :cancel, :timestamp => :cancellation_date do
79
+ transitions :from => [:placed, :paid, :prepared], :to => :cancelled
80
+ end
81
+
82
+ # should raise an exception as there is no timestamp col
83
+ event :reopen, :timestamp => true do
84
+ transitions :from => :cancelled, :to => :opened
85
+ end
86
+
87
+ end
88
+ end
89
+
90
+ describe "active record state machine" do
91
+
92
+ context "existing active record" do
93
+ before do
94
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
95
+ ActiveRecord::Migration.verbose = false
96
+ CreateTrafficLights.migrate(:up)
97
+ @light = TrafficLight.create!
98
+ end
99
+
100
+ it "should have an initial state" do
101
+ @light.off?.should == true
102
+ @light.current_state.should == :off
103
+ end
104
+
105
+ it "should go to a valid state on transition" do
106
+ @light.reset
107
+ @light.red?.should == true
108
+ @light.current_state.should == :red
109
+
110
+ @light.green_on
111
+ @light.green?.should == true
112
+ @light.current_state.should == :green
113
+ end
114
+
115
+ it "should not persist state on transition" do
116
+ @light.reset
117
+ @light.current_state.should == :red
118
+ @light.reload
119
+ @light.state.should == "off"
120
+ end
121
+
122
+ it "should persists state on transition" do
123
+ @light.reset!
124
+ @light.current_state.should == :red
125
+ @light.reload
126
+ @light.state.should == "red"
127
+ end
128
+
129
+ it "should raise error on transition to an invalid state" do
130
+ expect { @light.yellow_on }.should raise_error EdgeStateMachine::InvalidTransition
131
+ @light.current_state.should == :off
132
+ end
133
+
134
+ it "should persist state when state is protected on transition" do
135
+ protected_light = ProtectedTrafficLight.create!
136
+ protected_light.reset!
137
+ protected_light.current_state.should == :red
138
+ protected_light.reload
139
+ protected_light.state.should == "red"
140
+ end
141
+
142
+ it "should not validate when try transition with wrong state " do
143
+ for s in @light.class.state_machine.states
144
+ @light.state = s.name
145
+ @light.valid?.should == true
146
+ end
147
+ @light.state = "invalid_one"
148
+ @light.valid?.should_not == true
149
+ end
150
+
151
+ it "should raise exception when model validation fails on transition" do
152
+ validating_light = ValidatingTrafficLight.create!
153
+ expect {validating_light.reset!}.should raise_error ActiveRecord::RecordInvalid
154
+ validating_light.off?.should == true
155
+ end
156
+
157
+ #it "should state query method used in a validation condition" do
158
+ #validating_light = ConditionalValidatingTrafficLight.create!
159
+ #expect {validating_light.reset!}.should raise_error ActiveRecord::RecordInvalid
160
+ #validating_light.off?.should == true
161
+ #end
162
+
163
+ it "should reload the model when current state resets" do
164
+ @light.reset
165
+ @light.red?.should == true
166
+ @light.update_attribute(:state, 'green')
167
+ @light.reload.green?.should == false # reloaded state should come from instance variable not from database
168
+ # because the state can be changed without persist
169
+ end
170
+ end
171
+
172
+ context "new active record" do
173
+ before do
174
+ @light = TrafficLight.new
175
+ end
176
+
177
+ it "should have the initial state set" do
178
+ @light.current_state.should == :off
179
+ end
180
+ end
181
+
182
+ context "timestamp" do
183
+ before do
184
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
185
+ ActiveRecord::Migration.verbose = false
186
+ CreateOrders.migrate(:up)
187
+ end
188
+
189
+ def create_order(state = nil)
190
+ Order.create! order_number: 234, state: state
191
+ end
192
+
193
+ # control case, no timestamp has been set so we should expect default behaviour
194
+ it "should not raise any exceptions when moving to placed" do
195
+ @order = create_order
196
+ expect { @order.place! }.should_not raise_error
197
+ @order.state.should == "placed"
198
+ end
199
+
200
+ it "should set paid_at when moving to paid" do
201
+ @order = create_order(:placed)
202
+ @order.pay!
203
+ @order.reload
204
+ @order.paid_at.should_not be_nil
205
+ end
206
+
207
+ it "should set prepared_on when moving to prepared" do
208
+ @order = create_order(:paid)
209
+ @order.prepare!
210
+ @order.reload
211
+ @order.prepared_on.should_not be_nil
212
+ end
213
+
214
+ it "should set dispatched_at when moving to delivered" do
215
+ @order = create_order(:prepared)
216
+ @order.deliver!
217
+ @order.reload
218
+ @order.dispatched_at.should_not be_nil
219
+ end
220
+
221
+ it "should set cancellation_date when moving to cancelled" do
222
+ @order = create_order(:placed)
223
+ @order.cancel!
224
+ @order.reload
225
+ @order.cancellation_date.should_not be_nil
226
+ end
227
+
228
+ it "should raise an exception as there is no attribute when moving to reopened" do
229
+ @order = create_order(:cancelled)
230
+ expect { @order.re_open! }.should raise_error NoMethodError
231
+ @order.reload
232
+ end
233
+
234
+ it "should raise an exception when passing an invalid value to timestamp options" do
235
+ expect {
236
+ class Order < ActiveRecord::Base
237
+ include ActiveRecord::EdgeStateMachine
238
+ state_machine do
239
+ event :replace, timestamp: 1 do
240
+ transitions :from => :prepared, :to => :placed
241
+ end
242
+ end
243
+ end
244
+ }.should raise_error ArgumentError
245
+ end
246
+
247
+ end
248
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ class ArgumentsTestSubject
4
+ include EdgeStateMachine
5
+ attr_accessor :date
6
+
7
+ state_machine do
8
+ state :initial
9
+ state :opened
10
+
11
+ event :open do
12
+ transitions :from => :initial, :to => :opened, :on_transition => :update_date
13
+ end
14
+ end
15
+
16
+ def update_date(date = Date.today)
17
+ self.date = date
18
+ end
19
+ end
20
+
21
+ def new_event
22
+ @event = EdgeStateMachine::Event.new(nil, @state_name, {:success => @success}) do
23
+ transitions :to => :closed, :from => [:open, :received]
24
+ end
25
+ end
26
+
27
+ describe EdgeStateMachine::Event do
28
+
29
+ before do
30
+ @state_name = :close_order
31
+ @success = :success_callback
32
+ @event = EdgeStateMachine::Event.new(nil, @state_name, {:success => @success}) do
33
+ transitions :to => :closed, :from => [:open, :received]
34
+ end
35
+ end
36
+
37
+ it "should set the name" do
38
+ @state_name.should == @event.name
39
+ end
40
+
41
+ it "should set the success option" do
42
+ @success.should == @event.success
43
+ end
44
+
45
+ it "should create StateTransitions" do
46
+ EdgeStateMachine::StateTransition.should_receive(:new).with(:to => :closed, :from => :open)
47
+ EdgeStateMachine::StateTransition.should_receive(:new).with(:to => :closed, :from => :received)
48
+ new_event
49
+ end
50
+
51
+ describe "event arguments" do
52
+ it "should pass arguments to transition method" do
53
+ subject = ArgumentsTestSubject.new
54
+ subject.current_state.should == :initial
55
+ subject.open!(Date.strptime('2001-02-03', '%Y-%m-%d'))
56
+ subject.current_state.should == :opened
57
+ subject.date.should == Date.strptime('2001-02-03', '%Y-%m-%d')
58
+ end
59
+ end
60
+
61
+ describe "events being fired" do
62
+ it "should raise an EdgeStateMachine::InvalidTransition error if the transitions are empty" do
63
+ event = EdgeStateMachine::Event.new(nil, :event)
64
+ expect {event.fire(nil)}.should raise_error EdgeStateMachine::InvalidTransition
65
+ end
66
+
67
+ it "should return the state of the first matching transition it finds" do
68
+ event = EdgeStateMachine::Event.new(nil, :event) do
69
+ transitions :to => :closed, :from => [:open, :received]
70
+ end
71
+ obj = mock
72
+ obj.stub!(:current_state).and_return(:open)
73
+ event.fire(obj).should == :closed
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ class MachineTestSubject
4
+ include EdgeStateMachine
5
+
6
+ state_machine do
7
+ state :open
8
+ state :closed
9
+ end
10
+
11
+ state_machine :initial => :foo do
12
+ event :shutdown do
13
+ transitions :from => :open, :to => :closed
14
+ end
15
+
16
+ event :timeout do
17
+ transitions :from => :open, :to => :closed
18
+ end
19
+ end
20
+
21
+ state_machine :extra, :initial => :bar do
22
+ end
23
+ end
24
+
25
+ describe EdgeStateMachine::Machine do
26
+
27
+ it 'should allow reuse of existing machines' do
28
+ MachineTestSubject.state_machines.size.should == 2
29
+ end
30
+
31
+ it "should set #initial_state from :initial option" do
32
+ MachineTestSubject.state_machine(:extra).initial_state.should == :bar
33
+ end
34
+
35
+ it "should accesse non-default state machine" do
36
+ MachineTestSubject.state_machine(:extra).class.should == EdgeStateMachine::Machine
37
+ end
38
+
39
+ it "should find event for given state" do
40
+ events = MachineTestSubject.state_machine.events_for(:open)
41
+ events.should be_include(:shutdown)
42
+ events.should be_include(:timeout)
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ class CreateOrders < ActiveRecord::Migration
2
+ def self.up
3
+ create_table(:orders, force: true) do |t|
4
+ t.string :state
5
+ t.string :order_number
6
+ t.datetime :paid_at
7
+ t.datetime :prepared_on
8
+ t.datetime :dispatched_at
9
+ t.date :cancellation_date
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ class CreateTrafficLights < ActiveRecord::Migration
2
+ def self.up
3
+ create_table(:traffic_lights, force: true) do |t|
4
+ t.string :state
5
+ t.string :name
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'mongoid'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require 'mongoid/edge-state-machine'
7
+
8
+ Mongoid.configure do |config|
9
+ config.master = Mongo::Connection.new.db('edge_state_machine_test')
10
+ config.allow_dynamic_fields = false
11
+ end
12
+
@@ -0,0 +1,251 @@
1
+ require 'mongoid_helper'
2
+
3
+ class MongoTrafficLight
4
+ include Mongoid::Document
5
+ include Mongoid::EdgeStateMachine
6
+ field :state
7
+
8
+ state_machine do
9
+ state :off
10
+
11
+ state :red
12
+ state :green
13
+ state :yellow
14
+
15
+ event :red_on do
16
+ transitions :to => :red, :from => [:yellow]
17
+ end
18
+
19
+ event :green_on do
20
+ transitions :to => :green, :from => [:red]
21
+ end
22
+
23
+ event :yellow_on do
24
+ transitions :to => :yellow, :from => [:green]
25
+ end
26
+
27
+ event :reset do
28
+ transitions :to => :red, :from => [:off]
29
+ end
30
+ end
31
+ end
32
+
33
+ class MongoProtectedTrafficLight < MongoTrafficLight
34
+ attr_protected :state
35
+ end
36
+
37
+ class MongoValidatingTrafficLight < MongoTrafficLight
38
+ validate {|t| errors.add(:base, 'This TrafficLight will never validate after creation') unless t.new_record? }
39
+ end
40
+
41
+ class MongoConditionalValidatingTrafficLight < MongoTrafficLight
42
+ validates :name, :presence => true, :length => { :within => 20..40 }, :confirmation => true, :if => :red?
43
+ end
44
+
45
+ class MongoOrder
46
+ include Mongoid::Document
47
+ include Mongoid::EdgeStateMachine
48
+
49
+ field :state, :type => String
50
+ field :order_number, :type => Integer
51
+ field :paid_at, :type => DateTime
52
+ field :prepared_on, :type => DateTime
53
+ field :dispatched_at, :type => DateTime
54
+ field :cancellation_date, :type => Date
55
+
56
+ state_machine do
57
+ state :opened
58
+ state :placed
59
+ state :paid
60
+ state :prepared
61
+ state :delivered
62
+ state :cancelled
63
+
64
+ # no timestamp col is being specified here - should be ignored
65
+ event :place do
66
+ transitions :from => :opened, :to => :placed
67
+ end
68
+
69
+ # should set paid_at timestamp
70
+ event :pay, :timestamp => true do
71
+ transitions :from => :placed, :to => :paid
72
+ end
73
+
74
+ # should set prepared_on
75
+ event :prepare, :timestamp => true do
76
+ transitions :from => :paid, :to => :prepared
77
+ end
78
+
79
+ # should set dispatched_at
80
+ event :deliver, :timestamp => "dispatched_at" do
81
+ transitions :from => :prepared, :to => :delivered
82
+ end
83
+
84
+ # should set cancellation_date
85
+ event :cancel, :timestamp => :cancellation_date do
86
+ transitions :from => [:placed, :paid, :prepared], :to => :cancelled
87
+ end
88
+
89
+ # should raise an exception as there is no timestamp col
90
+ event :reopen, :timestamp => true do
91
+ transitions :from => :cancelled, :to => :opened
92
+ end
93
+
94
+ end
95
+ end
96
+
97
+ describe "mongoid state machine" do
98
+
99
+ context "existing mongo document" do
100
+ before do
101
+ Mongoid.master.collections.reject { |c| c.name =~ /^system\./ }.each(&:drop)
102
+ @light = MongoTrafficLight.create!
103
+ end
104
+
105
+ it "should have an initial state" do
106
+ @light.off?.should == true
107
+ @light.current_state.should == :off
108
+ end
109
+
110
+ it "should go to a valid state on transition" do
111
+ @light.reset
112
+ @light.red?.should == true
113
+ @light.current_state.should == :red
114
+
115
+ @light.green_on
116
+ @light.green?.should == true
117
+ @light.current_state.should == :green
118
+ end
119
+
120
+ it "should not persist state on transition" do
121
+ @light.reset
122
+ @light.current_state.should == :red
123
+ @light.reload
124
+ @light.state.should == "off"
125
+ end
126
+
127
+ it "should persists state on transition" do
128
+ @light.reset!
129
+ @light.current_state.should == :red
130
+ @light.reload
131
+ @light.state.should == "red"
132
+ end
133
+
134
+ it "should raise error on transition to an invalid state" do
135
+ expect { @light.yellow_on }.should raise_error EdgeStateMachine::InvalidTransition
136
+ @light.current_state.should == :off
137
+ end
138
+
139
+ it "should persist state when state is protected on transition" do
140
+ protected_light = MongoProtectedTrafficLight.create!
141
+ protected_light.reset!
142
+ protected_light.current_state.should == :red
143
+ protected_light.reload
144
+ protected_light.state.should == "red"
145
+ end
146
+
147
+ it "should not validate when try transition with wrong state " do
148
+ for s in @light.class.state_machine.states
149
+ @light.state = s.name
150
+ @light.valid?.should == true
151
+ end
152
+ @light.state = "invalid_one"
153
+ @light.valid?.should_not == true
154
+ end
155
+
156
+ it "should raise exception when model validation fails on transition" do
157
+ validating_light = MongoValidatingTrafficLight.create!
158
+ expect {validating_light.reset!}.should raise_error Mongoid::Errors::Validations
159
+ end
160
+
161
+ #it "should state query method used in a validation condition" do
162
+ #validating_light = MongoConditionalValidatingTrafficLight.create!
163
+ #expect {validating_light.reset!}.should raise_error Mongoid::RecordInvalid
164
+ #validating_light.off?.should == true
165
+ #end
166
+
167
+ it "should reload the model when current state resets" do
168
+ @light.reset
169
+ @light.red?.should == true
170
+ @light.update_attribute(:state, 'green')
171
+ @light.reload.green?.should == false # reloaded state should come from instance variable not from database
172
+ # because the state can be changed without persist
173
+ end
174
+ end
175
+
176
+ context "new active record" do
177
+ before do
178
+ @light = MongoTrafficLight.new
179
+ end
180
+
181
+ it "should have the initial state set" do
182
+ @light.current_state.should == :off
183
+ end
184
+ end
185
+
186
+ context "timestamp" do
187
+ before do
188
+ Mongoid.master.collections.reject { |c| c.name =~ /^system\./ }.each(&:drop)
189
+ end
190
+
191
+ def create_order(state = nil)
192
+ MongoOrder.create! order_number: 234, state: state
193
+ end
194
+
195
+ # control case, no timestamp has been set so we should expect default behaviour
196
+ it "should not raise any exceptions when moving to placed" do
197
+ @order = create_order
198
+ expect { @order.place! }.should_not raise_error
199
+ @order.state.should == "placed"
200
+ end
201
+
202
+ it "should set paid_at when moving to paid" do
203
+ @order = create_order(:placed)
204
+ @order.pay!
205
+ @order.reload
206
+ @order.paid_at.should_not be_nil
207
+ end
208
+
209
+ it "should set prepared_on when moving to prepared" do
210
+ @order = create_order(:paid)
211
+ @order.prepare!
212
+ @order.reload
213
+ @order.prepared_on.should_not be_nil
214
+ end
215
+
216
+ it "should set dispatched_at when moving to delivered" do
217
+ @order = create_order(:prepared)
218
+ @order.deliver!
219
+ @order.reload
220
+ @order.dispatched_at.should_not be_nil
221
+ end
222
+
223
+ it "should set cancellation_date when moving to cancelled" do
224
+ @order = create_order(:placed)
225
+ @order.cancel!
226
+ @order.reload
227
+ @order.cancellation_date.should_not be_nil
228
+ end
229
+
230
+ it "should raise an exception as there is no attribute when moving to reopened" do
231
+ @order = create_order(:cancelled)
232
+ expect { @order.re_open! }.should raise_error NoMethodError
233
+ @order.reload
234
+ end
235
+
236
+ it "should raise an exception when passing an invalid value to timestamp options" do
237
+ expect {
238
+ class MongoOrder
239
+ include Mongoid::Document
240
+ include Mongoid::EdgeStateMachine
241
+
242
+ state_machine do
243
+ event :replace, timestamp: 1 do
244
+ transitions :from => :prepared, :to => :placed
245
+ end
246
+ end
247
+ end
248
+ }.should raise_error ArgumentError
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'edge-state-machine'
4
+
5
+ RSpec.configure do |config|
6
+ # some (optional) config here
7
+ end