edge-state-machine 0.0.3 → 0.9.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.
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