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 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
-