simple_state_machine 0.5.3 → 0.6.2

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