simple_state_machine 0.5.3 → 0.6.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,34 +5,18 @@ A simple DSL to decorate existing methods with state transition guards.
5
5
  Instead of using a DSL to define events, SimpleStateMachine decorates methods
6
6
  to help you encapsulate state and guard state transitions.
7
7
 
8
- It supports exception rescuing, google chart visualization and mountable state_machines.
8
+ It supports exception rescuing, google chart visualization and mountable state machines.
9
9
 
10
- == Basic example
11
10
 
12
- class LampSwitch
11
+ == Installation
13
12
 
14
- extend SimpleStateMachine
13
+ Use gem install:
15
14
 
16
- def initialize
17
- self.state = 'off'
18
- end
15
+ gem install simple_state_machine
19
16
 
20
- def push_switch
21
- puts "pushed switch"
22
- end
23
- event :push_switch, :off => :on,
24
- :on => :off
25
-
26
- end
17
+ Or add it to your Gemfile:
27
18
 
28
- lamp = LampSwitch.new
29
- lamp.state # => 'off'
30
- lamp.off? # => true
31
- lamp.push_switch # => 'pushed switch'
32
- lamp.state # => 'on'
33
- lamp.on? # => true
34
- lamp.push_switch # => 'pushed switch'
35
- lamp.off? # => true
19
+ gem 'simple_state_machine'
36
20
 
37
21
 
38
22
  == Basic usage
@@ -53,59 +37,55 @@ That's it!
53
37
  You can now call activate_account and the state will automatically change.
54
38
  If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is raised.
55
39
 
56
- === Using ActiveRecord / ActiveModel validations
57
- When using ActiveRecord / ActiveModel you can add an error to the errors object.
58
- This will prevent the state from being changed.
59
-
60
- def activate_account(activation_code)
61
- if activation_code_invalid?(activation_code)
62
- errors.add(:activation_code, 'Invalid')
63
- end
64
- end
65
40
 
66
- activate_account!("INVALID_CODE") # => ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid"
41
+ == Basic example
67
42
 
68
- === Catching exceptions
69
- You can rescue exceptions and specify the failure state
43
+ class LampSwitch
70
44
 
71
- def download_data
72
- Service.download_data
73
- end
74
- event :download_data, :pending => :downloaded,
75
- Service::ConnectionError => :download_failed
45
+ extend SimpleStateMachine
76
46
 
77
- download_data # catches Service::ConnectionError
78
- state # => "download_failed"
47
+ def initialize
48
+ self.state = 'off'
49
+ end
79
50
 
80
- === Catching all from states
81
- If an event should transition from all states you can use :all
51
+ def push_switch
52
+ puts "pushed switch"
53
+ end
54
+ event :push_switch, :off => :on,
55
+ :on => :off
56
+
57
+ end
58
+
59
+ lamp = LampSwitch.new
60
+ lamp.state # => 'off'
61
+ lamp.off? # => true
62
+ lamp.push_switch # => 'pushed switch'
63
+ lamp.state # => 'on'
64
+ lamp.on? # => true
65
+ lamp.push_switch # => 'pushed switch'
66
+ lamp.off? # => true
82
67
 
83
- event :suspend, :all => :suspended
84
68
 
85
69
  == ActiveRecord Example
86
70
 
87
- To add a state machine with ActiveRecord persistence:
71
+ To add a state machine to an ActiveRecord class, you will have to:
88
72
  - extend SimpleStateMachine::ActiveRecord,
89
73
  - set the initial state in after_initialize,
90
74
  - turn methods into events
91
75
 
92
76
  class User < ActiveRecord::Base
93
-
77
+
94
78
  extend SimpleStateMachine::ActiveRecord
95
79
 
96
80
  def after_initialize
97
- self.ssm_state ||= 'pending'
98
- # if you get an ActiveRecord::MissingAttributeError
99
- # you'll probably need to do (http://bit.ly/35q23b):
100
- # write_attribute(:ssm_state, "pending") unless read_attribute(:ssm_state)
101
- end
102
-
81
+ self.state ||= 'pending'
82
+ end
83
+
103
84
  def invite
104
85
  self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
105
- #send_activation_email
106
86
  end
107
87
  event :invite, :pending => :invited
108
-
88
+
109
89
  def confirm_invitation activation_code
110
90
  if self.activation_code != activation_code
111
91
  errors.add 'activation_code', 'is invalid'
@@ -115,9 +95,10 @@ To add a state machine with ActiveRecord persistence:
115
95
  end
116
96
 
117
97
  This generates the following event methods
118
- - invite
119
- - invite!
120
- - confirm_invitation!
98
+ - invite (behaves like ActiveRecord save )
99
+ - invite! (behaves like ActiveRecord save!)
100
+ - confirm_invitation (behaves like ActiveRecord save )
101
+ - confirm_invitation! (behaves like ActiveRecord save!)
121
102
 
122
103
  And the following methods to query the state:
123
104
  - pending?
@@ -125,40 +106,82 @@ And the following methods to query the state:
125
106
  - active?
126
107
 
127
108
  If you want to be more verbose you can also use:
128
- - invite_and_save (is equal to invite and behaves like ActiveRecord save)
129
- - invite_and_save! (is equal to invite! and behaves like save!)
109
+ - invite_and_save (alias for invite)
110
+ - invite_and_save! (alias for invite!)
130
111
 
