edge-state-machine 0.0.2 → 0.0.3

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.
@@ -5,28 +5,73 @@ module Mongoid
5
5
  included do
6
6
  include ::EdgeStateMachine
7
7
  after_initialize :set_initial_state
8
- validates_presence_of :state
8
+ validate :state_variables_presence
9
9
  validate :state_inclusion
10
10
  end
11
11
 
12
+ # The optional options argument is passed to find when reloading so you may
13
+ # do e.g. record.reload(:lock => true) to reload the same record with an
14
+ # exclusive row lock.
15
+ def reload
16
+ super.tap do
17
+ @current_states = {}
18
+ end
19
+ end
20
+
12
21
  protected
13
22
 
14
- def write_state(state_machine, state)
15
- self.state = state.to_s
16
- save!
23
+ def load_from_persistence(machine_name)
24
+ machine = self.class.state_machines[machine_name]
25
+ send machine.persisted_variable_name.to_s
17
26
  end
18
27
 
19
- def read_state(state_machine)
20
- self.state.to_sym
28
+ def save_to_persistence(new_state, machine_name, options = {})
29
+ machine = self.class.state_machines[machine_name]
30
+ send("#{machine.persisted_variable_name}=".to_sym, new_state)
31
+ save! if options[:save]
21
32
  end
22
33
 
23
34
  def set_initial_state
24
- self.state ||= self.class.state_machine.initial_state.to_s
35
+ # set the initial state for each state machine in this class
36
+ self.class.state_machines.keys.each do |machine_name|
37
+ machine = self.class.state_machines[machine_name]
38
+
39
+ if persisted_variable_value(machine.persisted_variable_name).blank?
40
+ if load_from_persistence(machine_name)
41
+ send("#{machine.persisted_variable_name}=".to_sym, load_from_persistence(machine_name))
42
+ else
43
+ send("#{machine.persisted_variable_name}=".to_sym, machine.initial_state_name)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def persisted_variable_value(name)
50
+ send(name.to_s)
51
+ end
52
+
53
+ def state_variables_presence
54
+ # validate that state is in the right set of values
55
+ self.class.state_machines.keys.each do |machine_name|
56
+ machine = self.class.state_machines[machine_name]
57
+ validates_presence_of machine.persisted_variable_name.to_sym
58
+ end
25
59
  end
26
60
 
27
61
  def state_inclusion
28
- unless self.class.state_machine.states.map{|s| s.name.to_s }.include?(self.state.to_s)
29
- self.errors.add(:state, :inclusion, :value => self.state)
62
+ # validate that state is in the right set of values
63
+ self.class.state_machines.keys.each do |machine_name|
64
+ machine = self.class.state_machines[machine_name]
65
+ unless machine.states.keys.include?(persisted_variable_value(machine.persisted_variable_name).to_sym)
66
+ self.errors.add(machine.persisted_variable_name.to_sym, :inclusion, :value => persisted_variable_value(machine.persisted_variable_name))
67
+ end
68
+ end
69
+ end
70
+
71
+ module ClassMethods
72
+ def add_scope(state, machine_name)
73
+ machine = state_machines[machine_name]
74
+ scope state.name, where(machine.persisted_variable_name.to_sym => state.name.to_s)
30
75
  end
31
76
  end
32
77
  end
@@ -3,6 +3,8 @@ require 'active_record'
3
3
  require 'active_support/core_ext/module/aliasing'
4
4
  require 'active_record/migrations/create_orders'
5
5
  require 'active_record/migrations/create_traffic_lights'
6
+ require 'active_record/samples/traffic_light'
7
+ require 'active_record/samples/order'
6
8
 
7
9
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", 'lib'))
8
10
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -1,92 +1,5 @@
1
1
  require 'active_record/active_record_helper'
2
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
3
  describe "active record state machine" do
91
4
 
92
5
  context "existing active record" do
@@ -126,8 +39,14 @@ describe "active record state machine" do
126
39
  @light.state.should == "red"
127
40
  end
128
41
 
42
+ it "should initialize the current state when loaded from database" do
43
+ @light.reset!
44
+ loaded_light = TrafficLight.find_by_id(@light.id)
45
+ loaded_light.current_state.should == :red
46
+ end
47
+
129
48
  it "should raise error on transition to an invalid state" do
130
- expect { @light.yellow_on }.should raise_error EdgeStateMachine::InvalidTransition
49
+ expect { @light.yellow_on }.should raise_error EdgeStateMachine::NoTransitionFound
131
50
  @light.current_state.should == :off
132
51
  end
133
52
 
@@ -140,8 +59,8 @@ describe "active record state machine" do
140
59
  end
141
60
 
142
61
  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
62
+ for s in @light.class.state_machines[:default].states.keys
63
+ @light.state = s
145
64
  @light.valid?.should == true
146
65
  end
147
66
  @light.state = "invalid_one"
