simple_state_machine 0.6.0.pre → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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