131
- == Mountable Example
132
112
 
133
- You can define your state machine in a seperate class:
113
+ == ActiveRecord Mountable Example
134
114
 
135
- class MyStateMachine < SimpleStateMachine::StateMachineDefinition
136
- def initialize(subject)
137
- self.lazy_decorator = lambda { SimpleStateMachine::Decorator.new(subject) }
138
- add_transition(:invite, :new, :invited)
139
- add_transition(:confirm_invitation, :invited, :active)
140
- end
115
+ If you like to separate your state machine from your model class, you can do so as following:
116
+
117
+ class MyStateMachine < SimpleStateMachine::StateMachineDefinition
118
+
119
+ event(:invite, :new => :invited)
120
+ event(:confirm_invitation, :invited => :active)
121
+
122
+ def decorator_class
123
+ SimpleStateMachine::Decorator
141
124
  end
125
+ end
142
126
 
143
- class User < ActiveRecord::Base
144
-
145
- extend SimpleStateMachine::Mountable
146
- self.state_machine_definition = MyStateMachine.new self
127
+ class User < ActiveRecord::Base
147
128
 
148
- def after_initialize
149
- self.ssm_state ||= 'new'
150
- end
151
-
129
+ extend SimpleStateMachine::Mountable
130
+ mount_state_machine MyStateMachine
131
+
132
+ def after_initialize
133
+ self.state ||= 'new'
152
134
  end
153
135
 
136
+ end
154
137
 
155
- == Generating google chart visualizations
156
138
 
157
- If your using rails you get rake tasks for generating a graphiz google chart of the state machine.
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
148
+
149
+ activate_account!("INVALID_CODE") # => ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid"
150
+
151
+ This will prevent the state from being changed.
152
+
153
+
154
+ == Catching exceptions
155
+
156
+ You can let the state machine handle exceptions by specifying the failure state for an Error:
157
+
158
+ def download_data
159
+ Service.download_data
160
+ end
161
+ event :download_data, :pending => :downloaded,
162
+ Service::ConnectionError => :download_failed
163
+
164
+ download_data # catches Service::ConnectionError
165
+ state # => "download_failed"
166
+ state_machine.raised_error # the raised error
167
+
168
+ == Catching all from states
169
+ If an event should transition from all other defined states, you can use :all as from state:
170
+
171
+ event :suspend, :all => :suspended
172
+
173
+
174
+ == Generating state diagrams
175
+
176
+ When using Rails/ActiveRecord you can generate a state diagram of the state machine via the
177
+ built in rake tasks.
178
+ For details run:
179
+
180
+ rake -T ssm
158
181
 
159
182
 
160
183
  == Note on Patches/Pull Requests
161
-
184
+
162
185
  * Fork the project.
163
186
  * Make your feature addition or bug fix.
164
187
  * Add tests for it. This is important so I don't break it in a
@@ -167,6 +190,7 @@ If your using rails you get rake tasks for generating a graphiz google chart of
167
190
  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
168
191
  * Send me a pull request. Bonus points for topic branches.
169
192
 
193
+
170
194
  == Copyright
171
195
 
172
196
  Copyright (c) 2010 Marek & Petrik. See LICENSE for details.
@@ -1,12 +1,15 @@
1
1
  require 'digest/sha1'
2
2
  class User < ActiveRecord::Base
3
-
3
+
4
4
  validates_presence_of :name
5
-
5
+
6
6
  extend SimpleStateMachine::ActiveRecord
7
7
 
8
8
  def after_initialize
9
9
  self.state ||= 'new'
10
+ # if you get an ActiveRecord::MissingAttributeError
11
+ # you'll probably need to do (http://bit.ly/35q23b):
12
+ # write_attribute(:ssm_state, "pending") unless read_attribute(:ssm_state)
10
13
  end
11
14
 
12
15
  def invite
@@ -23,13 +26,13 @@ class User < ActiveRecord::Base
23
26
  event :confirm_invitation, :invited => :active
24
27
 
25
28
  #event :log_send_activation_code_failed, :new => :send_activation_code_failed
26
- #
29
+ #
27
30
  # def reset_password(new_password)
28
31
  # self.password = new_password
29
32
  # end
30
33
  # # do not change state, but ensure that we are in proper state
31
34
  # event :reset_password, :active => :active
32
-
35
+
33
36
  def send_activation_email(code)
34
37
  true
35
38
  end
@@ -14,46 +14,59 @@ module SimpleStateMachine::ActiveRecord
14
14
  def decorate transition
15
15
  super transition
16
16
  event_name = transition.event_name.to_s
17
- event_name_and_save = "#{event_name}_and_save"
18
- unless @subject.method_defined?(event_name_and_save)
19
- @subject.send(:define_method, event_name_and_save) do |*args|
20
- old_state = self.send(self.class.state_machine_definition.state_method)
21
- send "with_managed_state_#{event_name}", *args
22
- if !self.errors.entries.empty?
23
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
24
- return false
25
- else
26
- if save
27
- return true
28
- else
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?
29
31
  self.send("#{self.class.state_machine_definition.state_method}=", old_state)