@@ -151,21 +70,42 @@ describe "active record state machine" do
151
70
  it "should raise exception when model validation fails on transition" do
152
71
  validating_light = ValidatingTrafficLight.create!
153
72
  expect {validating_light.reset!}.should raise_error ActiveRecord::RecordInvalid
73
+ validating_light.red?.should == false
154
74
  validating_light.off?.should == true
155
75
  end
156
76
 
157
- #it "should state query method used in a validation condition" do
158
- #validating_light = ConditionalValidatingTrafficLight.create!
77
+ it "should state query method used in a validation condition" do
78
+ validating_light = ConditionalValidatingTrafficLight.create!
159
79
  #expect {validating_light.reset!}.should raise_error ActiveRecord::RecordInvalid
160
- #validating_light.off?.should == true
161
- #end
80
+ validating_light.off?.should == true
81
+ validating_light.red?.should == false
82
+ end
162
83
 
163
84
  it "should reload the model when current state resets" do
164
85
  @light.reset
165
86
  @light.red?.should == true
166
87
  @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
88
+ @light.reload.green?.should == true # reloaded state should come from database
89
+ end
90
+
91
+ describe "scopes" do
92
+ it "should be added for each state" do
93
+ TrafficLight.should respond_to(:off)
94
+ TrafficLight.should respond_to(:red)
95
+ end
96
+
97
+ it "should not be added for each state" do
98
+ #TrafficLightNoScope.should_not respond_to(:off)
99
+ #TrafficLightNoScope.should_not respond_to(:red)
100
+ end
101
+
102
+ it "should behave like scopes" do
103
+ 3.times { TrafficLight.create(:state => "off") }
104
+ 3.times { TrafficLight.create(:state => "red") }
105
+ # one was created before
106
+ TrafficLight.off.count.should == 4
107
+ TrafficLight.red.count.should == 3
108
+ end
169
109
  end
170
110
  end
171
111
 
@@ -196,53 +136,5 @@ describe "active record state machine" do
196
136
  expect { @order.place! }.should_not raise_error
197
137
  @order.state.should == "placed"
198
138
  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
139
  end
248
140
  end
@@ -0,0 +1,47 @@
1
+ require 'active_record'
2
+ require 'active_record/edge-state-machine'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
5
+
6
+ class Order < ActiveRecord::Base
7
+ include ActiveRecord::EdgeStateMachine
8
+
9
+ state_machine do
10
+ state :opened
11
+ state :placed
12
+ state :paid
13
+ state :prepared
14
+ state :delivered
15
+ state :cancelled
16
+
17
+ # no timestamp col is being specified here - should be ignored
18
+ event :place do
19
+ transition :from => :opened, :to => :placed
20
+ end
21
+
22
+ # should set paid_at timestamp
23
+ event :pay do
24
+ transition :from => :placed, :to => :paid
25
+ end
26
+
27
+ # should set prepared_on
28
+ event :prepare do
29
+ transition :from => :paid, :to => :prepared
30
+ end
31
+
32
+ # should set dispatched_at
33
+ event :deliver do
34
+ transition :from => :prepared, :to => :delivered
35
+ end
36
+
37
+ # should set cancellation_date
38
+ event :cancel do
39
+ transition :from => [:placed, :paid, :prepared], :to => :cancelled
40
+ end
41
+
42
+ # should raise an exception as there is no timestamp col
43
+ event :reopen do
44
+ transition :from => :cancelled, :to => :opened
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ require 'active_record'
2
+ require 'active_record/edge-state-machine'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
5
+
6
+ class TrafficLight < ActiveRecord::Base
7
+ include ActiveRecord::EdgeStateMachine
8
+
9
+ state_machine do
10
+ create_scopes true
11
+ persisted_to :state
12
+ state :off
13
+
14
+ state :red
15
+ state :green
16
+ state :yellow
17
+
18
+ event :red_on do
19
+ transition :to => :red, :from => [:yellow]
20
+ end
21
+
22
+ event :green_on do
23
+ transition :to => :green, :from => [:red]
24
+ end
25
+
26
+ event :yellow_on do
27
+ transition :to => :yellow, :from => [:green]
28
+ end
29
+
30
+ event :reset do
31
+ transition :to => :red, :from => [:off]
32
+ end
33
+ end
34
+ end
35
+
36
+ class ProtectedTrafficLight < TrafficLight
37
+ attr_protected :state
38
+ end
39
+
40
+ class ValidatingTrafficLight < TrafficLight
41
+ validate {|t| errors.add(:base, 'This TrafficLight will never validate after creation') unless t.new_record? }
42
+ end
43
+
44
+ class ConditionalValidatingTrafficLight < TrafficLight
45
+ validate :name, :presence => true, :length => { :within => 20..40 }, :confirmation => true, :if => :red?
46
+ end
data/spec/event_spec.rb CHANGED
@@ -9,7 +9,7 @@ class ArgumentsTestSubject
9
9
  state :opened
10
10
 
