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.
@@ -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]
@@ -19,9 +19,21 @@ Or add it to your Gemfile:
19
19
  gem 'simple_state_machine'
20
20
 
21
21
 
22
- == Basic usage
22
+ == Usage
23
23
 
24
- Define your event as a method, arguments are allowed:
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. If we want the state to change from :pending to :active we write:
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 # => 'pushed switch'
68
+ lamp.push_switch #
63
69
  lamp.state # => 'on'
64
70
  lamp.on? # => true
65
- lamp.push_switch # => 'pushed switch'
71
+ lamp.push_switch #
66
72
  lamp.off? # => true
67
73
 
68
74
 
69
- == ActiveRecord Example
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
- This generates the following event methods
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
- == ActiveRecord Mountable Example
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(:invite, :new => :invited)
120
- event(:confirm_invitation, :invited => :active)
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
- == Using ActiveRecord / ActiveModel validations
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
- activate_account!("INVALID_CODE") # => ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid"
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
- This will prevent the state from being changed.
168
+ event :suspend, :all => :suspended
152
169
 
153
170
 
154
- == Catching exceptions
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.download_data
176
+ raise Service::ConnectionError
160
177
  end
161
- event :download_data, :pending => :downloaded,
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
- event :suspend, :all => :suspended
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
- == Generating state diagrams
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
 
@@ -1,4 +1,13 @@
1
1
  require 'simple_state_machine/simple_state_machine'
2
- require 'simple_state_machine/active_record'
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
 
@@ -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