30
- return false
32
+ else
33
+ if save
34
+ result = true
35
+ else
36
+ self.send("#{self.class.state_machine_definition.state_method}=", old_state)
37
+ end
31
38
  end
39
+ return result
32
40
  end
41
+ @subject.send :alias_method, "#{event_name}", event_name_and_save
33
42
  end
34
- @subject.send :alias_method, "#{transition.event_name}", event_name_and_save
35
43
  end
36
- event_name_and_save_bang = "#{event_name_and_save}!"
37
- unless @subject.method_defined?(event_name_and_save_bang)
38
- @subject.send(:define_method, event_name_and_save_bang) do |*args|
39
- old_state = self.send(self.class.state_machine_definition.state_method)
40
- send "with_managed_state_#{event_name}", *args
41
- if !self.errors.entries.empty?
42
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
43
- raise ActiveRecord::RecordInvalid.new(self)
44
- end
45
- begin
46
- save!
47
- rescue ActiveRecord::RecordInvalid
48
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
49
- raise #re raise
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
50
63
  end
64
+ @subject.send :alias_method, "#{event_name}!", event_name_and_save_bang
51
65
  end
52
- @subject.send :alias_method, "#{transition.event_name}!", event_name_and_save_bang
53
66
  end
54
- end
55
67
 
56
- protected
68
+ def decorate_ar_method
69
+ end
57
70
 
58
71
  def alias_event_methods event_name
59
72
  @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
@@ -68,7 +81,8 @@ module SimpleStateMachine::ActiveRecord
68
81
  def state_machine_definition
69
82
  unless @state_machine_definition
70
83
  @state_machine_definition = SimpleStateMachine::StateMachineDefinition.new
71
- @state_machine_definition.lazy_decorator = lambda { Decorator.new(self) }
84
+ @state_machine_definition.decorator_class = Decorator
85
+ @state_machine_definition.subject = self
72
86
  end
73
87
  @state_machine_definition
74
88
  end
@@ -4,7 +4,7 @@ require 'rails'
4
4
  module SimpleStateMachine
5
5
  class Railtie < Rails::Railtie
6
6
  rake_tasks do
7
- load "tasks/graphiz.rake"
7
+ load "tasks/graphviz.rake"
8
8
  end
9
9
  end
10
10
  end
@@ -11,7 +11,7 @@ module SimpleStateMachine
11
11
  def state_machine_definition
12
12
  unless @state_machine_definition
13
13
  @state_machine_definition = StateMachineDefinition.new
14
- @state_machine_definition.lazy_decorator = lambda { Decorator.new(self) }
14
+ @state_machine_definition.subject = self
15
15
  end
16
16
  @state_machine_definition
17
17
  end
@@ -22,6 +22,12 @@ module SimpleStateMachine
22
22
  state_machine_definition.decorator.decorate(transition)
23
23
  end
24
24
  end
25
+
26
+ def mount_state_machine mountable_class
27
+ self.state_machine_definition = mountable_class.new
28
+ self.state_machine_definition.subject = self
29
+ self.state_machine_definition.add_events
30
+ end
25
31
  end
26
32
  include Mountable
27
33
 
@@ -31,11 +37,7 @@ module SimpleStateMachine
31
37
 
32
38
  # mark the method as an event and specify how the state should transition
33
39
  def event event_name, state_transitions
34
- state_transitions.each do |froms, to|
35
- [froms].flatten.each do |from|
36
- state_machine_definition.add_transition(event_name, from, to)
37
- end
38
- end
40
+ state_machine_definition.define_event event_name, state_transitions
39
41
  end
40
42
 
41
43
  end
@@ -58,16 +60,33 @@ module SimpleStateMachine
58
60
  # Defines state machine transitions
59
61
  class StateMachineDefinition
60
62
 
61
- attr_writer :state_method, :decorator, :lazy_decorator
63
+ attr_writer :default_error_state, :state_method, :subject, :decorator,
64
+ :decorator_class
62
65
 
63
66
  def decorator
64
- @decorator ||= @lazy_decorator.call
67
+ @decorator ||= decorator_class.new(@subject)
68
+ end
69
+
70
+ def decorator_class
71
+ @decorator_class ||= Decorator
72
+ end
73
+
74
+ def default_error_state
75
+ @default_error_state && @default_error_state.to_s
65
76
  end
66
77
 
67
78
  def transitions
68
79
  @transitions ||= []
69
80
  end
70
81
 
82
+ def define_event event_name, state_transitions
83
+ state_transitions.each do |froms, to|
84
+ [froms].flatten.each do |from|
85
+ add_transition(event_name, from, to)
86
+ end
87
+ end
88
+ end
89
+
71
90
  def add_transition event_name, from, to
72
91
  transition = Transition.new(event_name, from, to)
73
92
  transitions << transition
@@ -83,21 +102,88 @@ module SimpleStateMachine
83
102
  transitions.map(&:to_s).join("\n")
84
103
  end
85
104
 
86
- # Graphiz dot format for rendering as a directional graph
87
- def to_graphiz_dot
88
- transitions.map { |t| t.to_graphiz_dot }.join(";")
105
+ module Graphviz
106
+ # Graphviz dot format for rendering as a directional graph
107
+ def to_graphviz_dot
108
+ transitions.map { |t| t.to_graphviz_dot }.sort.join(";")
109
+ end
110
+
111
+ # Generates a url that renders states and events as a directional graph.
112
+ # See http://code.google.com/apis/chart/docs/gallery/graphviz.html
113
+ def google_chart_url
114
+ "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape to_graphviz_dot}}"
115
+ end
89
116
  end