11
11
  event :open do
12
- transitions :from => :initial, :to => :opened, :on_transition => :update_date
12
+ transition :from => :initial, :to => :opened, :on_transition => :update_date
13
13
  end
14
14
  end
15
15
 
@@ -19,8 +19,8 @@ class ArgumentsTestSubject
19
19
  end
20
20
 
21
21
  def new_event
22
- @event = EdgeStateMachine::Event.new(nil, @state_name, {:success => @success}) do
23
- transitions :to => :closed, :from => [:open, :received]
22
+ @event = EdgeStateMachine::Event.new(@state_name, nil) do
23
+ transition :to => :closed, :from => [:open, :received]
24
24
  end
25
25
  end
26
26
 
@@ -29,8 +29,8 @@ describe EdgeStateMachine::Event do
29
29
  before do
30
30
  @state_name = :close_order
31
31
  @success = :success_callback
32
- @event = EdgeStateMachine::Event.new(nil, @state_name, {:success => @success}) do
33
- transitions :to => :closed, :from => [:open, :received]
32
+ @event = EdgeStateMachine::Event.new(@state_name, nil) do
33
+ transition :to => :closed, :from => [:open, :received]
34
34
  end
35
35
  end
36
36
 
@@ -38,13 +38,8 @@ describe EdgeStateMachine::Event do
38
38
  @state_name.should == @event.name
39
39
  end
40
40
 
41
- it "should set the success option" do
42
- @success.should == @event.success
43
- end
44
-
45
41
  it "should create Transitions" do
46
- EdgeStateMachine::Transition.should_receive(:new).with(:to => :closed, :from => :open)
47
- EdgeStateMachine::Transition.should_receive(:new).with(:to => :closed, :from => :received)
42
+ EdgeStateMachine::Transition.should_receive(:new).with(:to => :closed, :from => [:open, :received])
48
43
  new_event
49
44
  end
50
45
 
@@ -52,25 +47,22 @@ describe EdgeStateMachine::Event do
52
47
  it "should pass arguments to transition method" do
53
48
  subject = ArgumentsTestSubject.new
54
49
  subject.current_state.should == :initial
55
- subject.open!(Date.strptime('2001-02-03', '%Y-%m-%d'))
50
+ subject.open!
56
51
  subject.current_state.should == :opened
57
- subject.date.should == Date.strptime('2001-02-03', '%Y-%m-%d')
58
52
  end
59
53
  end
60
54
 
61
55
  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
56
+ before do
57
+ @machine = mock
58
+ @machine.stub!(:name).and_return(:default)
65
59
  end
66
60
 
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
61
+ it "should raise an EdgeStateMachine::NoTransitionFound error if the transitions are empty" do
62
+ event = EdgeStateMachine::Event.new(:event, @machine)
71
63
  obj = mock
72
64
  obj.stub!(:current_state).and_return(:open)
73
- event.fire(obj).should == :closed
65
+ expect {event.fire(obj)}.should raise_error EdgeStateMachine::NoTransitionFound
74
66
  end
75
67
  end
76
68
  end
data/spec/machine_spec.rb CHANGED
@@ -8,17 +8,20 @@ class MachineTestSubject
8
8
  state :closed
9
9
  end
10
10
 
11
- state_machine :initial => :foo do
11
+ state_machine do
12
+ initial_state :open
13
+
12
14
  event :shutdown do
13
- transitions :from => :open, :to => :closed
15
+ transition :from => :open, :to => :closed
14
16
  end
15
17
 
16
18
  event :timeout do
17
- transitions :from => :open, :to => :closed
19
+ transition :from => :open, :to => :closed
18
20
  end
19
21
  end
20
22
 
21
- state_machine :extra, :initial => :bar do
23
+ state_machine :extra do
24
+ initial_state :bar
22
25
  end
23
26
  end
24
27
 
@@ -28,17 +31,11 @@ describe EdgeStateMachine::Machine do
28
31
  MachineTestSubject.state_machines.size.should == 2
29
32
  end
30
33
 
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
34
+ it "should set #initial_state_name from initial_state method" do
35
+ MachineTestSubject.state_machines[:extra].initial_state_name.should == :bar
37
36
  end
38
37
 
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)
38
+ it "should access non-default state machine" do
39
+ MachineTestSubject.state_machines[:extra].class.should == EdgeStateMachine::Machine
43
40
  end
44
41
  end
@@ -1,12 +1,14 @@
1
1
  require 'spec_helper'
2
2
  require 'mongoid'
3
+ require 'mongoid/samples/traffic_light'
4
+ require 'mongoid/samples/order'
3
5
 
4
6
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
7
  $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+
6
9
  require 'mongoid/edge-state-machine'
7
10
 
8
11
  Mongoid.configure do |config|
9
12
  config.master = Mongo::Connection.new.db('edge_state_machine_test')
10
13
  config.allow_dynamic_fields = false
11
- end
12
-
14
+ end