edge-state-machine 0.0.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,10 +1,20 @@
1
1
  = Edge State Machine
2
2
 
3
- Edge state machine wants to be a complete state machine solution.
4
- It offers support for ActiveRecord and Mongoid
3
+ Edge State Machine is a complete state machine solution.
4
+ It offers support for ActiveRecord and Mongoid for persistence.
5
5
 
6
6
  {<img src="https://secure.travis-ci.org/danpersa/edge-state-machine.png"/>}[http://travis-ci.org/danpersa/edge-state-machine]
7
7
 
8
+ == Supported Features
9
+
10
+ * Multiple state machines per class each of them acting independently
11
+ * Find errors in state machine definitions as early as possible
12
+ * Transition guards
13
+ * Multiple actions executed on transitions
14
+ * Multiple actions executed on entering and exiting a state
15
+ * No other dependencies for non-persistent state machines
16
+ * Minimal dependencies for persistent ones
17
+
8
18
  == Installation
9
19
 
10
20
  If you're using Rails + ActiveRecord + Bundler
@@ -57,10 +67,124 @@ If you're using Rails + Mongoid + Bundler
57
67
  end
58
68
  end
59
69
 
60
- = Credits
70
+ == State Machine Examples
71
+
72
+ === Microwave State Machine
73
+
74
+ class Microwave
75
+ state_machine :microwave do # name should be optional, if the name is not present, it should have a default name
76
+ # we give state machines names, so we can pun many machines inside a class
77
+ initial_state :unplugged # initial state should be optional, if the initial state is not present, the initial state will be the first defined state
78
+
79
+ state :unplugged
80
+
81
+ state :plugged
82
+
83
+ state :door_opened do
84
+ enter :light_on # enter should be executed on entering the state
85
+ exit :light_off # exit method should be executed on exiting the state
86
+ end
87
+
88
+ state :door_closed
89
+
90
+ state :started_in_grill_mode do
91
+ enter lambda { |t| p "Entering hate" } # should have support for Procs
92
+ exit :grill_off
93
+ end
94
+
95
+ state :started do
96
+ enter :microwaves_on
97
+ exit :microwaves_off
98
+ end
99
+
100
+ event :plug_in do
101
+ transition :from => :unplugged, :to => :plugged
102
+ end
103
+
104
+ event :open_door do
105
+ transition :from => :plugged, :to => :door_opened
106
+ end
107
+
108
+ event :close_door do
109
+ transition :from => :door_opened, :to => :door_closed,
110
+ :on_transition => :put_food_in_the_microwave # we can put many actions in an Array for the on_transition parameter
111
+ end
112
+
113
+ event :start do
114
+ transition :from => :door_closed, :to => [:started, :started_in_grill_mode],
115
+ :on_transition => :start_spinning_the_food,
116
+ :guard => :grill_button_pressed? # the method grill_button_pressed? should choose the next state
117
+ end
118
+
119
+ event :stop do
120
+ transition :from => [:started, :started_in_grill_mode], :to => :door_closed
121
+ end
122
+ end
123
+ end
124
+
125
+ === Dice State Machine
126
+
127
+ class Dice
128
+
129
+ state_machine do
130
+ state :one
131
+ state :two
132
+ state :three
133
+ state :four
134
+ state :five
135
+ state :six
136
+
137
+ event :roll do
138
+ transition :from => [:one, :two, :three, :four, :five, :six],
139
+ :to => [:one, :two, :three, :four, :five, :six],
140
+ :guard => :roll_result,
141
+ :on_transition => :display_dice_rolling_animation
142
+ end
143
+ end
144
+
145
+ def roll_result
146
+ # return one of the states
147
+ end
148
+
149
+ def display_dice_rolling_animation
150
+ # draw the new position of the dice
151
+ end
152
+ end
153
+
154
+ class User
155
+ state_machine do
156
+ state :pending # first one is initial state
157
+ state :active
158
+ state :blocked # the user in this state can't sign in
159
+
160
+ event :activate do
161
+ transition :from => [:pending], :to => :active, :on_transition => :do_activate
162
+ end
163
+
164
+ event :block do
165
+ transition :from => [:pending, :active], :to => :blocked
166
+ end
167
+ end
168
+ end
169
+
170
+ === Other Examples
171
+
172
+ For other (more complex) examples, please check the following links:
173
+
174
+ * {Examples without Persistence}[https://github.com/danpersa/edge-state-machine/tree/master/spec/non_persistent/samples]
175
+ * {Examples with ActiveRecord}[https://github.com/danpersa/edge-state-machine/tree/master/spec/active_record/samples]
176
+ * {Examples with Mongoid}[https://github.com/danpersa/edge-state-machine/tree/master/spec/mongoid/samples]
177
+
178
+ == Notes
179
+
180
+ For classes with multiple state machines, the state names, machine names must be unique per class.
181
+
182
+ The same thing with the event names.
183
+
184
+ == Credits
61
185
 
62
186
  The gem is based on Rick Olson's code of ActiveModel::StateMachine,
63
187
  axed from ActiveModel in {this
64
188
  commit}[http://github.com/rails/rails/commit/db49c706b62e7ea2ab93f05399dbfddf5087ee0c].
65
189
 
66
- And on Krzysiek Heród's gem, {Transitions}[https://github.com/netizer/transitions], which added Mongoid support.
190
+ And on Krzysiek Heród's gem, {Transitions}[https://github.com/netizer/transitions], which added Mongoid support.
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["Dan Persa"]
9
9
  s.email = ["dan.persa@gmail.com"]
10
10
  s.homepage = "http://github.com/danpersa/edge-state-machine"
11
- s.summary = %q{State machine extracted from ActiveModel}
12
- s.description = %q{Lightweight state machine extracted from ActiveModel}
11
+ s.summary = %q{Edge State Machine}
12
+ s.description = %q{Edge State Machine is a complete state machine solution. It offers support for ActiveRecord and Mongoid for persistence.}
13
13
 
14
14
  s.rubyforge_project = "edge-state-machine"
15
15
 
@@ -1,3 +1,3 @@
1
1
  module EdgeStateMachine
2
- VERSION = "0.0.3"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -3,8 +3,10 @@ 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/migrations/create_double_machine'
6
7
  require 'active_record/samples/traffic_light'
7
8
  require 'active_record/samples/order'
9
+ require 'active_record/samples/double_machine'
8
10
 
9
11
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", 'lib'))
10
12
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -1,127 +1,129 @@
1
1
  require 'active_record/active_record_helper'
2
2
 
3
- describe "active record state machine" do
3
+ describe 'active record state machine' do
4
4
 
5
- context "existing active record" do
5
+ context 'existing active record' do
6
6
  before do
7
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
7
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
8
8
  ActiveRecord::Migration.verbose = false
9
9
  CreateTrafficLights.migrate(:up)
10
- @light = TrafficLight.create!
11
10
  end
12
11
 
13
- it "should have an initial state" do
14
- @light.off?.should == true
15
- @light.current_state.should == :off
12
+ let :light do
13
+ TrafficLight.create!
16
14
  end
17
15
 
18
- it "should go to a valid state on transition" do
19
- @light.reset
20
- @light.red?.should == true
21
- @light.current_state.should == :red
16
+ it 'should have an initial state' do
17
+ light.off?.should == true
18
+ light.current_state.should == :off
19
+ end
20
+
21
+ it 'should go to a valid state on transition' do
22
+ light.reset
23
+ light.red?.should == true
24
+ light.current_state.should == :red
22
25
 
23
- @light.green_on
24
- @light.green?.should == true
25
- @light.current_state.should == :green
26
+ light.green_on
27
+ light.green?.should == true
28
+ light.current_state.should == :green
26
29
  end
27
30
 
28
- it "should not persist state on transition" do
29
- @light.reset
30
- @light.current_state.should == :red
31
- @light.reload
32
- @light.state.should == "off"
31
+ it 'should not persist state on transition' do
32
+ light.reset
33
+ light.current_state.should == :red
34
+ light.reload
35
+ light.state.should == 'off'
33
36
  end
34
37
 
35
- it "should persists state on transition" do
36
- @light.reset!
37
- @light.current_state.should == :red
38
- @light.reload
39
- @light.state.should == "red"
38
+ it 'should persists state on transition' do
39
+ light.reset!
40
+ light.current_state.should == :red
41
+ light.reload
42
+ light.state.should == 'red'
40
43
  end
41
44
 
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
+ it 'should initialize the current state when loaded from database' do
46
+ light.reset!
47
+ loaded_light = TrafficLight.find_by_id(light.id)
45
48
  loaded_light.current_state.should == :red
46
49
  end
47
50
 
48
- it "should raise error on transition to an invalid state" do
49
- expect { @light.yellow_on }.should raise_error EdgeStateMachine::NoTransitionFound
50
- @light.current_state.should == :off
51
+ it 'should raise error on transition to an invalid state' do
52
+ expect { light.yellow_on }.should raise_error EdgeStateMachine::NoTransitionFound
53
+ light.current_state.should == :off
51
54
  end
52
55
 
53
- it "should persist state when state is protected on transition" do
56
+ it 'should persist state when state is protected on transition' do
54
57
  protected_light = ProtectedTrafficLight.create!
55
58
  protected_light.reset!
56
59
  protected_light.current_state.should == :red
57
60
  protected_light.reload
58
- protected_light.state.should == "red"
61
+ protected_light.state.should == 'red'
59
62
  end
60
63
 
61
- it "should not validate when try transition with wrong state " do
62
- for s in @light.class.state_machines[:default].states.keys
63
- @light.state = s
64
- @light.valid?.should == true
64
+ it 'should not validate when try transition with wrong state' do
65
+ for s in light.class.state_machines[:default].states.keys
66
+ light.state = s
67
+ light.valid?.should == true
65
68
  end
66
- @light.state = "invalid_one"
67
- @light.valid?.should_not == true
69
+ light.state = 'invalid_one'
70
+ light.valid?.should_not == true
68
71
  end
69
72
 
70
- it "should raise exception when model validation fails on transition" do
73
+ it 'should raise exception when model validation fails on transition' do
71
74
  validating_light = ValidatingTrafficLight.create!
72
75
  expect {validating_light.reset!}.should raise_error ActiveRecord::RecordInvalid
73
76
  validating_light.red?.should == false
74
77
  validating_light.off?.should == true
75
78
  end
76
79
 
77
- it "should state query method used in a validation condition" do
80
+ it 'should state query method used in a validation condition' do
78
81
  validating_light = ConditionalValidatingTrafficLight.create!
79
82
  #expect {validating_light.reset!}.should raise_error ActiveRecord::RecordInvalid
80
83
  validating_light.off?.should == true
81
84
  validating_light.red?.should == false
82
85
  end
83
86
 
84
- it "should reload the model when current state resets" do
85
- @light.reset
86
- @light.red?.should == true
87
- @light.update_attribute(:state, 'green')
88
- @light.reload.green?.should == true # reloaded state should come from database
87
+ it 'should reload the model when current state resets' do
88
+ light.reset
89
+ light.red?.should == true
90
+ light.update_attribute(:state, 'green')
91
+ light.reload.green?.should == true # reloaded state should come from database
89
92
  end
90
93
 
91
- describe "scopes" do
92
- it "should be added for each state" do
94
+ describe 'scopes' do
95
+ it 'should be added for each state' do
93
96
  TrafficLight.should respond_to(:off)
94
97
  TrafficLight.should respond_to(:red)
95
98
  end
96
99
 
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
+ it 'should not be added for each state' do
101
+ TrafficLightNoScope.should_not respond_to(:off)
102
+ TrafficLightNoScope.should_not respond_to(:red)
100
103
  end
101
104
 
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
105
+ it 'should behave like scopes' do
106
+ 3.times { TrafficLight.create(:state => 'off') }
107
+ 3.times { TrafficLight.create(:state => 'red') }
108
+ TrafficLight.off.count.should == 3
107
109
  TrafficLight.red.count.should == 3
108
110
  end
109
111
  end
110
112
  end
111
113
 
112
- context "new active record" do
113
- before do
114
- @light = TrafficLight.new
114
+ context 'new active record' do
115
+ let :light do
116
+ TrafficLight.new
115
117
  end
116
118
 
117
- it "should have the initial state set" do
118
- @light.current_state.should == :off
119
+ it 'should have the initial state set' do
120
+ light.current_state.should == :off
119
121
  end
120
122
  end
121
123
 
122
- context "timestamp" do
124
+ context 'timestamp' do
123
125
  before do
124
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
126
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
125
127
  ActiveRecord::Migration.verbose = false
126
128
  CreateOrders.migrate(:up)
127
129
  end
@@ -131,10 +133,10 @@ describe "active record state machine" do
131
133
  end
132
134
 
133
135
  # control case, no timestamp has been set so we should expect default behaviour
134
- it "should not raise any exceptions when moving to placed" do
136
+ it 'should not raise any exceptions when moving to placed' do
135
137
  @order = create_order
136
138
  expect { @order.place! }.should_not raise_error
137
- @order.state.should == "placed"
139
+ @order.state.should == 'placed'
138
140
  end
139
141
  end
140
142
  end
@@ -0,0 +1,75 @@
1
+ require 'active_record/active_record_helper'
2
+
3
+ describe DoubleMachineActiveRecord do
4
+
5
+ before do
6
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
7
+ ActiveRecord::Migration.verbose = false
8
+ CreateDoubleMachine.migrate(:up)
9
+ end
10
+
11
+ let :double_machine do
12
+ DoubleMachineActiveRecord.create!
13
+ end
14
+
15
+ it 'should have a current state equals with the initial state for each machine' do
16
+ double_machine.current_state.should == :first_state
17
+ double_machine.current_state(:second).should == :red
18
+ end
19
+
20
+ it 'should have the corresponding methods for verifying the states' do
21
+ double_machine.first_state?.should == true
22
+ double_machine.second_state?.should == false
23
+ double_machine.red?.should == true
24
+ double_machine.green?.should == false
25
+ end
26
+
27
+ it 'should trigger events from the state machines' do
28
+ double_machine.first_move
29
+ double_machine.current_state.should == :second_state
30
+ double_machine.second_state?.should == true
31
+
32
+ double_machine.go_green
33
+ double_machine.current_state(:second).should == :green
34
+ double_machine.green?.should == true
35
+
36
+ double_machine.second_move
37
+ double_machine.current_state.should == :third_state
38
+ double_machine.third_state?.should == true
39
+ end
40
+
41
+ it 'should execute the on_transition method' do
42
+ double_machine.should_receive :do_move
43
+ double_machine.first_move
44
+ double_machine.go_green
45
+
46
+ double_machine.should_receive :turn_off
47
+ double_machine.should_receive :color_in_red
48
+ double_machine.go_red
49
+ end
50
+
51
+ context 'persistence ' do
52
+ it 'should create scopes for each state machine' do
53
+ 3.times { DoubleMachineActiveRecord.create(:state => 'second_state', :second_state => 'blue') }
54
+ 3.times { DoubleMachineActiveRecord.create(:state => 'first_state', :second_state => 'blue') }
55
+ DoubleMachineActiveRecord.first_state.count.should == 3
56
+ DoubleMachineActiveRecord.second_state.count.should == 3
57
+ DoubleMachineActiveRecord.blue.count.should == 6
58
+ end
59
+
60
+ it 'should save the state machines in the database' do
61
+ machine = DoubleMachineActiveRecord.create(:state => 'second_state', :second_state => 'blue')
62
+ machine.go_red!
63
+ machine.current_state(:second).should == :red
64
+ machine.red?.should == true
65
+
66
+ machine.second_move!
67
+ machine.third_state?.should == true
68
+ machine.current_state.should == :third_state
69
+
70
+ loaded_machine = DoubleMachineActiveRecord.find_by_id(machine.id)
71
+ loaded_machine.third_state?.should == true
72
+ loaded_machine.current_state.should == :third_state
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,8 @@
1
+ class CreateDoubleMachine < ActiveRecord::Migration
2
+ def self.up
3
+ create_table(:double_machine_active_records, force: true) do |t|
4
+ t.string :state
5
+ t.string :second_state
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_record'
2
+ require 'active_record/edge-state-machine'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
5
+
6
+ class DoubleMachineActiveRecord < ActiveRecord::Base
7
+ include ActiveRecord::EdgeStateMachine
8
+
9
+ state_machine do
10
+ # the machine is automatically named :default
11
+ # the scopes are not created by default
12
+ create_scopes true
13
+ # the persistence instance variable is the default one (:state)
14
+ state :first_state # first one is initial state
15
+ state :second_state
16
+ state :third_state # the user in this state can't sign in
17
+
18
+ event :first_move do
19
+ transition :from => :first_state, :to => :second_state, :on_transition => :do_move
20
+ end
21
+
22
+ event :second_move do
23
+ transition :from => :second_state, :to => :third_state
24
+ end
25
+ end
26
+
27
+ state_machine :second do
28
+ # the scopes are not created by default
29
+ create_scopes true
30
+ # for the second machine we must specify the name of the persistence instance variable
31
+ # so there are no conflicts between the state machines
32
+ persisted_to :second_state
33
+ initial_state :red
34
+ state :blue
35
+ state :green
36
+ state :red
37
+
38
+ event :go_blue do
39
+ transition :from => [:red, :green], :to => :blue
40
+ end
41
+
42
+ event :go_red do
43
+ transition :from => [:blue, :green], :to => :red, :on_transition => [:turn_off, :color_in_red]
44
+ end
45
+
46
+ event :go_green do
47
+ transition :from => [:blue, :red], :to => :green
48
+ end
49
+ end
50
+
51
+ def do_move
52
+ end
53
+
54
+ def turn_off
55
+ end
56
+
57
+ def color_in_red
58
+ end
59
+ end
@@ -43,4 +43,33 @@ end
43
43
 
44
44
  class ConditionalValidatingTrafficLight < TrafficLight
45
45
  validate :name, :presence => true, :length => { :within => 20..40 }, :confirmation => true, :if => :red?
46
+ end
47
+
48
+ class TrafficLightNoScope < ActiveRecord::Base
49
+ include ActiveRecord::EdgeStateMachine
50
+
51
+ state_machine do
52
+ persisted_to :state
53
+ state :off
54
+
55
+ state :red
56
+ state :green
57
+ state :yellow
58
+
59
+ event :red_on do
60
+ transition :to => :red, :from => [:yellow]
61
+ end
62
+
63
+ event :green_on do
64
+ transition :to => :green, :from => [:red]
65
+ end
66
+
67
+ event :yellow_on do
68
+ transition :to => :yellow, :from => [:green]
69
+ end
70
+
71
+ event :reset do
72
+ transition :to => :red, :from => [:off]
73
+ end
74
+ end
46
75
  end
data/spec/event_spec.rb CHANGED
@@ -34,17 +34,17 @@ describe EdgeStateMachine::Event do
34
34
  end
35
35
  end
36
36
 
37
- it "should set the name" do
37
+ it 'should set the name' do
38
38
  @state_name.should == @event.name
39
39
  end
40
40
 
41
- it "should create Transitions" do
41
+ it 'should create Transitions' do
42
42
  EdgeStateMachine::Transition.should_receive(:new).with(:to => :closed, :from => [:open, :received])
43
43
  new_event
44
44
  end
45
45
 
46
- describe "event arguments" do
47
- it "should pass arguments to transition method" do
46
+ describe 'event arguments' do
47
+ it 'should pass arguments to transition method' do
48
48
  subject = ArgumentsTestSubject.new
49
49
  subject.current_state.should == :initial
50
50
  subject.open!
@@ -52,13 +52,13 @@ describe EdgeStateMachine::Event do
52
52
  end
53
53
  end
54
54
 
55
- describe "events being fired" do
55
+ describe 'events being fired' do
56
56
  before do
57
57
  @machine = mock
58
58
  @machine.stub!(:name).and_return(:default)
59
59
  end
60
60
 
61
- it "should raise an EdgeStateMachine::NoTransitionFound error if the transitions are empty" do
61
+ it 'should raise an EdgeStateMachine::NoTransitionFound error if the transitions are empty' do
62
62
  event = EdgeStateMachine::Event.new(:event, @machine)
63
63
  obj = mock
64
64
  obj.stub!(:current_state).and_return(:open)
data/spec/machine_spec.rb CHANGED
@@ -31,11 +31,11 @@ describe EdgeStateMachine::Machine do
31
31
  MachineTestSubject.state_machines.size.should == 2
32
32
  end
33
33
 
34
- it "should set #initial_state_name from initial_state method" do
34
+ it 'should set #initial_state_name from initial_state method' do
35
35
  MachineTestSubject.state_machines[:extra].initial_state_name.should == :bar
36
36
  end
37
37
 
38
- it "should access non-default state machine" do
38
+ it 'should access non-default state machine' do
39
39
  MachineTestSubject.state_machines[:extra].class.should == EdgeStateMachine::Machine
40
40
  end
41
41
  end