117
+ include Graphviz
118
+
119
+ module Inspector
120
+ def begin_states
121
+ from_states - to_states
122
+ end
123
+
124
+ def end_states
125
+ to_states - from_states
126
+ end
127
+
128
+ def states
129
+ (to_states + from_states).uniq
130
+ end
131
+
132
+ private
133
+
134
+ def from_states
135
+ to_uniq_sym(sample_transitions.map(&:from))
136
+ end
90
137
 
91
- # Generates a url that renders states and events as a directional graph.
92
- # See http://code.google.com/apis/chart/docs/gallery/graphviz.html
93
- def google_chart_url
94
- "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape to_graphiz_dot}}"
138
+ def to_states
139
+ to_uniq_sym(sample_transitions.map(&:to))
140
+ end
141
+
142
+ def to_uniq_sym(array)
143
+ array.map { |state| state.is_a?(String) ? state.to_sym : state }.uniq
144
+ end
145
+
146
+ def sample_transitions
147
+ (@subject || sample_subject).state_machine_definition.send :transitions
148
+ end
149
+
150
+ def sample_subject
151
+ self_class = self.class
152
+ sample = Class.new do
153
+ extend SimpleStateMachine::Mountable
154
+ mount_state_machine self_class
155
+ end
156
+ sample
157
+ end
95
158
  end
159
+ include Inspector
160
+
161
+ module Mountable
162
+ def event event_name, state_transitions
163
+ events << [event_name, state_transitions]
164
+ end
165
+
166
+ def events
167
+ @events ||= []
168
+ end
169
+
170
+ module InstanceMethods
171
+ def add_events
172
+ self.class.events.each do |event_name, state_transitions|
173
+ define_event event_name, state_transitions
174
+ end
175
+ end
176
+ end
177
+ end
178
+ extend Mountable
179
+ include Mountable::InstanceMethods
180
+
96
181
  end
97
182
 
98
183
  ##
99
184
  # Defines the state machine used by the instance
100
185
  class StateMachine
186
+ attr_reader :raised_error
101
187
 
102
188
  def initialize(subject)
103
189
  @subject = subject
@@ -116,13 +202,20 @@ module SimpleStateMachine
116
202
  end
117
203
 
118
204
  # Transitions to the next state if next_state exists.
205
+ # When an error occurs, it uses the error to determine next state.
206
+ # If no next state can be determined it transitions to the default error
207
+ # state if defined, otherwise the error is re-raised.
119
208
  # Calls illegal_event_callback event_name if no next_state is found
120
209
  def transition(event_name)
210
+ clear_raised_error
121
211
  if to = next_state(event_name)
122
212
  begin
123
213
  result = yield
124
214
  rescue => e
125
- if error_state = error_state(event_name, e)
215
+ error_state = error_state(event_name, e) ||
216
+ state_machine_definition.default_error_state
217
+ if error_state
218
+ @raised_error = e
126
219
  @subject.send("#{state_method}=", error_state)
127
220
  return result
128
221
  else
@@ -148,6 +241,10 @@ module SimpleStateMachine
148
241
 
149
242
  private
150
243
 
244
+ def clear_raised_error
245
+ @raised_error = nil
246
+ end
247
+
151
248
  def state_machine_definition
152
249
  @subject.class.state_machine_definition
153
250
  end
@@ -184,18 +281,17 @@ module SimpleStateMachine
184
281
 
185
282
  # returns true if it's a error transition for event_name and error
186
283
  def is_error_transition_for?(event_name, error)
187
- is_same_event?(event_name) && error.class == from
284
+ is_same_event?(event_name) && from.is_a?(Class) && error.is_a?(from)
188
285
  end
189
286
 
190
287
  def to_s
191
288
  "#{from}.#{event_name}! => #{to}"
192
289
  end
193
290
 
