simple_state_machine 0.2.2 → 0.3.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 +73 -36
- data/VERSION +1 -1
- data/examples/user.rb +3 -11
- data/lib/simple_state_machine/active_record.rb +9 -8
- data/lib/simple_state_machine/simple_state_machine.rb +40 -17
- data/simple_state_machine.gemspec +2 -3
- data/spec/active_record_spec.rb +58 -3
- metadata +3 -5
- data/TODO +0 -4
data/README.rdoc
CHANGED
@@ -1,14 +1,11 @@
|
|
1
1
|
= SimpleStateMachine
|
2
2
|
|
3
|
-
A
|
3
|
+
A simple DSL to decorate existing methods with state transition guards.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
- Guard state transitions,
|
8
|
-
- Make it trivial to add state transitions to any method.
|
5
|
+
SimpleStateMachine helps you to encapsulate state and guard state transitions.
|
6
|
+
Adding state transitions to a method is trivial.
|
9
7
|
|
10
|
-
Instead of using a DSL to define events,
|
11
|
-
We have a very simple DSL to decorate existing methods with logic that guards
|
8
|
+
Instead of using a DSL to define events, SimpleStateMachine decorates existing methods with logic that guards
|
12
9
|
state transitions.
|
13
10
|
|
14
11
|
==== example
|
@@ -21,40 +18,80 @@ state transitions.
|
|
21
18
|
event :activate_account, :pending => :active
|
22
19
|
|
23
20
|
|
24
|
-
|
25
|
-
-
|
26
|
-
- Arguments can be passed to
|
27
|
-
-
|
28
|
-
- Validation errors can be set on the model if you are using ActiveRecord / ActiveModel
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
21
|
+
Decorating methods has a couple of advantages:
|
22
|
+
- No need for defining extra methods for adding logic
|
23
|
+
- Arguments can be passed to events
|
24
|
+
- State transitions are encapsulated. No need for :guards
|
25
|
+
- Validation errors can be set on the model if you are using ActiveRecord / ActiveModel
|
26
|
+
|
27
|
+
== Example
|
28
|
+
|
29
|
+
To add a state machine:
|
30
|
+
- extend SimpleStateMachine
|
31
|
+
- set the initial state
|
32
|
+
- turn methods into events
|
33
|
+
|
34
|
+
class LampWithHotelSwitch
|
35
|
+
|
36
|
+
extend SimpleStateMachine
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
self.state = :off
|
40
|
+
end
|
41
|
+
|
42
|
+
def push_switch_1
|
43
|
+
puts 'pushed switch 1 #{state}'
|
44
|
+
end
|
45
|
+
event :push_switch_1, :off => :on,
|
46
|
+
:on => :off
|
47
|
+
|
48
|
+
def push_switch_2
|
49
|
+
puts 'pushed switch 2 #{state}'
|
50
|
+
end
|
51
|
+
event :push_switch_2, :off => :on,
|
52
|
+
:on => :off
|
53
|
+
|
43
54
|
end
|
44
55
|
|
45
|
-
|
46
|
-
|
56
|
+
== ActiveRecord Example
|
57
|
+
|
58
|
+
To add a state machine with ActiveRecord persistence:
|
59
|
+
- extend SimpleStateMachine::ActiveRecord,
|
60
|
+
- set the initial state in after_initialize,
|
61
|
+
- turn methods into events
|
62
|
+
|
63
|
+
class User < ActiveRecord::Base
|
64
|
+
|
65
|
+
extend SimpleStateMachine::ActiveRecord
|
66
|
+
|
67
|
+
def after_initialize
|
68
|
+
self.state ||= 'new'
|
69
|
+
# if you get an ActiveRecord::MissingAttributeError
|
70
|
+
# you'll probably need to do (http://bit.ly/35q23b):
|
71
|
+
# write_attribute(:state, "new") unless read_attribute(:state)
|
72
|
+
end
|
73
|
+
|
74
|
+
def invite
|
75
|
+
self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
|
76
|
+
#send_activation_email
|
77
|
+
end
|
78
|
+
event :invite, :new => :invited
|
79
|
+
|
80
|
+
def confirm_invitation activation_code
|
81
|
+
if self.activation_code != activation_code
|
82
|
+
errors.add 'activation_code', 'is invalid'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
event :confirm_invitation, :invited => :active
|
86
|
+
|
47
87
|
end
|
48
|
-
event :push_switch_1, :off => :on,
|
49
|
-
:on => :off
|
50
88
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
89
|
+
This generates the following methods
|
90
|
+
- {event}_and_save works like save
|
91
|
+
- {event}_and_save! works like save!
|
92
|
+
- {event}! works the same as {event}_and_save!
|
93
|
+
- {state}? whether or not the current state is {state}
|
56
94
|
|
57
|
-
end
|
58
95
|
|
59
96
|
This code was just released, we do not claim it to be stable.
|
60
97
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/examples/user.rb
CHANGED
@@ -11,9 +11,9 @@ class User < ActiveRecord::Base
|
|
11
11
|
|
12
12
|
def invite
|
13
13
|
self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
|
14
|
-
|
14
|
+
true
|
15
15
|
end
|
16
|
-
event :invite, :new => :invited
|
16
|
+
event :invite, :new => :invited, :on_success => :send_activation_email
|
17
17
|
|
18
18
|
def confirm_invitation activation_code
|
19
19
|
if self.activation_code != activation_code
|
@@ -22,15 +22,7 @@ class User < ActiveRecord::Base
|
|
22
22
|
end
|
23
23
|
event :confirm_invitation, :invited => :active
|
24
24
|
|
25
|
-
|
26
|
-
#
|
27
|
-
# def reset_password(new_password)
|
28
|
-
# self.password = new_password
|
29
|
-
# end
|
30
|
-
# # do not change state, but ensure that we are in proper state
|
31
|
-
# event :reset_password, :active => :active
|
32
|
-
|
33
|
-
def send_activation_email(code)
|
25
|
+
def send_activation_email
|
34
26
|
true
|
35
27
|
end
|
36
28
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module SimpleStateMachine::ActiveRecord
|
2
2
|
|
3
|
-
include SimpleStateMachine::
|
3
|
+
include SimpleStateMachine::StateMachineMixin
|
4
4
|
|
5
5
|
def state_machine_decorator subject
|
6
6
|
Decorator.new subject
|
@@ -12,16 +12,16 @@ module SimpleStateMachine::ActiveRecord
|
|
12
12
|
super transition
|
13
13
|
unless @subject.method_defined?("#{transition.event_name}_and_save")
|
14
14
|
@subject.send(:define_method, "#{transition.event_name}_and_save") do |*args|
|
15
|
-
old_state =
|
15
|
+
old_state = self.send(self.class.state_machine_definition.state_method)
|
16
16
|
send "#{transition.event_name}", *args
|
17
17
|
if !self.errors.entries.empty?
|
18
|
-
self.
|
18
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
19
19
|
return false
|
20
20
|
else
|
21
21
|
if save
|
22
22
|
return true
|
23
23
|
else
|
24
|
-
self.
|
24
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
25
25
|
return false
|
26
26
|
end
|
27
27
|
end
|
@@ -29,19 +29,20 @@ module SimpleStateMachine::ActiveRecord
|
|
29
29
|
end
|
30
30
|
unless @subject.method_defined?("#{transition.event_name}_and_save!")
|
31
31
|
@subject.send(:define_method, "#{transition.event_name}_and_save!") do |*args|
|
32
|
-
old_state =
|
32
|
+
old_state = self.send(self.class.state_machine_definition.state_method)
|
33
33
|
send "#{transition.event_name}", *args
|
34
34
|
if !self.errors.entries.empty?
|
35
|
-
self.
|
35
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
36
36
|
raise ActiveRecord::RecordInvalid.new(self)
|
37
37
|
end
|
38
38
|
begin
|
39
39
|
save!
|
40
40
|
rescue ActiveRecord::RecordInvalid
|
41
|
-
self.
|
42
|
-
raise
|
41
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
42
|
+
raise #re raise
|
43
43
|
end
|
44
44
|
end
|
45
|
+
@subject.send :alias_method, "#{transition.event_name}!", "#{transition.event_name}_and_save!"
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module SimpleStateMachine
|
2
|
-
|
3
|
-
|
2
|
+
##
|
3
|
+
# Adds state machine methods to extended class
|
4
|
+
module StateMachineMixin
|
4
5
|
|
5
6
|
def event event_name, state_transitions
|
6
7
|
state_transitions.each do |from, to|
|
@@ -30,10 +31,14 @@ module SimpleStateMachine
|
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
33
|
-
include
|
34
|
+
include StateMachineMixin
|
34
35
|
|
36
|
+
##
|
37
|
+
# Defines state machine transitions
|
35
38
|
class StateMachineDefinition
|
36
39
|
|
40
|
+
attr_writer :state_method
|
41
|
+
|
37
42
|
def transitions
|
38
43
|
@transitions ||= []
|
39
44
|
end
|
@@ -43,9 +48,14 @@ module SimpleStateMachine
|
|
43
48
|
transitions << transition
|
44
49
|
transition
|
45
50
|
end
|
46
|
-
|
51
|
+
|
52
|
+
def state_method
|
53
|
+
@state_method ||= :state
|
54
|
+
end
|
47
55
|
end
|
48
56
|
|
57
|
+
##
|
58
|
+
# The state machine used by the instance
|
49
59
|
class StateMachine
|
50
60
|
|
51
61
|
def initialize(subject)
|
@@ -53,7 +63,7 @@ module SimpleStateMachine
|
|
53
63
|
end
|
54
64
|
|
55
65
|
def next_state(event_name)
|
56
|
-
transition = transitions.select{|t| t.event_name.to_s == event_name.to_s && @subject.
|
66
|
+
transition = transitions.select{|t| t.event_name.to_s == event_name.to_s && @subject.send(state_method).to_s == t.from.to_s}.first
|
57
67
|
transition ? transition.to : nil
|
58
68
|
end
|
59
69
|
|
@@ -63,13 +73,13 @@ module SimpleStateMachine
|
|
63
73
|
# TODO refactor out to AR module
|
64
74
|
if defined?(::ActiveRecord) && @subject.is_a?(::ActiveRecord::Base)
|
65
75
|
if @subject.errors.entries.empty?
|
66
|
-
@subject.
|
76
|
+
@subject.send("#{state_method}=", to)
|
67
77
|
return true
|
68
78
|
else
|
69
79
|
return false
|
70
80
|
end
|
71
81
|
else
|
72
|
-
@subject.
|
82
|
+
@subject.send("#{state_method}=", to)
|
73
83
|
return result
|
74
84
|
end
|
75
85
|
else
|
@@ -78,9 +88,17 @@ module SimpleStateMachine
|
|
78
88
|
end
|
79
89
|
|
80
90
|
private
|
81
|
-
|
91
|
+
|
92
|
+
def state_machine_definition
|
93
|
+
@subject.class.state_machine_definition
|
94
|
+
end
|
95
|
+
|
82
96
|
def transitions
|
83
|
-
|
97
|
+
state_machine_definition.transitions
|
98
|
+
end
|
99
|
+
|
100
|
+
def state_method
|
101
|
+
state_machine_definition.state_method
|
84
102
|
end
|
85
103
|
|
86
104
|
def illegal_event_callback event_name
|
@@ -93,6 +111,8 @@ module SimpleStateMachine
|
|
93
111
|
class Transition < Struct.new(:event_name, :from, :to)
|
94
112
|
end
|
95
113
|
|
114
|
+
##
|
115
|
+
# Decorates the extended class with methods to access the state machine
|
96
116
|
class Decorator
|
97
117
|
|
98
118
|
def initialize(subject)
|
@@ -120,7 +140,7 @@ module SimpleStateMachine
|
|
120
140
|
def define_state_helper_method state
|
121
141
|
unless @subject.method_defined?("#{state.to_s}?")
|
122
142
|
@subject.send(:define_method, "#{state.to_s}?") do
|
123
|
-
self.
|
143
|
+
self.send(self.class.state_machine_definition.state_method) == state.to_s
|
124
144
|
end
|
125
145
|
end
|
126
146
|
end
|
@@ -145,21 +165,24 @@ module SimpleStateMachine
|
|
145
165
|
end
|
146
166
|
|
147
167
|
def define_state_setter_method
|
148
|
-
unless @subject.method_defined?(
|
149
|
-
@subject.send(:define_method,
|
150
|
-
|
168
|
+
unless @subject.method_defined?("#{state_method}=")
|
169
|
+
@subject.send(:define_method, "#{state_method}=") do |new_state|
|
170
|
+
instance_variable_set(:"@#{self.class.state_machine_definition.state_method}", new_state)
|
151
171
|
end
|
152
172
|
end
|
153
173
|
end
|
154
174
|
|
155
175
|
def define_state_getter_method
|
156
|
-
unless @subject.method_defined?(
|
157
|
-
@subject.send(:
|
158
|
-
@state
|
159
|
-
end
|
176
|
+
unless @subject.method_defined?(state_method)
|
177
|
+
@subject.send(:attr_reader, state_method)
|
160
178
|
end
|
161
179
|
end
|
162
180
|
|
181
|
+
protected
|
182
|
+
|
183
|
+
def state_method
|
184
|
+
@subject.state_machine_definition.state_method
|
185
|
+
end
|
163
186
|
end
|
164
187
|
|
165
188
|
end
|
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{simple_state_machine}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Marek de Heus", "Petrik de Heus"]
|
@@ -14,8 +14,7 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.email = ["FIX@example.com"]
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
17
|
-
"README.rdoc"
|
18
|
-
"TODO"
|
17
|
+
"README.rdoc"
|
19
18
|
]
|
20
19
|
s.files = [
|
21
20
|
"LICENSE",
|
data/spec/active_record_spec.rb
CHANGED
@@ -18,6 +18,12 @@ def setup_db
|
|
18
18
|
t.column :updated_at, :datetime
|
19
19
|
end
|
20
20
|
end
|
21
|
+
ActiveRecord::Schema.define(:version => 1) do
|
22
|
+
create_table :tickets do |t|
|
23
|
+
t.column :id, :integer
|
24
|
+
t.column :ssm_state, :string
|
25
|
+
end
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
29
|
def teardown_db
|
@@ -26,7 +32,19 @@ def teardown_db
|
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
29
|
-
|
35
|
+
class Ticket < ActiveRecord::Base
|
36
|
+
extend SimpleStateMachine::ActiveRecord
|
37
|
+
|
38
|
+
state_machine_definition.state_method = :ssm_state
|
39
|
+
|
40
|
+
def after_initialize
|
41
|
+
self.ssm_state ||= 'open'
|
42
|
+
end
|
43
|
+
|
44
|
+
event :close, :open => :closed
|
45
|
+
end
|
46
|
+
|
47
|
+
describe ActiveRecord do
|
30
48
|
|
31
49
|
before do
|
32
50
|
setup_db
|
@@ -42,7 +60,7 @@ describe User do
|
|
42
60
|
|
43
61
|
# TODO needs nesting/grouping, seems to have some duplication
|
44
62
|
|
45
|
-
describe "
|
63
|
+
describe "event_and_save" do
|
46
64
|
it "persists transitions" do
|
47
65
|
user = User.create!(:name => 'name')
|
48
66
|
user.invite_and_save.should == true
|
@@ -82,7 +100,7 @@ describe User do
|
|
82
100
|
|
83
101
|
end
|
84
102
|
|
85
|
-
describe "
|
103
|
+
describe "event_and_save!" do
|
86
104
|
|
87
105
|
it "persists transitions" do
|
88
106
|
user = User.create!(:name => 'name')
|
@@ -117,4 +135,41 @@ describe User do
|
|
117
135
|
|
118
136
|
end
|
119
137
|
|
138
|
+
describe "event!" do
|
139
|
+
|
140
|
+
it "persists transitions" do
|
141
|
+
user = User.create!(:name => 'name')
|
142
|
+
user.invite!.should == true
|
143
|
+
User.find(user.id).should be_invited
|
144
|
+
User.find(user.id).activation_code.should_not be_nil
|
145
|
+
end
|
146
|
+
|
147
|
+
it "raises a RecordInvalid and keeps state if record is invalid" do
|
148
|
+
user = User.new
|
149
|
+
user.should be_new
|
150
|
+
user.should_not be_valid
|
151
|
+
l = lambda { user.invite! }
|
152
|
+
l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
|
153
|
+
user.should be_new
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
describe 'custom state method' do
|
159
|
+
|
160
|
+
it "persists transitions" do
|
161
|
+
ticket = Ticket.create!
|
162
|
+
ticket.should be_open
|
163
|
+
ticket.close.should == true
|
164
|
+
ticket.should be_closed
|
165
|
+
end
|
166
|
+
|
167
|
+
it "persists transitions with !" do
|
168
|
+
ticket = Ticket.create!
|
169
|
+
ticket.should be_open
|
170
|
+
ticket.close!
|
171
|
+
ticket.should be_closed
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
120
175
|
end
|
metadata
CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Marek de Heus
|
@@ -45,7 +45,6 @@ extensions: []
|
|
45
45
|
extra_rdoc_files:
|
46
46
|
- LICENSE
|
47
47
|
- README.rdoc
|
48
|
-
- TODO
|
49
48
|
files:
|
50
49
|
- LICENSE
|
51
50
|
- README.rdoc
|
@@ -66,7 +65,6 @@ files:
|
|
66
65
|
- spec/simple_state_machine_spec.rb
|
67
66
|
- spec/spec.opts
|
68
67
|
- spec/spec_helper.rb
|
69
|
-
- TODO
|
70
68
|
has_rdoc: true
|
71
69
|
homepage: http://github.com/p8/simple_state_machine
|
72
70
|
licenses: []
|