MINT-statemachine 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGES +135 -0
  2. data/LICENSE +16 -0
  3. data/MINT-statemachine.gemspec +27 -0
  4. data/README.rdoc +69 -0
  5. data/Rakefile +88 -0
  6. data/TODO +2 -0
  7. data/lib/statemachine.rb +26 -0
  8. data/lib/statemachine/action_invokation.rb +83 -0
  9. data/lib/statemachine/builder.rb +383 -0
  10. data/lib/statemachine/generate/dot_graph.rb +1 -0
  11. data/lib/statemachine/generate/dot_graph/dot_graph_statemachine.rb +127 -0
  12. data/lib/statemachine/generate/java.rb +1 -0
  13. data/lib/statemachine/generate/java/java_statemachine.rb +265 -0
  14. data/lib/statemachine/generate/src_builder.rb +48 -0
  15. data/lib/statemachine/generate/util.rb +50 -0
  16. data/lib/statemachine/parallelstate.rb +196 -0
  17. data/lib/statemachine/state.rb +102 -0
  18. data/lib/statemachine/statemachine.rb +279 -0
  19. data/lib/statemachine/stub_context.rb +26 -0
  20. data/lib/statemachine/superstate.rb +53 -0
  21. data/lib/statemachine/transition.rb +76 -0
  22. data/lib/statemachine/version.rb +17 -0
  23. data/spec/action_invokation_spec.rb +101 -0
  24. data/spec/builder_spec.rb +243 -0
  25. data/spec/default_transition_spec.rb +111 -0
  26. data/spec/generate/dot_graph/dot_graph_stagemachine_spec.rb +27 -0
  27. data/spec/generate/java/java_statemachine_spec.rb +349 -0
  28. data/spec/history_spec.rb +107 -0
  29. data/spec/noodle.rb +23 -0
  30. data/spec/sm_action_parameterization_spec.rb +99 -0
  31. data/spec/sm_activation_spec.rb +116 -0
  32. data/spec/sm_entry_exit_actions_spec.rb +99 -0
  33. data/spec/sm_odds_n_ends_spec.rb +67 -0
  34. data/spec/sm_parallel_state_spec.rb +207 -0
  35. data/spec/sm_simple_spec.rb +26 -0
  36. data/spec/sm_super_state_spec.rb +55 -0
  37. data/spec/sm_turnstile_spec.rb +76 -0
  38. data/spec/spec_helper.rb +121 -0
  39. data/spec/transition_spec.rb +107 -0
  40. metadata +115 -0