194
- def to_graphiz_dot
291
+ def to_graphviz_dot
195
292
  %("#{from}"->"#{to}"[label=#{event_name}])
196
293
  end
197
294
 
198
-
199
295
  private
200
296
 
201
297
  def is_same_event?(event_name)
@@ -214,12 +310,13 @@ module SimpleStateMachine
214
310
  attr_writer :subject
215
311
  def initialize(subject)
216
312
  @subject = subject
313
+ end
314
+
315
+ def decorate transition
217
316
  define_state_machine_method
218
317
  define_state_getter_method
219
318
  define_state_setter_method
220
- end
221
319
 
222
- def decorate transition
223
320
  define_state_helper_method(transition.from)
224
321
  define_state_helper_method(transition.to)
225
322
  define_event_method(transition.event_name)
@@ -229,8 +326,10 @@ module SimpleStateMachine
229
326
  private
230
327
 
231
328
  def define_state_machine_method
232
- @subject.send(:define_method, "state_machine") do
233
- @state_machine ||= StateMachine.new(self)
329
+ unless any_method_defined?("state_machine")
330
+ @subject.send(:define_method, "state_machine") do
331
+ @state_machine ||= StateMachine.new(self)
332
+ end
234
333
  end
235
334
  end
236
335
 
@@ -1,3 +1,3 @@
1
1
  module SimpleStateMachine
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.0.pre"
3
3
  end
@@ -0,0 +1,21 @@
1
+ namespace :ssm do
2
+ namespace :graph do
3
+ desc 'Generate a url for a google chart. You must specify class=ClassName'
4
+ task :url => :environment do
5
+ if clazz = ENV['class']
6
+ puts clazz.constantize.state_machine_definition.google_chart_url
7
+ else
8
+ puts "Missing argument: class. Please specify class=ClassName"
9
+ end
10
+ end
11
+
12
+ desc 'Opens the google chart in your browser. You must specify class=ClassNAME'
13
+ task :open => :environment do
14
+ if clazz = ENV['class']
15
+ `open '#{::CGI.unescape(clazz.constantize.state_machine_definition.google_chart_url)}'`
16
+ else
17
+ puts "Missing argument: class. Please specify class=ClassName"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -27,5 +27,6 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency "rspec"
28
28
  s.add_development_dependency "activerecord", "~>2.3.5"
29
29
  s.add_development_dependency "sqlite3-ruby"
30
+ s.add_development_dependency "ruby-debug"
30
31
  end
31
32
 
@@ -111,6 +111,24 @@ describe ActiveRecord do
111
111
  user.errors.entries.should == [['activation_code', 'is invalid']]
112
112
  end
113
113
 
114
+ it "rollsback if an exception is raised" do
115
+ user_class = Class.new(User)
116
+ user_class.instance_eval do
117
+ define_method :without_managed_state_invite do
118
+ User.create!(:name => 'name2') #this shouldn't be persisted
119
+ User.create! #this should raise an error
120
+ end
121
+ end
122
+ user_class.count.should == 0
123
+ user = user_class.create!(:name => 'name')
124
+ expect {
125
+ user.transaction { user.invite_and_save }
126
+ }.to raise_error(ActiveRecord::RecordInvalid,
127
+ "Validation failed: Name can't be blank")
128
+ user_class.count.should == 1
129
+ user_class.first.name.should == 'name'
130
+ user_class.first.should be_new
131
+ end
114
132
  end
115
133
 
116
134
  describe "event_and_save!" do
@@ -161,6 +179,25 @@ describe ActiveRecord do
161
179
  user.should be_invited
162
180
  end
163
181
 
182
+ it "rollsback if an exception is raised" do
183
+ user_class = Class.new(User)
184
+ user_class.instance_eval do
185
+ define_method :without_managed_state_invite do
186
+ User.create!(:name => 'name2') #this shouldn't be persisted
187
+ User.create! #this should raise an error
188
+ end
189
+ end
190
+ user_class.count.should == 0
191
+ user = user_class.create!(:name => 'name')
192
+ expect {
193
+ user.transaction { user.invite_and_save! }
194
+ }.to raise_error(ActiveRecord::RecordInvalid,
195
+ "Validation failed: Name can't be blank")
196
+ user_class.count.should == 1
197
+ user_class.first.name.should == 'name'
198
+ user_class.first.should be_new
199
+ end
200
+
164
201
  end
165
202
 
166
203
  describe "event" do
@@ -3,14 +3,19 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
3
  describe "Mountable" do
4
4
  before do
5
5
  mountable_class = Class.new(SimpleStateMachine::StateMachineDefinition) do
6
- def initialize(subject)
7
- self.lazy_decorator = lambda { SimpleStateMachine::Decorator.new(subject) }
8
- add_transition(:event, :state1, :state2)
6
+ event(:event, :state1 => :state2)
7
+
8
+ def decorator_class
9
+ SimpleStateMachine::Decorator
9
10
  end
10
11
  end
11
12
  klass = Class.new do
13
+ attr_accessor :event_called
12
14
  extend SimpleStateMachine::Mountable
13
- self.state_machine_definition = mountable_class.new self
15
+ mount_state_machine mountable_class
16
+ def without_managed_state_event
17
+ @event_called = true
18
+ end
14
19
  end
15
20
  @instance = klass.new
16
21
  @instance.state = 'state1'
@@ -21,4 +26,9 @@ describe "Mountable" do
21
26
  @instance.should_not be_state2
22
27
  end
23
28
 
29
+ it "calls existing methods" do
30
+ @instance.event
31
+ @instance.should be_state2
32
+ @instance.event_called.should == true
33
+ end
24
34
  end
@@ -2,7 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
  require 'cgi'
3
3
 
4
4
  describe SimpleStateMachine do
5
-
5
+
6
6
  it "has an error that extends RuntimeError" do
7
7
  SimpleStateMachine::IllegalStateTransitionError.superclass.should == RuntimeError
8
8
  end
@@ -18,109 +18,138 @@ describe SimpleStateMachine do
18
18
  end
19
19
  end
20
20
 
21
- it "changes state if event has multiple transitions" do
21
+ it "returns what the decorated method returns" do
22
22
  klass = Class.new(@klass)
23
23
  klass.instance_eval do
24
- event :event, :state1 => :state2, :state2 => :state3
24
+ event :event1, :state1 => :state2
25
+ define_method :event2 do
26
+ 'event2'
27
+ end
28
+ event :event2, :state2 => :state3
25
29
  end
26
30
  example = klass.new
27
- example.should be_state1
28
- example.event
29
- example.should be_state2
30
- example.event
31
- example.should be_state3
31
+ example.event1.should == nil
32
+ example.event2.should == 'event2'
32
33
  end
33
34
 
34
- it "changes state if event has multiple froms" do
35
+ it "calls existing methods" do
35
36
  klass = Class.new(@klass)
36
37
  klass.instance_eval do
37
- event :event, [:state1, :state2] => :state3
38
+ attr_accessor :event_called
39
+ define_method :event do
40
+ @event_called = true
41
+ end
42
+ event :event, :state1 => :state2
38
43
  end
39
44
  example = klass.new
40
45
  example.event
41
- example.should be_state3
42
- example = klass.new 'state2'
43
- example.should be_state2
44
- example.event
45
- example.should be_state3
46
+ example.event_called.should == true
46
47
  end
47
48
 
48
- it "changes state if event has all as from" do
49
- klass = Class.new(@klass)
50
- klass.instance_eval do
51
- event :other_event, :state1 => :state2
52
- event :event, :all => :state3
49
+ context "given an event has multiple transitions" do
50
+ it "changes state for all transitions" do
51
+ klass = Class.new(@klass)
52
+ klass.instance_eval do
53
+ event :event, :state1 => :state2, :state2 => :state3
54
+ end
55
+ example = klass.new
56
+ example.should be_state1
57
+ example.event
58
+ example.should be_state2
59
+ example.event
60
+ example.should be_state3
53
61
  end
54
- example = klass.new
55
- example.event
56
- example.should be_state3
57
- example = klass.new 'state2'
58
- example.should be_state2
59
- example.event
60
- example.should be_state3
61
62
  end
62
63
 
63
- it "changes state when state is a symbol instead of a string" do
64
- klass = Class.new(@klass)
65
- klass.instance_eval do
66
- event :event, :state1 => :state2
67
- end
68
- example = klass.new :state1
69
- example.state.should == :state1
70
- example.send(:event)
71
- example.should be_state2
64
+ context "given an event has multiple from states" do
65
+ it "changes state for all from states" do
66
+ klass = Class.new(@klass)
67
+ klass.instance_eval do
68
+ event :event, [:state1, :state2] => :state3
69
+ end
70
+ example = klass.new
71
+ example.event
72
+ example.should be_state3
73
+ example = klass.new 'state2'
74
+ example.should be_state2
75
+ example.event
76
+ example.should be_state3
77
+ end
72
78
  end
73
79
 
74
- it "changes state to error_state when error can be caught" do
75
- class_with_error = Class.new(@klass)
76
- class_with_error.instance_eval do
77
- define_method :raise_error do
78
- raise RuntimeError.new
80
+ context "given an event has :all as from state" do
81
+ it "changes state from all states" do
82
+ klass = Class.new(@klass)
83
+ klass.instance_eval do
84
+ event :other_event, :state1 => :state2
85
+ event :event, :all => :state3
79
86
  end
80
- event :raise_error, :state1 => :state2, RuntimeError => :failed
87
+ example = klass.new
88
+ example.event
89
+ example.should be_state3
90
+ example = klass.new 'state2'
91
+ example.should be_state2
92
+ example.event
93
+ example.should be_state3
81
94
  end
82
- example = class_with_error.new
83
- example.should be_state1
84
- example.raise_error
85
- example.should be_failed
86
95
  end
87
-
88
- it "raises an error if an invalid state_transition is called" do
89
- klass = Class.new(@klass)
90
- klass.instance_eval do
91
- event :event, :state1 => :state2
92
- event :event2, :state2 => :state3
93
- end
94
- example = klass.new
95
- lambda { example.event2 }.should raise_error(SimpleStateMachine::IllegalStateTransitionError, "You cannot 'event2' when state is 'state1'")
96
+
97
+ context "given state is a symbol instead of a string" do
98
+ it "changes state" do
99
+ klass = Class.new(@klass)
100
+ klass.instance_eval do
101
+ event :event, :state1 => :state2
102
+ end
103
+ example = klass.new :state1
104
+ example.state.should == :state1
105
+ example.send(:event)
106
+ example.should be_state2
107
+ end
96
108
  end
97
109
 
98
- it "returns what the decorated method returns" do
99
- klass = Class.new(@klass)
100
- klass.instance_eval do
101
- event :event1, :state1 => :state2
102
- define_method :event2 do
103
- 'event2'
110
+ context "given an RuntimeError begin state" do
111
+ it "changes state to error_state when error can be caught" do
112
+ class_with_error = Class.new(@klass)
113
+ class_with_error.instance_eval do
114
+ define_method :raise_error do
115
+ raise RuntimeError.new
116
+ end
117
+ event :raise_error, :state1 => :state2, RuntimeError => :failed
104
118
  end
105
- event :event2, :state2 => :state3
106
- end
107
- example = klass.new
108
- example.event1.should == nil
109
- example.event2.should == 'event2'
119
+ example = class_with_error.new
120
+ example.should be_state1
121
+ example.raise_error
122
+ example.should be_failed
123
+ end
124
+
125
+ it "changes state to error_state when error superclass can be caught" do
126
+ error_subclass = Class.new(RuntimeError)
127
+ class_with_error = Class.new(@klass)
128
+ class_with_error.instance_eval do
129
+ define_method :raise_error do
130
+ raise error_subclass.new
131
+ end
132
+ event :raise_error, :state1 => :state2, RuntimeError => :failed
133
+ end
134
+ example = class_with_error.new
135
+ example.should be_state1
136
+ example.raise_error
137
+ example.should be_failed
138
+ end
110
139
  end
111
140
 
112
- it "calls existing methods" do
113
- klass = Class.new(@klass)
114
- klass.instance_eval do
115
- attr_accessor :event_called
116
- define_method :event do
117
- @event_called = true
141
+ context "given an invalid state_transition is called" do
142
+ it "raises an IllegalStateTransitionError" do
143
+ klass = Class.new(@klass)
144
+ klass.instance_eval do
145
+ event :event, :state1 => :state2
146
+ event :event2, :state2 => :state3
118
147
  end
119
- event :event, :state1 => :state2
120
- end
121
- example = klass.new
122
- example.event
123
- example.event_called.should == true
148
+ example = klass.new
149
+ lambda { example.event2 }.should raise_error(
150
+ SimpleStateMachine::IllegalStateTransitionError,
151
+ "You cannot 'event2' when state is 'state1'")
152
+ end
124
153
  end
125
154
 
126
155
  end
@@ -8,7 +8,7 @@ describe SimpleStateMachine::StateMachineDefinition do
8
8
  def initialize(state = 'state1')
9
9
  @state = state
10
10
  end
11
- event :event1, :state1 => :state2, :state2 => :state3
11
+ event :event1, :state1 => :state2, :state2 => :state3
12
12
  end
13
13
  @smd = @klass.state_machine_definition
14
14
  end
@@ -53,13 +53,103 @@ describe SimpleStateMachine::StateMachineDefinition do
53
53
  subject.event2
54
54
  subject.should be_state3
55
55
  end
56
-
56
+
57
57
  it "raise an error if an invalid state_transition is called" do
58
58
  lambda { subject.event2 }.should raise_error(SimpleStateMachine::IllegalStateTransitionError, "You cannot 'event2' when state is 'state1'")
59
59
  end
60
60
 
61
61
  end
62
62
 
63
+ describe '#default_error_state' do
64
+ subject do
65
+ klass = Class.new do
66
+ attr_reader :state
67
+ extend SimpleStateMachine
68
+ state_machine_definition.default_error_state = :failed
69
+
70
+ def initialize(state = 'state1')
71
+ @state = state
72
+ end
73
+
74
+ def event1
75
+ raise "Some error during event"
76
+ end
77
+ event :event1, :state1 => :state2
78
+ end
79
+ klass.new
80
+ end
81
+
82
+ it "is set when an error occurs during an event" do
83
+ subject.state.should == 'state1'
84
+ subject.event1
85
+ subject.state.should == 'failed'
86
+ end
87
+ end
88
+
89
+ describe "#begin_states" do
90
+ before do
91
+ @klass = Class.new(SimpleStateMachine::StateMachineDefinition) do
92
+ def add_events
93
+ define_event(:event_a, :state1 => :state2)
94
+ define_event(:event_b, :state2 => :state3)
95
+ define_event(:event_c, :state1 => :state3)
96
+ define_event(:event_c, RuntimeError => :state3)
97
+ end
98
+
99
+ def decorator_class
100
+ SimpleStateMachine::Decorator
101
+ end
102
+ end
103
+ end
104
+
105
+ it "returns all 'from' states that aren't 'to' states" do
106
+ @klass.new.begin_states.should == [:state1, RuntimeError]
107
+ end
108
+ end
109
+
110
+ describe "#end_states" do
111
+ before do
112
+ @klass = Class.new(SimpleStateMachine::StateMachineDefinition) do
113
+ def add_events
114
+ define_event(:event_a, :state1 => :state2)
115
+ define_event(:event_b, :state2 => :state3)
116
+ define_event(:event_c, :state1 => :state3)
117
+ end
118
+
119
+ def decorator_class
120
+ SimpleStateMachine::Decorator
121
+ end
122
+
123
+ end
124
+ end
125
+
126
+ it "returns all 'to' states that aren't 'from' states" do
127
+ @klass.new.end_states.should == [:state3]
128
+ end
129
+ end
130
+
131
+ describe "#states" do
132
+ before do
133
+ @klass = Class.new(SimpleStateMachine::StateMachineDefinition) do
134
+ def add_events
135
+ define_event(:event_a, :state1 => :state2)
136
+ define_event(:event_b, :state2 => :state3)
137
+ define_event(:event_c, :state1 => :state3)
138
+ end
139
+
140
+ def decorator_class
141
+ SimpleStateMachine::Decorator
142
+ end
143
+
144
+ end
145
+ end
146
+
147
+ it "returns all states" do
148
+ @klass.new.states.map(&:to_s).sort.should == %w{state1 state2 state3}
149
+ end
150
+ end
151
+
152
+
63
153
  describe "#transitions" do
64
154
  it "has a list of transitions" do
65
155
  @smd.transitions.should be_a(Array)
@@ -73,16 +163,15 @@ describe SimpleStateMachine::StateMachineDefinition do
73
163
  end
74
164
  end
75
165
 
76
- describe "#to_graphiz_dot" do
77
- it "converts to graphiz dot format" do
78
- @smd.to_graphiz_dot.should == %("state1"->"state2"[label=event1];"state2"->"state3"[label=event1])
166
+ describe "#to_graphviz_dot" do
167
+ it "converts to graphviz dot format" do
168
+ @smd.to_graphviz_dot.should == %("state1"->"state2"[label=event1];"state2"->"state3"[label=event1])
79
169
  end
80
170
  end
81
171
 
82
172
  describe "#google_chart_url" do
83
173
  it "shows the state and event dependencies as a Google chart" do
84
- puts "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape @smd.to_graphiz_dot}}"
85
- @smd.google_chart_url.should == "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape @smd.to_graphiz_dot}}"
174
+ @smd.google_chart_url.should == "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape @smd.to_graphviz_dot}}"
86
175
  end
87
176
  end
88
177
  end
@@ -22,5 +22,39 @@ describe SimpleStateMachine::StateMachine do
22
22
  end
23
23
  end
24
24
 
25
+ describe "#raised_error" do
26
+ context "given an error can be caught" do
27
+ let(:class_with_error) do
28
+ Class.new do
29
+ extend SimpleStateMachine
30
+ def initialize(state = 'state1'); @state = state; end
31
+ def raise_error
32
+ raise "Something went wrong"
33
+ end
34
+ event :raise_error, :state1 => :state2,
35
+ RuntimeError => :failed
36
+ event :retry, :failed => :success
37
+ end
38
+ end
39
+
40
+ it "the raised error is accessible" do
41
+ example = class_with_error.new
42
+ example.raise_error
43
+ raised_error = example.state_machine.raised_error
44
+ raised_error.should be_a(RuntimeError)
45
+ raised_error.message.should == "Something went wrong"
46
+ end
47
+
48
+ it "the raised error is set to nil on the next transition" do
49
+ example = class_with_error.new
50
+ example.raise_error
51
+ example.state_machine.raised_error.should be
52
+ example.retry
53
+ example.state_machine.raised_error.should_not be
54
+ end
55
+
56
+ end
57
+ end
58
+
25
59
  end
26
60
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
5
- prerelease:
4
+ hash: 961916020
5
+ prerelease: 6
6
6
  segments:
7
7
  - 0
8
- - 5
9
- - 3
10
- version: 0.5.3
8
+ - 6
9
+ - 0
10
+ - pre
11
+ version: 0.6.0.pre
11
12
  platform: ruby
12
13
  authors:
13
14
  - Marek de Heus
@@ -16,7 +17,7 @@ autorequire:
16
17
  bindir: bin
17
18
  cert_chain: []
18
19
 
19
- date: 2011-03-06 00:00:00 +01:00
20
+ date: 2011-07-06 00:00:00 +02:00
20
21
  default_executable:
21
22
  dependencies:
22
23
  - !ruby/object:Gem::Dependency
@@ -91,6 +92,20 @@ dependencies:
91
92
  version: "0"
92
93
  type: :development
93
94
  version_requirements: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ name: ruby-debug
97
+ prerelease: false
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ type: :development
108
+ version_requirements: *id006
94
109
  description: Simple State Machine is a state machine that focuses on events instead of states
95
110
  email:
96
111
  - FIX@example.com
@@ -120,8 +135,7 @@ files:
120
135
  - lib/simple_state_machine/railtie.rb
121
136
  - lib/simple_state_machine/simple_state_machine.rb
122
137
  - lib/simple_state_machine/version.rb
123
- - lib/tasks/graphiz.rake
124
- - rails/graphiz.rake
138
+ - lib/tasks/graphviz.rake
125
139
  - simple_state_machine.gemspec
126
140
  - spec/active_record_spec.rb
127
141
  - spec/decorator_spec.rb
@@ -152,12 +166,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
166
  required_rubygems_version: !ruby/object:Gem::Requirement
153
167
  none: false
154
168
  requirements:
155
- - - ">="
169
+ - - ">"
156
170
  - !ruby/object:Gem::Version
157
- hash: 3
171
+ hash: 25
158
172
  segments:
159
- - 0
160
- version: "0"
173
+ - 1
174
+ - 3
175
+ - 1
176
+ version: 1.3.1
161
177
  requirements: []
162
178
 
163
179
  rubyforge_project:
@@ -1,13 +0,0 @@
1
- namespace :ssm do
2
- namespace :graph do
3
- desc 'Generate a url for a google chart for [class]'
4
- task :url => :environment do
5
- puts ENV['class'].constantize.state_machine_definition.google_chart_url
6
- end
7
-
8
- desc 'Opens the google chart in the browser for [class]'
9
- task :open => :environment do
10
- `open '#{::CGI.unescape(ENV['class'].constantize.state_machine_definition.google_chart_url)}'`
11
- end
12
- end
13
- end
@@ -1,16 +0,0 @@
1
- namespace :ssm do
2
-
3
- namespace :graph do
4
-
5
- desc 'url'
6
- task 'url' do |t|
7
- end
8
-
9
- desc 'image'
10
- task 'image' do |t|
11
- end
12
-
13
- end
14
-
15
- end
16
-