simple_state_machine 0.5.3 → 0.6.2

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/Changelog.rdoc +23 -0
  3. data/README.rdoc +154 -100
  4. data/lib/simple_state_machine/active_record.rb +2 -62
  5. data/lib/simple_state_machine/decorator/active_record.rb +68 -0
  6. data/lib/simple_state_machine/decorator/default.rb +91 -0
  7. data/lib/simple_state_machine/railtie.rb +1 -1
  8. data/lib/simple_state_machine/simple_state_machine.rb +8 -251
  9. data/lib/simple_state_machine/state_machine.rb +88 -0
  10. data/lib/simple_state_machine/state_machine_definition.rb +72 -0
  11. data/lib/simple_state_machine/tools/graphviz.rb +21 -0
  12. data/lib/simple_state_machine/tools/inspector.rb +44 -0
  13. data/lib/simple_state_machine/transition.rb +40 -0
  14. data/lib/simple_state_machine/version.rb +1 -1
  15. data/lib/simple_state_machine.rb +13 -3
  16. data/lib/tasks/graphviz.rake +31 -0
  17. metadata +37 -150
  18. data/.gitignore +0 -1
  19. data/.rspec +0 -3
  20. data/Gemfile +0 -4
  21. data/Rakefile +0 -28
  22. data/autotest/discover.rb +0 -1
  23. data/examples/conversation.rb +0 -33
  24. data/examples/lamp.rb +0 -21
  25. data/examples/relationship.rb +0 -87
  26. data/examples/traffic_light.rb +0 -17
  27. data/examples/user.rb +0 -37
  28. data/lib/tasks/graphiz.rake +0 -13
  29. data/rails/graphiz.rake +0 -16
  30. data/simple_state_machine.gemspec +0 -31
  31. data/spec/active_record_spec.rb +0 -223
  32. data/spec/decorator_spec.rb +0 -195
  33. data/spec/examples_spec.rb +0 -60
  34. data/spec/mountable_spec.rb +0 -24
  35. data/spec/simple_state_machine_spec.rb +0 -129
  36. data/spec/spec_helper.rb +0 -7
  37. data/spec/state_machine_definition_spec.rb +0 -89
  38. data/spec/state_machine_spec.rb +0 -26
@@ -1,17 +1,12 @@
1
1
  module SimpleStateMachine
2
2
 
3
- require 'cgi'
4
-
5
- class IllegalStateTransitionError < ::RuntimeError
6
- end
7
-
8
3
  ##
9
4
  # Allows class to mount a state_machine
10
5
  module Mountable
11
6
  def state_machine_definition
12
7
  unless @state_machine_definition
13
8
  @state_machine_definition = StateMachineDefinition.new
14
- @state_machine_definition.lazy_decorator = lambda { Decorator.new(self) }
9
+ @state_machine_definition.subject = self
15
10
  end
16
11
  @state_machine_definition
17
12
  end
@@ -22,22 +17,22 @@ module SimpleStateMachine
22
17
  state_machine_definition.decorator.decorate(transition)
23
18
  end
24
19
  end
20
+
21
+ def mount_state_machine mountable_class
22
+ self.state_machine_definition = mountable_class.new
23
+ self.state_machine_definition.subject = self
24
+ self.state_machine_definition.add_events
25
+ end
25
26
  end
26
27
  include Mountable
27
28
 
28
29
  ##
29
30
  # Adds state machine methods to the extended class
30
31
  module Extendable
31
-
32
32
  # mark the method as an event and specify how the state should transition
33
33
  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
34
+ state_machine_definition.define_event event_name, state_transitions
39
35
  end
40
-
41
36
  end
42
37
  include Extendable
43
38
 
@@ -54,242 +49,4 @@ module SimpleStateMachine
54
49
  end
55
50
  include Inheritable
56
51
 
