simple_state_machine 0.6.0.pre → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog.rdoc +16 -0
- data/README.rdoc +79 -51
- data/lib/simple_state_machine.rb +10 -1
- data/lib/simple_state_machine/.DS_Store +0 -0
- data/lib/simple_state_machine/active_record.rb +1 -75
- data/lib/simple_state_machine/decorator/active_record.rb +74 -0
- data/lib/simple_state_machine/decorator/default.rb +91 -0
- data/lib/simple_state_machine/simple_state_machine.rb +0 -342
- data/lib/simple_state_machine/state_machine.rb +88 -0
- data/lib/simple_state_machine/state_machine_definition.rb +72 -0
- data/lib/simple_state_machine/tools/graphviz.rb +17 -0
- data/lib/simple_state_machine/tools/inspector.rb +44 -0
- data/lib/simple_state_machine/transition.rb +40 -0
- data/lib/simple_state_machine/version.rb +1 -1
- data/simple_state_machine.gemspec +1 -1
- data/spec/.DS_Store +0 -0
- data/spec/active_record_spec.rb +7 -14
- data/spec/{decorator_spec.rb → decorator/default_spec.rb} +8 -8
- data/spec/examples_spec.rb +4 -4
- data/spec/mountable_spec.rb +3 -3
- data/spec/simple_state_machine_spec.rb +1 -2
- data/spec/spec_helper.rb +6 -0
- data/spec/state_machine_definition_spec.rb +1 -76
- data/spec/tools/graphviz_spec.rb +29 -0
- data/spec/tools/inspector_spec.rb +70 -0
- metadata +25 -14
data/Changelog.rdoc
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
== 0.6.0
|
2
|
+
=== Enhancements
|
3
|
+
- Define decorated methods as #{event_name}_with_managed_state instead of with_managed_state_#{event_name} [Petrik]
|
4
|
+
- Added raised_error method to access caught Errors [Petrik]
|
5
|
+
- Added support for a default error state [Marek de Heus]
|
6
|
+
- Removed transaction support, you can still add it manually, see active_record_spec.rb [Marek de Heus]
|
7
|
+
- Allow catching errors for subclasses of the error [Petrik]
|
8
|
+
- Added gem install instructions [Marek de Heus]
|
9
|
+
- Added define_event method to simplify mountable state machine definitions [Petrik]
|
10
|
+
- Added end_states method so we can check the state machine correctness [Petrik]
|
11
|
+
- Added begin_states method so we can check the state machine correctness [Petrik]
|
12
|
+
|
13
|
+
=== Bugfixes
|
14
|
+
- Fixed bug in Inspector, from state can now be a Error class [Marek de Heus]
|
15
|
+
- Make sure from and end states are uniq [Petrik]
|
16
|
+
- fixed typos (graphiz -> graphviz) (redmar-master) [rjk]
|
data/README.rdoc
CHANGED
@@ -19,9 +19,21 @@ Or add it to your Gemfile:
|
|
19
19
|
gem 'simple_state_machine'
|
20
20
|
|
21
21
|
|
22
|
-
==
|
22
|
+
== Usage
|
23
23
|
|
24
|
-
Define
|
24
|
+
Define an event and specify how the state should transition. If we want the state to change
|
25
|
+
from :pending to :active we write:
|
26
|
+
|
27
|
+
event :activate_account, :pending => :active
|
28
|
+
|
29
|
+
That's it. You can now call activate_account and the state will automatically change.
|
30
|
+
If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is
|
31
|
+
raised.
|
32
|
+
|
33
|
+
=== Methods with arguments
|
34
|
+
|
35
|
+
If you want to pass arguments and call other methods before the state transition, define your
|
36
|
+
event as a method.
|
25
37
|
|
26
38
|
def activate_account(activation_code)
|
27
39
|
# call other methods, no need to add these in callbacks
|
@@ -29,13 +41,10 @@ Define your event as a method, arguments are allowed:
|
|
29
41
|
end
|
30
42
|
|
31
43
|
Now mark the method as an event and specify how the state should transition
|
32
|
-
when the method is called.
|
44
|
+
when the method is called.
|
33
45
|
|
34
46
|
event :activate_account, :pending => :active
|
35
47
|
|
36
|
-
That's it!
|
37
|
-
You can now call activate_account and the state will automatically change.
|
38
|
-
If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is raised.
|
39
48
|
|
40
49
|
|
41
50
|
== Basic example
|
@@ -48,9 +57,6 @@ If the state change is not allowed, a SimpleStateMachine::IllegalStateTransition
|
|
48
57
|
self.state = 'off'
|
49
58
|
end
|
50
59
|
|
51
|
-
def push_switch
|
52
|
-
puts "pushed switch"
|
53
|
-
end
|
54
60
|
event :push_switch, :off => :on,
|
55
61
|
:on => :off
|
56
62
|
|
@@ -59,14 +65,16 @@ If the state change is not allowed, a SimpleStateMachine::IllegalStateTransition
|
|
59
65
|
lamp = LampSwitch.new
|
60
66
|
lamp.state # => 'off'
|
61
67
|
lamp.off? # => true
|
62
|
-
lamp.push_switch #
|
68
|
+
lamp.push_switch #
|
63
69
|
lamp.state # => 'on'
|
64
70
|
lamp.on? # => true
|
65
|
-
lamp.push_switch #
|
71
|
+
lamp.push_switch #
|
66
72
|
lamp.off? # => true
|
67
73
|
|
68
74
|
|
69
|
-
== ActiveRecord
|
75
|
+
== ActiveRecord
|
76
|
+
|
77
|
+
=== Example
|
70
78
|
|
71
79
|
To add a state machine to an ActiveRecord class, you will have to:
|
72
80
|
- extend SimpleStateMachine::ActiveRecord,
|
@@ -85,42 +93,58 @@ To add a state machine to an ActiveRecord class, you will have to:
|
|
85
93
|
self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
|
86
94
|
end
|
87
95
|
event :invite, :pending => :invited
|
88
|
-
|
89
|
-
def confirm_invitation activation_code
|
90
|
-
if self.activation_code != activation_code
|
91
|
-
errors.add 'activation_code', 'is invalid'
|
92
|
-
end
|
93
|
-
end
|
94
|
-
event :confirm_invitation, :invited => :active
|
95
96
|
end
|
96
97
|
|
97
|
-
|
98
|
+
user = User.new
|
99
|
+
user.pending? # => true
|
100
|
+
user.invite # => true
|
101
|
+
user.invited? # => true
|
102
|
+
user.activation_code # => 'SOMEDIGEST'
|
103
|
+
|
104
|
+
For the invite method this generates the following event methods
|
98
105
|
- invite (behaves like ActiveRecord save )
|
99
106
|
- invite! (behaves like ActiveRecord save!)
|
100
|
-
- confirm_invitation (behaves like ActiveRecord save )
|
101
|
-
- confirm_invitation! (behaves like ActiveRecord save!)
|
102
|
-
|
103
|
-
And the following methods to query the state:
|
104
|
-
- pending?
|
105
|
-
- invited?
|
106
|
-
- active?
|
107
107
|
|
108
108
|
If you want to be more verbose you can also use:
|
109
109
|
- invite_and_save (alias for invite)
|
110
110
|
- invite_and_save! (alias for invite!)
|
111
111
|
|
112
112
|
|
113
|
-
|
113
|
+
=== Using ActiveRecord / ActiveModel validations
|
114
|
+
|
115
|
+
When using ActiveRecord / ActiveModel you can add an error to the errors object.
|
116
|
+
This will prevent the state from being changed.
|
117
|
+
|
118
|
+
If we add an activate_account method to User
|
119
|
+
|
120
|
+
class User < ActiveRecord::Base
|
121
|
+
...
|
122
|
+
def activate_account(activation_code)
|
123
|
+
if activation_code_invalid?(activation_code)
|
124
|
+
errors.add(:activation_code, 'Invalid')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
event :activate_account, :invited => :confirmed
|
128
|
+
...
|
129
|
+
end
|
130
|
+
|
131
|
+
user.confirm_invitation!('INVALID') # => raises ActiveRecord::RecordInvalid,
|
132
|
+
# "Validation failed: Activation code is invalid"
|
133
|
+
user.confirmed? # => false
|
134
|
+
user.confirm_invitation!('VALID')
|
135
|
+
user.confirmed? # => true
|
136
|
+
|
137
|
+
== Mountable StateMachines
|
114
138
|
|
115
139
|
If you like to separate your state machine from your model class, you can do so as following:
|
116
140
|
|
117
141
|
class MyStateMachine < SimpleStateMachine::StateMachineDefinition
|
118
142
|
|
119
|
-
event
|
120
|
-
event
|
143
|
+
event :invite, :new => :invited
|
144
|
+
event :confirm_invitation, :invited => :active
|
121
145
|
|
122
146
|
def decorator_class
|
123
|
-
SimpleStateMachine::Decorator
|
147
|
+
SimpleStateMachine::Decorator::Default
|
124
148
|
end
|
125
149
|
end
|
126
150
|
|
@@ -136,49 +160,53 @@ If you like to separate your state machine from your model class, you can do so
|
|
136
160
|
end
|
137
161
|
|
138
162
|
|
139
|
-
==
|
140
|
-
|
141
|
-
When using ActiveRecord / ActiveModel you can add an error to the errors object:
|
142
|
-
|
143
|
-
def activate_account(activation_code)
|
144
|
-
if activation_code_invalid?(activation_code)
|
145
|
-
errors.add(:activation_code, 'Invalid')
|
146
|
-
end
|
147
|
-
end
|
163
|
+
== Transitions
|
148
164
|
|
149
|
-
|
165
|
+
=== Catching all from states
|
166
|
+
If an event should transition from all other defined states, you can use :all as from state:
|
150
167
|
|
151
|
-
|
168
|
+
event :suspend, :all => :suspended
|
152
169
|
|
153
170
|
|
154
|
-
|
171
|
+
=== Catching exceptions
|
155
172
|
|
156
173
|
You can let the state machine handle exceptions by specifying the failure state for an Error:
|
157
174
|
|
158
175
|
def download_data
|
159
|
-
Service
|
176
|
+
raise Service::ConnectionError
|
160
177
|
end
|
161
|
-
event :download_data,
|
162
|
-
Service::ConnectionError => :download_failed
|
178
|
+
event :download_data, Service::ConnectionError => :download_failed
|
163
179
|
|
164
180
|
download_data # catches Service::ConnectionError
|
165
181
|
state # => "download_failed"
|
166
182
|
state_machine.raised_error # the raised error
|
167
183
|
|
168
|
-
== Catching all from states
|
169
|
-
If an event should transition from all other defined states, you can use :all as from state:
|
170
184
|
|
171
|
-
|
185
|
+
=== Default error state
|
186
|
+
|
187
|
+
To automatically change all states to a default error state use default_error_state:
|
188
|
+
|
189
|
+
state_machine_definition.default_error_state = :failed
|
190
|
+
|
191
|
+
== Transactions
|
192
|
+
|
193
|
+
If you want to run events in transactions run them in a transaction block:
|
172
194
|
|
195
|
+
user.transaction { user.invite }
|
173
196
|
|
174
|
-
==
|
197
|
+
== Tools
|
198
|
+
|
199
|
+
===Generating state diagrams
|
175
200
|
|
176
201
|
When using Rails/ActiveRecord you can generate a state diagram of the state machine via the
|
177
202
|
built in rake tasks.
|
178
203
|
For details run:
|
179
|
-
|
204
|
+
|
180
205
|
rake -T ssm
|
181
206
|
|
207
|
+
A Googlechart example:
|
208
|
+
http://tinyurl.com/79xztr6
|
209
|
+
|
182
210
|
|
183
211
|
== Note on Patches/Pull Requests
|
184
212
|
|
data/lib/simple_state_machine.rb
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
require 'simple_state_machine/simple_state_machine'
|
2
|
-
require 'simple_state_machine/
|
2
|
+
require 'simple_state_machine/state_machine'
|
3
|
+
require 'simple_state_machine/tools/graphviz'
|
4
|
+
require 'simple_state_machine/tools/inspector'
|
5
|
+
require 'simple_state_machine/state_machine_definition'
|
6
|
+
require 'simple_state_machine/transition'
|
7
|
+
require 'simple_state_machine/decorator/default'
|
8
|
+
# if defined?(ActiveRecord)
|
9
|
+
require 'simple_state_machine/active_record'
|
10
|
+
require 'simple_state_machine/decorator/active_record'
|
11
|
+
# end
|
3
12
|
require "simple_state_machine/railtie" if defined?(Rails::Railtie)
|
4
13
|
|
Binary file
|
@@ -4,84 +4,10 @@ module SimpleStateMachine::ActiveRecord
|
|
4
4
|
include SimpleStateMachine::Extendable
|
5
5
|
include SimpleStateMachine::Inheritable
|
6
6
|
|
7
|
-
class Decorator < SimpleStateMachine::Decorator
|
8
|
-
|
9
|
-
# decorates subject with:
|
10
|
-
# * {event_name}_and_save
|
11
|
-
# * {event_name}_and_save!
|
12
|
-
# * {event_name}!
|
13
|
-
# * {event_name}
|
14
|
-
def decorate transition
|
15
|
-
super transition
|
16
|
-
event_name = transition.event_name.to_s
|
17
|
-
decorate_save event_name
|
18
|
-
decorate_save! event_name
|
19
|
-
end
|
20
|
-
|
21
|
-
protected
|
22
|
-
|
23
|
-
def decorate_save event_name
|
24
|
-
event_name_and_save = "#{event_name}_and_save"
|
25
|
-
unless @subject.method_defined?(event_name_and_save)
|
26
|
-
@subject.send(:define_method, event_name_and_save) do |*args|
|
27
|
-
result = false
|
28
|
-
old_state = self.send(self.class.state_machine_definition.state_method)
|
29
|
-
send "with_managed_state_#{event_name}", *args
|
30
|
-
if !self.errors.entries.empty?
|
31
|
-
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
32
|
-
else
|
33
|
-
if save
|
34
|
-
result = true
|
35
|
-
else
|
36
|
-
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
return result
|
40
|
-
end
|
41
|
-
@subject.send :alias_method, "#{event_name}", event_name_and_save
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def decorate_save! event_name
|
46
|
-
event_name_and_save_bang = "#{event_name}_and_save!"
|
47
|
-
unless @subject.method_defined?(event_name_and_save_bang)
|
48
|
-
@subject.send(:define_method, event_name_and_save_bang) do |*args|
|
49
|
-
result = nil
|
50
|
-
old_state = self.send(self.class.state_machine_definition.state_method)
|
51
|
-
send "with_managed_state_#{event_name}", *args
|
52
|
-
if !self.errors.entries.empty?
|
53
|
-
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
54
|
-
raise ActiveRecord::RecordInvalid.new(self)
|
55
|
-
end
|
56
|
-
begin
|
57
|
-
result = save!
|
58
|
-
rescue ActiveRecord::RecordInvalid
|
59
|
-
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
60
|
-
raise #re raise
|
61
|
-
end
|
62
|
-
return result
|
63
|
-
end
|
64
|
-
@subject.send :alias_method, "#{event_name}!", event_name_and_save_bang
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def decorate_ar_method
|
69
|
-
end
|
70
|
-
|
71
|
-
def alias_event_methods event_name
|
72
|
-
@subject.send :alias_method, "without_managed_state_#{event_name}", event_name
|
73
|
-
end
|
74
|
-
|
75
|
-
def define_state_setter_method; end
|
76
|
-
|
77
|
-
def define_state_getter_method; end
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
7
|
def state_machine_definition
|
82
8
|
unless @state_machine_definition
|
83
9
|
@state_machine_definition = SimpleStateMachine::StateMachineDefinition.new
|
84
|
-
@state_machine_definition.decorator_class = Decorator
|
10
|
+
@state_machine_definition.decorator_class = SimpleStateMachine::Decorator::ActiveRecord
|
85
11
|
@state_machine_definition.subject = self
|
86
12
|
end
|
87
13
|
@state_machine_definition
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module SimpleStateMachine
|
2
|
+
module Decorator
|
3
|
+
class ActiveRecord < SimpleStateMachine::Decorator::Default
|
4
|
+
|
5
|
+
# decorates subject with:
|
6
|
+
# * {event_name}_and_save
|
7
|
+
# * {event_name}_and_save!
|
8
|
+
# * {event_name}!
|
9
|
+
# * {event_name}
|
10
|
+
def decorate transition
|
11
|
+
super transition
|
12
|
+
event_name = transition.event_name.to_s
|
13
|
+
decorate_save event_name
|
14
|
+
decorate_save! event_name
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def decorate_save event_name
|
20
|
+
event_name_and_save = "#{event_name}_and_save"
|
21
|
+
unless @subject.method_defined?(event_name_and_save)
|
22
|
+
@subject.send(:define_method, event_name_and_save) do |*args|
|
23
|
+
result = false
|
24
|
+
old_state = self.send(self.class.state_machine_definition.state_method)
|
25
|
+
send "#{event_name}_with_managed_state", *args
|
26
|
+
if !self.errors.entries.empty?
|
27
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
28
|
+
else
|
29
|
+
if save
|
30
|
+
result = true
|
31
|
+
else
|
32
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
return result
|
36
|
+
end
|
37
|
+
@subject.send :alias_method, "#{event_name}", event_name_and_save
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def decorate_save! event_name
|
42
|
+
event_name_and_save_bang = "#{event_name}_and_save!"
|
43
|
+
unless @subject.method_defined?(event_name_and_save_bang)
|
44
|
+
@subject.send(:define_method, event_name_and_save_bang) do |*args|
|
45
|
+
result = nil
|
46
|
+
old_state = self.send(self.class.state_machine_definition.state_method)
|
47
|
+
send "#{event_name}_with_managed_state", *args
|
48
|
+
if !self.errors.entries.empty?
|
49
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
50
|
+
raise ::ActiveRecord::RecordInvalid.new(self)
|
51
|
+
end
|
52
|
+
begin
|
53
|
+
result = save!
|
54
|
+
rescue ::ActiveRecord::RecordInvalid
|
55
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
56
|
+
raise #re raise
|
57
|
+
end
|
58
|
+
return result
|
59
|
+
end
|
60
|
+
@subject.send :alias_method, "#{event_name}!", event_name_and_save_bang
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def alias_event_methods event_name
|
65
|
+
@subject.send :alias_method, "#{event_name}_without_managed_state", event_name
|
66
|
+
end
|
67
|
+
|
68
|
+
def define_state_setter_method; end
|
69
|
+
|
70
|
+
def define_state_getter_method; end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module SimpleStateMachine
|
2
|
+
module Decorator
|
3
|
+
##
|
4
|
+
# Decorates @subject with methods to access the state machine
|
5
|
+
class Default
|
6
|
+
|
7
|
+
attr_writer :subject
|
8
|
+
def initialize(subject)
|
9
|
+
@subject = subject
|
10
|
+
end
|
11
|
+
|
12
|
+
def decorate transition
|
13
|
+
define_state_machine_method
|
14
|
+
define_state_getter_method
|
15
|
+
define_state_setter_method
|
16
|
+
|
17
|
+
define_state_helper_method(transition.from)
|
18
|
+
define_state_helper_method(transition.to)
|
19
|
+
define_event_method(transition.event_name)
|
20
|
+
decorate_event_method(transition.event_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def define_state_machine_method
|
26
|
+
unless any_method_defined?("state_machine")
|
27
|
+
@subject.send(:define_method, "state_machine") do
|
28
|
+
@state_machine ||= StateMachine.new(self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_state_helper_method state
|
34
|
+
unless any_method_defined?("#{state.to_s}?")
|
35
|
+
@subject.send(:define_method, "#{state.to_s}?") do
|
36
|
+
self.send(self.class.state_machine_definition.state_method) == state.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def define_event_method event_name
|
42
|
+
unless any_method_defined?("#{event_name}")
|
43
|
+
@subject.send(:define_method, "#{event_name}") {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def decorate_event_method event_name
|
48
|
+
# TODO put in transaction for activeRecord?
|
49
|
+
unless @subject.method_defined?("#{event_name}_with_managed_state")
|
50
|
+
@subject.send(:define_method, "#{event_name}_with_managed_state") do |*args|
|
51
|
+
return state_machine.transition(event_name) do
|
52
|
+
send("#{event_name}_without_managed_state", *args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
alias_event_methods event_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def define_state_setter_method
|
60
|
+
unless any_method_defined?("#{state_method}=")
|
61
|
+
@subject.send(:define_method, "#{state_method}=") do |new_state|
|
62
|
+
instance_variable_set(:"@#{self.class.state_machine_definition.state_method}", new_state)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def define_state_getter_method
|
68
|
+
unless any_method_defined?(state_method)
|
69
|
+
@subject.send(:attr_reader, state_method)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def any_method_defined?(method)
|
74
|
+
@subject.method_defined?(method) ||
|
75
|
+
@subject.protected_method_defined?(method) ||
|
76
|
+
@subject.private_method_defined?(method)
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
def alias_event_methods event_name
|
82
|
+
@subject.send :alias_method, "#{event_name}_without_managed_state", event_name
|
83
|
+
@subject.send :alias_method, event_name, "#{event_name}_with_managed_state"
|
84
|
+
end
|
85
|
+
|
86
|
+
def state_method
|
87
|
+
@subject.state_machine_definition.state_method
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|