simple_state_machine 0.6.0.pre → 0.6.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/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
|