simple_state_machine 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,14 +1,11 @@
1
1
  = SimpleStateMachine
2
2
 
3
- A Ruby Statemachine that focuses on events instead of states.
3
+ A simple DSL to decorate existing methods with state transition guards.
4
4
 
5
- A state machine should help you:
6
- - Encapsulate state,
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, we use a different approach.
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
- This has a couple of advantages:
25
- - Encapsulate state transitions (no need for :guards),
26
- - Arguments can be passed to 'events',
27
- - 'events' can return a value, remember, it's just a method,
28
- - Validation errors can be set on the model if you are using ActiveRecord / ActiveModel,
29
-
30
- To use the code, you need to do 3 things:
31
- - extend SimpleStateMachine,
32
- - set the initial state,
33
- - decorate a method with the event DSL
34
-
35
- ==== Example usage
36
-
37
- class LampWithHotelSwitch
38
-
39
- extend SimpleStateMachine
40
-
41
- def initialize
42
- self.state = :off
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
- def push_switch_1
46
- puts 'pushed switch 1 #{state}'
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
- def push_switch_2
52
- puts 'pushed switch 2 #{state}'
53
- end
54
- event :push_switch_2, :off => :on,
55
- :on => :off
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.2.2
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
- send_activation_email(self.activation_code)
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
- #event :log_send_activation_code_failed, :new => :send_activation_code_failed
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::EventMixin
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 = 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.state = old_state
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.state = old_state
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 = 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.state = old_state
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.state = old_state
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
- module EventMixin
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 EventMixin
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.state.to_s == t.from.to_s}.first
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.state = to
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.state = to
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
- @subject.class.state_machine_definition.transitions
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.state == state.to_s
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?('state=')
149
- @subject.send(:define_method, 'state=') do |new_state|
150
- @state = new_state.to_s
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?('state')
157
- @subject.send(:define_method, 'state') do
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.2.2"
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",
@@ -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
- describe User do
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 "and_save" do
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 "and_save!" do
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
- - 2
9
- - 2
10
- version: 0.2.2
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: []
data/TODO DELETED
@@ -1,4 +0,0 @@
1
- - allow other state methods
2
- - alias ..._and_save with ...!
3
- - Document after_initialize bug
4
-