MINT-statemachine 1.2.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 (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