@@ -0,0 +1,102 @@
1
+ module Statemachine
2
+
3
+ class State #:nodoc:
4
+
5
+ attr_reader :id, :statemachine
6
+ attr_accessor :entry_action, :exit_action, :superstate
7
+ attr_writer :default_transition
8
+
9
+ def initialize(id, superstate, state_machine)
10
+ @id = id
11
+ @superstate = superstate
12
+ @statemachine = state_machine
13
+ @transitions = {}
14
+ end
15
+
16
+ def add(transition)
17
+ @transitions[transition.event] = transition
18
+ end
19
+
20
+ def transitions
21
+ return @superstate ? @transitions.merge(@superstate.transitions) : @transitions
22
+ end
23
+
24
+ def non_default_transition_for(event)
25
+ transition = @transitions[event]
26
+ if @superstate
27
+ transition = @superstate.non_default_transition_for(event) if @superstate and @superstate.is_parallel == false and not transition
28
+ end
29
+ return transition
30
+ end
31
+
32
+ def default_transition
33
+ return @default_transition if @default_transition
34
+ return @superstate.default_transition if @superstate
35
+ return nil
36
+ end
37
+
38
+ def transition_for(event)
39
+ transition = non_default_transition_for(event)
40
+ transition = default_transition if not transition
41
+ return transition
42
+ end
43
+
44
+ def exit(args)
45
+ messenger = self.statemachine.messenger
46
+ message_queue = self.statemachine.message_queue
47
+ @statemachine.trace("\texiting #{self}")
48
+ @statemachine.invoke_action(@exit_action, args, "exit action for #{self}", messenger, message_queue) if @exit_action
49
+ @superstate.substate_exiting(self) if @superstate
50
+ end
51
+
52
+ def enter(args=[])
53
+ messenger = self.statemachine.messenger
54
+ message_queue = self.statemachine.message_queue
55
+ @statemachine.trace("\tentering #{self}")
56
+ @statemachine.invoke_action(@entry_action, args, "entry action for #{self}", messenger, message_queue) if @entry_action
57
+ end
58
+
59
+ def activate
60
+ @statemachine.state = self
61
+ if (@statemachine.is_parallel)
62
+ @statemachine.activation.call(self.id,@statemachine.is_parallel.abstract_states,@statemachine.is_parallel.statemachine.states_id) if @statemachine.activation
63
+ else
64
+
65
+ @statemachine.activation.call(self.id,@statemachine.abstract_states,@statemachine.states_id) if @statemachine.activation
66
+ end
67
+ end
68
+
69
+ def concrete?
70
+ return true
71
+ end
72
+
73
+ def resolve_startstate
74
+ return self
75
+ end
76
+
77
+ def reset
78
+
79
+ end
80
+
81
+ def to_s
82
+ return "'#{id}' state"
83
+ end
84
+
85
+ def has_superstate(id)
86
+ return false if not @superstate
87
+ return true if @superstate.id == id
88
+ return @superstate.has_superstate(id)
89
+ end
90
+
91
+ def abstract_states
92
+ return [] if not superstate
93
+ return @superstate.abstract_states if not @superstate.is_parallel
94
+ []
95
+ end
96
+
97
+ def is_parallel
98
+ false
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,279 @@
1
+ module Statemachine
2
+
3
+ class StatemachineException < Exception
4
+ end
5
+
6
+ class TransitionMissingException < Exception
7
+ end
8
+
9
+ # Used at runtime to execute the behavior of the statemachine.
10
+ # Should be created by using the Statemachine.build method.
11
+ #
12
+ # sm = Statemachine.build do
13
+ # trans :locked, :coin, :unlocked
14
+ # trans :unlocked, :pass, :locked:
15
+ # end
16
+ #
17
+ # sm.coin
18
+ # sm.state
19
+ #
20
+ # This class will accept any method that corresponds to an event. If the
21
+ # current state responds to the event, the appropriate transition will be invoked.
22
+ # Otherwise an exception will be raised.
23
+ class Statemachine
24
+ include ActionInvokation
25
+
26
+ # The tracer is an IO object. The statemachine will write run time execution
27
+ # information to the +tracer+. Can be helpful in debugging. Defaults to nil.
28
+ attr_accessor :tracer
29
+
30
+ # Provides access to the +context+ of the statemachine. The context is a object
31
+ # where all actions will be invoked. This provides a way to separate logic from
32
+ # behavior. The statemachine is responsible for all the logic and the context
33
+ # is responsible for all the behavior.
34
+ attr_reader :context
35
+
36
+ attr_reader :root, :states
37
+ attr_accessor :messenger, :message_queue, :is_parallel #:nodoc:
38
+ attr_accessor :activation
39
+
40
+ # Should not be called directly. Instances of Statemachine::Statemachine are created
41
+ # through the Statemachine.build method.
42
+ def initialize(root = Superstate.new(:root, nil, self))
43
+ @root = root
44
+ @states = {}
45
+ end
46
+
47
+ # Returns the id of the startstate of the statemachine.
48
+ def startstate
49
+ return @root.startstate_id
50
+ end
51
+
52
+ # Resets the statemachine back to its starting state.
53
+ def reset(startstate_id=nil)
54
+
55
+ if (startstate_id and @root.is_parallel) # called when enterin a parallel state or dierctly entering a child of a parallel state from outside the parallel state
56
+ @state = get_state(startstate_id)
57
+ else
58
+ @state = get_state(@root.startstate_id)
59
+ end
60
+ while @state and not @state.concrete?
61
+ @state = get_state(@state.startstate_id)
62
+ end
63
+ raise StatemachineException.new("The state machine doesn't know where to start. Try setting the startstate.") if @state == nil
64
+ @state.enter
65
+ @states.values.each { |state|
66
+ state.reset if not state.is_a? Parallelstate
67
+ }
68
+ end
69
+
70
+ def context= c
71
+ @context = c
72
+
73
+ p = get_parallel
74
+ if p
75
+ p.context = c
76
+ end
77
+ end
78
+
79
+ #Return the id of the current state of the statemachine.
80
+ def state
81
+ return @state.id
82
+ end
83
+
84
+ # returns an array with the ids of the current active states of the machine.
85
+ def states_id(atomic = true)
86
+ belongs, parallel = belongs_to_parallel(@state.id)
87
+ if belongs
88
+ return parallel.states
89
+ else
90
+ return [@state.id]
91
+ end
92
+ end
93
+
94
+ # returns an array with all currently active super states
95
+ def abstract_states
96
+ @state.abstract_states
97
+ end
98
+
99
+ # You may change the state of the statemachine by using this method. The parameter should be
100
+ # the id of the desired state.
101
+ def state= value
102
+ if value.is_a? State
103
+ @state = value
104
+ elsif @states[value]
105
+ @state = @states[value]
106
+ elsif value and @states[value.to_sym]
107
+ @state = @states[value.to_sym]
108
+ end
109
+ end
110
+
111
+ def states= values
112
+ if values.is_a? Array and values.length==1
113
+ self.state=self.get_state(values.first)
114
+ else
115
+ values.each do |v|
116
+ if @states.has_key? v
117
+ self.state=v
118
+ else
119
+ belongs,parallel = belongs_to_parallel(v)
120
+ if belongs
121
+ self.state=parallel.id
122
+ parallel.state=v
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ # The key method to exercise the statemachine. Any extra arguments supplied will be passed into
130
+ # any actions associated with the transition.
131
+ #
132
+ # Alternatively to this method, you may invoke methods, names the same as the event, on the statemachine.
133
+ # The advantage of using +process_event+ is that errors messages are more informative.
134
+ def process_event(event, *args)
135
+ event = event.to_sym
136
+ trace "Event: #{event}"
137
+ if @state
138
+ belongs, parallel = belongs_to_parallel(@state.id)
139
+ if belongs
140
+ r = parallel.process_event(event, *args)
141
+ return true if r
142
+ end
143
+ transition = @state.transition_for(event)
144
+ if transition
145
+ cond = true
146
+ if transition.cond!=true and transition.cond.is_a? Proc
147
+ cond = @state.statemachine.invoke_action(transition.cond, [], "condition from #{@state} invoked by '#{event}' event", nil, nil)
148
+ else
149
+ cond = instance_eval(transition.cond) if transition.cond != true and @is_parallel == nil
150
+ end
151
+ if cond
152
+ transition.invoke(@state, self, args)
153
+ end
154
+ else
155
+ raise TransitionMissingException.new("#{@state} does not respond to the '#{event}' event.")
156
+ end
157
+
158
+ else
159
+ raise StatemachineException.new("The state machine isn't in any state while processing the '#{event}' event.")
160
+ end
161
+ end
162
+
163
+ def trace(message) #:nodoc:
164
+ @tracer.puts message if @tracer
165
+ end
166
+
167
+ def belongs_to_parallel(id)
168
+ @states.each_value do |v|
169
+ # It doesn't belong to parallel, it is parallel
170
+ return [true, v] if v.id == id and v.is_a? Parallelstate
171
+ return [v.has_state(id),v] if v.is_a? Parallelstate
172
+ end
173
+ return [false, nil]
174
+ end
175
+
176
+ def get_parallel
177
+ @states.each_value do |v|
178
+ return v if v.is_a? Parallelstate
179
+ end
180
+ return false
181
+ end
182
+
183
+ def get_state(id) #:nodoc:
184
+ if @states.has_key? id
185
+ return @states[id]
186
+ elsif @is_parallel and @is_parallel.statemachine.get_state(id)
187
+ return @is_parallel.statemachine.states[id]
188
+ elsif p = get_parallel and s = p.get_state(id)
189
+ return s
190
+ elsif(is_history_state_id?(id))
191
+ superstate_id = base_id(id)
192
+ superstate = @states[superstate_id]
193
+ raise StatemachineException.new("No history exists for #{superstate} since it is not a super state.") if superstate.concrete?
194
+ return load_history(superstate)
195
+ elsif @is_parallel and @is_parallel.has_state(id)
196
+ @is_parallel.get_state(id)
197
+ else
198
+ state = State.new(id, @root, self)
199
+ @states[id] = state
200
+ return state
201
+ end
202
+ end
203
+
204
+ def add_state(state) #:nodoc:
205
+ @states[state.id] = state
206
+ end
207
+
208
+ def remove_state(state)
209
+ @states.delete(state.id)
210
+ end
211
+
212
+ def has_state(id) #:nodoc:
213
+ if(is_history_state_id?(id))
214
+ return @states.has_key?(base_id(id))
215
+ else
216
+ return @states.has_key?(id)
217
+ end
218
+ end
219
+
220
+ def respond_to?(message)
221
+ return true if super(message)
222
+ return true if @state and @state.transition_for(message)
223
+ return false
224
+ end
225
+
226
+ def method_missing(message, *args) #:nodoc:
227
+ if @state and @state.transition_for(message)
228
+ process_event(message.to_sym, *args)
229
+ # method = self.method(:process_event)
230
+ # params = [message.to_sym].concat(args)
231
+ # method.call(*params)
232
+ else
233
+ begin
234
+ super(message, args)
235
+ rescue NoMethodError
236
+ process_event(message.to_sym, *args)
237
+ end
238
+ end
239
+ end
240
+
241
+ def In(id)
242
+ # check if it is one of the actual states
243
+ return true if @state.id == id
244
+
245
+ # check if it is one of the superstates
246
+ return true if @state.has_superstate(id)
247
+
248
+ # check if it is one of the running parallel states
249
+ belongs, parallel = belongs_to_parallel(@state.id)
250
+ if belongs
251
+ return parallel.In(id)
252
+ end
253
+ end
254
+
255
+ private
256
+
257
+ def is_history_state_id?(id)
258
+ return id.to_s[-2..-1] == "_H"
259
+ end
260
+
261
+ def base_id(history_id)
262
+ return history_id.to_s[0...-2].to_sym
263
+ end
264
+
265
+ def load_history(superstate)
266
+ 100.times do
267
+ history = superstate.history_id ? get_state(superstate.history_id) : nil
268
+ raise StatemachineException.new("#{superstate} doesn't have any history yet.") if not history
269
+ if history.concrete?
270
+ return history
271
+ else
272
+ superstate = history
273
+ end
274
+ end
275
+ raise StatemachineException.new("No history found within 100 levels of nested superstates.")
276
+ end
277
+
278
+ end
279
+ end
@@ -0,0 +1,26 @@
1
+ module Statemachine
2
+
3
+ class StubContext
4
+
5
+ def initialize(options = {})
6
+ @verbose = options[:verbose]
7
+ end
8
+
9
+ attr_accessor :statemachine
10
+
11
+ def method(name)
12
+ super(name)
13
+ rescue
14
+ self.class.class_eval "def #{name}(*args, &block); __generic_method(:#{name}, *args, &block); end"
15
+ return super(name)
16
+ end
17
+
18
+ def __generic_method(name, *args)
19
+ if !defined?($IS_TEST)
20
+ puts "action invoked: #{name}(#{args.join(", ")}) #{block_given? ? "with block" : ""}" if @verbose
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,53 @@
1
+ module Statemachine
2
+
3
+ class Superstate < State #:nodoc:
4
+
5
+ attr_accessor :startstate_id
6
+ attr_reader :history_id
7
+
8
+ def initialize(id, superstate, statemachine)
9
+ super(id, superstate, statemachine)
10
+ @startstate = nil
11
+ @history_id = nil
12
+ end
13
+
14
+ def concrete?
15
+ return false
16
+ end
17
+
18
+ def startstate
19
+ return @statemachine.get_state(@startstate_id)
20
+ end
21
+
22
+ def resolve_startstate
23
+ return startstate.resolve_startstate
24
+ end
25
+
26
+ def substate_exiting(substate)
27
+ @history_id = substate.id
28
+ end
29
+
30
+ def add_substates(*substate_ids)
31
+ do_substate_adding(substate_ids)
32
+ end
33
+
34
+ def default_history=(state_id)
35
+ @history_id = @default_history_id = state_id
36
+ end
37
+
38
+ def reset
39
+ @history_id = @default_history_id
40
+ end
41
+
42
+ def to_s
43
+ return "'#{id}' superstate"
44
+ end
45
+
46
+ def abstract_states
47
+ return [@id] if not @superstate or @superstate.is_parallel
48
+ ([@id] + @superstate.abstract_states).uniq
49
+ end
50
+
51
+ end
52
+
53
+ end