57
- ##
58
- # Defines state machine transitions
59
- class StateMachineDefinition
60
-
61
- attr_writer :state_method, :decorator, :lazy_decorator
62
-
63
- def decorator
64
- @decorator ||= @lazy_decorator.call
65
- end
66
-
67
- def transitions
68
- @transitions ||= []
69
- end
70
-
71
- def add_transition event_name, from, to
72
- transition = Transition.new(event_name, from, to)
73
- transitions << transition
74
- decorator.decorate(transition)
75
- end
76
-
77
- def state_method
78
- @state_method ||= :state
79
- end
80
-
81
- # Human readable format: old_state.event! => new_state
82
- def to_s
83
- transitions.map(&:to_s).join("\n")
84
- end
85
-
86
- # Graphiz dot format for rendering as a directional graph
87
- def to_graphiz_dot
88
- transitions.map { |t| t.to_graphiz_dot }.join(";")
89
- end
90
-
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}}"
95
- end
96
- end
97
-
98
- ##
99
- # Defines the state machine used by the instance
100
- class StateMachine
101
-
102
- def initialize(subject)
103
- @subject = subject
104
- end
105
-
106
- # Returns the next state for the subject for event_name
107
- def next_state(event_name)
108
- transition = transitions.select{|t| t.is_transition_for?(event_name, @subject.send(state_method))}.first
109
- transition ? transition.to : nil
110
- end
111
-
112
- # Returns the error state for the subject for event_name and error
113
- def error_state(event_name, error)
114
- transition = transitions.select{|t| t.is_error_transition_for?(event_name, error) }.first
115
- transition ? transition.to : nil
116
- end
117
-
118
- # Transitions to the next state if next_state exists.
119
- # Calls illegal_event_callback event_name if no next_state is found
120
- def transition(event_name)
121
- if to = next_state(event_name)
122
- begin
123
- result = yield
124
- rescue => e
125
- if error_state = error_state(event_name, e)
126
- @subject.send("#{state_method}=", error_state)
127
- return result
128
- else
129
- raise
130
- end
131
- end
132
- # TODO refactor out to AR module
133
- if defined?(::ActiveRecord) && @subject.is_a?(::ActiveRecord::Base)
134
- if @subject.errors.entries.empty?
135
- @subject.send("#{state_method}=", to)
136
- return true
137
- else
138
- return false
139
- end
140
- else
141
- @subject.send("#{state_method}=", to)
142
- return result
143
- end
144
- else
145
- illegal_event_callback event_name
146
- end
147
- end
148
-
149
- private
150
-
151
- def state_machine_definition
152
- @subject.class.state_machine_definition
153
- end
154
-
155
- def transitions
156
- state_machine_definition.transitions
157
- end
158
-
159
- def state_method
160
- state_machine_definition.state_method
161
- end
162
-
163
- # override with your own implementation, like setting errors in your model
164
- def illegal_event_callback event_name
165
- raise IllegalStateTransitionError.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
166
- end
167
-
168
- end
169
-
170
- ##
171
- # Defines transitions for events
172
- class Transition
173
- attr_reader :event_name, :from, :to
174
- def initialize(event_name, from, to)
175
- @event_name = event_name.to_s
176
- @from = from.is_a?(Class) ? from : from.to_s
177
- @to = to.to_s
178
- end
179
-
180
- # returns true if it's a transition for event_name
181
- def is_transition_for?(event_name, subject_state)
182
- is_same_event?(event_name) && is_same_from?(subject_state)
183
- end
184
-
185
- # returns true if it's a error transition for event_name and error
186
- def is_error_transition_for?(event_name, error)
187
- is_same_event?(event_name) && error.class == from
188
- end
189
-
190
- def to_s
191
- "#{from}.#{event_name}! => #{to}"
192
- end
193
-
194
- def to_graphiz_dot
195
- %("#{from}"->"#{to}"[label=#{event_name}])
196
- end
197
-
198
-
199
- private
200
-
201
- def is_same_event?(event_name)
202
- self.event_name == event_name.to_s
203
- end
204
-
205
- def is_same_from?(subject_from)
206
- from.to_s == 'all' || subject_from.to_s == from.to_s
207
- end
208
- end
209
-
210
- ##
211
- # Decorates @subject with methods to access the state machine
212
- class Decorator
213
-
214
- attr_writer :subject
215
- def initialize(subject)
216
- @subject = subject
217
- define_state_machine_method
218
- define_state_getter_method
219
- define_state_setter_method
220
- end
221
-
222
- def decorate transition
223
- define_state_helper_method(transition.from)
224
- define_state_helper_method(transition.to)
225
- define_event_method(transition.event_name)
226
- decorate_event_method(transition.event_name)
227
- end
228
-
229
- private
230
-
231
- def define_state_machine_method
232
- @subject.send(:define_method, "state_machine") do
233
- @state_machine ||= StateMachine.new(self)
234
- end
235
- end
236
-
237
- def define_state_helper_method state
238
- unless any_method_defined?("#{state.to_s}?")
239
- @subject.send(:define_method, "#{state.to_s}?") do
240
- self.send(self.class.state_machine_definition.state_method) == state.to_s
241
- end
242
- end
243
- end
244
-
245
- def define_event_method event_name
246
- unless any_method_defined?("#{event_name}")
247
- @subject.send(:define_method, "#{event_name}") {}
248
- end
249
- end
250
-
251
- def decorate_event_method event_name
252
- # TODO put in transaction for activeRecord?
253
- unless @subject.method_defined?("with_managed_state_#{event_name}")
254
- @subject.send(:define_method, "with_managed_state_#{event_name}") do |*args|
255
- return state_machine.transition(event_name) do
256
- send("without_managed_state_#{event_name}", *args)
257
- end
258
- end
259
- alias_event_methods event_name
260
- end
261
- end
262
-
263
- def define_state_setter_method
264
- unless any_method_defined?("#{state_method}=")
265
- @subject.send(:define_method, "#{state_method}=") do |new_state|
266
- instance_variable_set(:"@#{self.class.state_machine_definition.state_method}", new_state)
267
- end
268
- end
269
- end
270
-
271
- def define_state_getter_method
272
- unless any_method_defined?(state_method)
273
- @subject.send(:attr_reader, state_method)
274
- end
275
- end
276
-
277
- def any_method_defined?(method)
278
- @subject.method_defined?(method) ||
279
- @subject.protected_method_defined?(method) ||
280
- @subject.private_method_defined?(method)
281
- end
282
-
283
- protected
284
-
285
- def alias_event_methods event_name
286
- @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
287
- @subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
288
- end
289
-
290
- def state_method
291
- @subject.state_machine_definition.state_method
292
- end
293
- end
294
-
295
52
  end
@@ -0,0 +1,88 @@
1
+ module SimpleStateMachine
2
+
3
+ class IllegalStateTransitionError < ::RuntimeError; end
4
+
5
+ ##
6
+ # Defines the state machine used by the instance
7
+ class StateMachine
8
+ attr_reader :raised_error
9
+
10
+ def initialize(subject)
11
+ @subject = subject
12
+ end
13
+
14
+ # Returns the next state for the subject for event_name
15
+ def next_state(event_name)
16
+ transition = transitions.select{|t| t.is_transition_for?(event_name, @subject.send(state_method))}.first
17
+ transition ? transition.to : nil
18
+ end
19
+
20
+ # Returns the error state for the subject for event_name and error
21
+ def error_state(event_name, error)
22
+ transition = transitions.select{|t| t.is_error_transition_for?(event_name, error) }.first
23
+ transition ? transition.to : nil
24
+ end
25
+
26
+ # Transitions to the next state if next_state exists.
27
+ # When an error occurs, it uses the error to determine next state.
28
+ # If no next state can be determined it transitions to the default error
29
+ # state if defined, otherwise the error is re-raised.
30
+ # Calls illegal_event_callback event_name if no next_state is found
31
+ def transition(event_name)
32
+ clear_raised_error
33
+ if to = next_state(event_name)
34
+ begin
35
+ result = yield
36
+ rescue => e
37
+ error_state = error_state(event_name, e) ||
38
+ state_machine_definition.default_error_state
39
+ if error_state
40
+ @raised_error = e
41
+ @subject.send("#{state_method}=", error_state)
42
+ return result
43
+ else
44
+ raise
45
+ end
46
+ end
47
+ # TODO refactor out to AR module
48
+ if defined?(::ActiveRecord) && @subject.is_a?(::ActiveRecord::Base)
49
+ if @subject.errors.entries.empty?
50
+ @subject.send("#{state_method}=", to)
51
+ return true
52
+ else
53
+ return false
54
+ end
55
+ else
56
+ @subject.send("#{state_method}=", to)
57
+ return result
58
+ end
59
+ else
60
+ illegal_event_callback event_name
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def clear_raised_error
67
+ @raised_error = nil
68
+ end
69
+
70
+ def state_machine_definition
71
+ @subject.class.state_machine_definition
72
+ end
73
+
74
+ def transitions
75
+ state_machine_definition.transitions
76
+ end
77
+
78
+ def state_method
79
+ state_machine_definition.state_method
80
+ end
81
+
82
+ # override with your own implementation, like setting errors in your model
83
+ def illegal_event_callback event_name
84
+ raise IllegalStateTransitionError.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,72 @@
1
+ module SimpleStateMachine
2
+ ##
3
+ # Defines state machine transitions
4
+ class StateMachineDefinition
5
+
6
+ attr_writer :default_error_state, :state_method, :subject, :decorator,
7
+ :decorator_class
8
+
9
+ def decorator
10
+ @decorator ||= decorator_class.new(@subject)
11
+ end
12
+
13
+ def decorator_class
14
+ @decorator_class ||= Decorator::Default
15
+ end
16
+
17
+ def default_error_state
18
+ @default_error_state && @default_error_state.to_s
19
+ end
20
+
21
+ def transitions
22
+ @transitions ||= []
23
+ end
24
+
25
+ def define_event event_name, state_transitions
26
+ state_transitions.each do |froms, to|
27
+ [froms].flatten.each do |from|
28
+ add_transition(event_name, from, to)
29
+ end
30
+ end
31
+ end
32
+
33
+ def add_transition event_name, from, to
34
+ transition = Transition.new(event_name, from, to)
35
+ transitions << transition
36
+ decorator.decorate(transition)
37
+ end
38
+
39
+ def state_method
40
+ @state_method ||= :state
41
+ end
42
+
43
+ # Human readable format: old_state.event! => new_state
44
+ def to_s
45
+ transitions.map(&:to_s).join("\n")
46
+ end
47
+
48
+ module Mountable
49
+ def event event_name, state_transitions
50
+ events << [event_name, state_transitions]
51
+ end
52
+
53
+ def events
54
+ @events ||= []
55
+ end
56
+
57
+ module InstanceMethods
58
+ def add_events
59
+ self.class.events.each do |event_name, state_transitions|
60
+ define_event event_name, state_transitions
61
+ end
62
+ end
63
+ end
64
+ end
65
+ extend Mountable
66
+ include Mountable::InstanceMethods
67
+
68
+ # TODO only include in rake task?
69
+ include ::SimpleStateMachine::Tools::Graphviz
70
+ include ::SimpleStateMachine::Tools::Inspector
71
+ end
72
+ end
@@ -0,0 +1,21 @@
1
+ module SimpleStateMachine
2
+ module Tools
3
+ require 'cgi'
4
+ module Graphviz
5
+ # Graphviz dot format for rendering as a directional graph
6
+ def to_graphviz_dot
7
+ "digraph G {\n" +
8
+ transitions.map { |t| t.to_graphviz_dot }.sort.join(";\n") +
9
+ "\n}"
10
+ end
11
+
12
+ # Generates a url that renders states and events as a directional graph.
13
+ # See http://code.google.com/apis/chart/docs/gallery/graphviz.html
14
+ def google_chart_url
15
+ graph = transitions.map { |t| t.to_graphviz_dot }.sort.join(";")
16
+ puts graph
17
+ "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape graph}}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ module SimpleStateMachine
2
+ module Tools
3
+ module Inspector
4
+ def begin_states
5
+ from_states - to_states
6
+ end
7
+
8
+ def end_states
9
+ to_states - from_states
10
+ end
11
+
12
+ def states
13
+ (to_states + from_states).uniq
14
+ end
15
+
16
+ private
17
+
18
+ def from_states
19
+ to_uniq_sym(sample_transitions.map(&:from))
20
+ end
21
+
22
+ def to_states
23
+ to_uniq_sym(sample_transitions.map(&:to))
24
+ end
25
+
26
+ def to_uniq_sym(array)
27
+ array.map { |state| state.is_a?(String) ? state.to_sym : state }.uniq
28
+ end
29
+
30
+ def sample_transitions
31
+ (@subject || sample_subject).state_machine_definition.send :transitions
32
+ end
33
+
34
+ def sample_subject
35
+ self_class = self.class
36
+ sample = Class.new do
37
+ extend SimpleStateMachine::Mountable
38
+ mount_state_machine self_class
39
+ end
40
+ sample
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ module SimpleStateMachine
2
+ # Defines transitions for events
3
+ class Transition
4
+ attr_reader :event_name, :from, :to
5
+ def initialize(event_name, from, to)
6
+ @event_name = event_name.to_s
7
+ @from = from.is_a?(Class) ? from : from.to_s
8
+ @to = to.to_s
9
+ end
10
+
11
+ # returns true if it's a transition for event_name and subject_state
12
+ def is_transition_for?(event_name, subject_state)
13
+ is_same_event?(event_name) && is_same_from?(subject_state)
14
+ end
15
+
16
+ # returns true if it's a error transition for event_name and error
17
+ def is_error_transition_for?(event_name, error)
18
+ is_same_event?(event_name) && from.is_a?(Class) && error.is_a?(from)
19
+ end
20
+
21
+ def to_s
22
+ "#{from}.#{event_name}! => #{to}"
23
+ end
24
+
25
+ # TODO move to Graphiz module
26
+ def to_graphviz_dot
27
+ %("#{from}"->"#{to}"[label=#{event_name}])
28
+ end
29
+
30
+ private
31
+
32
+ def is_same_event?(event_name)
33
+ self.event_name == event_name.to_s
34
+ end
35
+
36
+ def is_same_from?(subject_from)
37
+ from.to_s == 'all' || subject_from.to_s == from.to_s
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module SimpleStateMachine
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.2"
3
3
  end
@@ -1,4 +1,14 @@
1
1
  require 'simple_state_machine/simple_state_machine'
2
- require 'simple_state_machine/active_record'
3
- require "simple_state_machine/railtie" if defined?(Rails::Railtie)
4
-
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
12
+ if defined?(Rails::Railtie)
13
+ require "simple_state_machine/railtie"
14
+ end
@@ -0,0 +1,31 @@
1
+ namespace :ssm do
2
+ namespace :graph do
3
+
4
+ desc 'Outputs a dot file. You must specify class=ClassNAME'
5
+ task :dot => :environment do
6
+ if clazz = ENV['class']
7
+ puts clazz.constantize.state_machine_definition.to_graphviz_dot
8
+ else
9
+ puts "Missing argument: class. Please specify class=ClassName"
10
+ end
11
+ end
12
+
13
+ desc 'Generate a url for a google chart. You must specify class=ClassName'
14
+ task :url => :environment do
15
+ if clazz = ENV['class']
16
+ puts clazz.constantize.state_machine_definition.google_chart_url
17
+ else
18
+ puts "Missing argument: class. Please specify class=ClassName"
19
+ end
20
+ end
21
+
22
+ desc 'Opens the google chart in your browser. You must specify class=ClassNAME'
23
+ task :open => :environment do
24
+ if clazz = ENV['class']
25
+ `open '#{::CGI.unescape(clazz.constantize.state_machine_definition.google_chart_url)}'`
26
+ else
27
+ puts "Missing argument: class. Please specify class=ClassName"
28
+ end
29
+ end
30
+ end
31
+ end