simple_state_machine 0.5.3 → 0.6.0.pre

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