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,383 @@
1
+ module Statemachine
2
+
3
+ # The starting point for building instances of Statemachine.
4
+ # The block passed in should contain all the declarations for all
5
+ # states, events, and actions with in the statemachine.
6
+ #
7
+ # Sample: Turnstyle
8
+ #
9
+ # sm = Statemachine.build do
10
+ # trans :locked, :coin, :unlocked, :unlock
11
+ # trans :unlocked, :pass, :locked, :lock
12
+ # end
13
+ #
14
+ # An optional statemachine paramter may be passed in to modify
15
+ # an existing statemachine instance.
16
+ #
17
+ # Actions:
18
+ # Where ever an action paramter is used, it may take on one of three forms:
19
+ # 1. Symbols: will execute a method by the same name on the _context_
20
+ # 2. String: Ruby code that will be executed within the binding of the _context_
21
+ # 3. Proc: Will be executed within the binding of the _context_
22
+ #
23
+ # See Statemachine::SuperstateBuilding
24
+ # See Statemachine::StateBuilding
25
+ #
26
+ def self.build(statemachine = nil, &block)
27
+ builder = statemachine ? StatemachineBuilder.new(statemachine) : StatemachineBuilder.new
28
+ builder.instance_eval(&block)
29
+ builder.statemachine.reset
30
+ return builder.statemachine
31
+ end
32
+
33
+ class Builder #:nodoc:
34
+ attr_reader :statemachine
35
+
36
+ def initialize(statemachine)
37
+ @statemachine = statemachine
38
+ end
39
+
40
+ protected
41
+ def acquire_state_in(state_id, context)
42
+ return nil if state_id == nil
43
+ return state_id if state_id.is_a? State
44
+ state = nil
45
+ if @statemachine.has_state(state_id)
46
+ state = @statemachine.get_state(state_id)
47
+ else
48
+ state = State.new(state_id, context, @statemachine)
49
+ @statemachine.add_state(state)
50
+ end
51
+ context.startstate_id = state_id if context.startstate_id == nil
52
+ return state
53
+ end
54
+ end
55
+
56
+ class ParallelBuilder
57
+ attr_reader :parallel_statemachine
58
+
59
+ def initialize(statemachines)
60
+ @parallel_statemachine = ParallelStatemachine.new statemachines
61
+ end
62
+
63
+ end
64
+
65
+
66
+ # The builder module used to declare states.
67
+ module StateBuilding
68
+ attr_reader :subject
69
+
70
+ # Declares that the state responds to the spcified event.
71
+ # The +event+ paramter should be a Symbol.
72
+ # The +destination_id+, which should also be a Symbol, is the id of the state
73
+ # that will event will transition into.
74
+ #
75
+ # The 3rd +action+ paramter is optional
76
+ #
77
+ # sm = Statemachine.build do
78
+ # state :locked do
79
+ # event :coin, :unlocked, :unlock
80
+ # end
81
+ # end
82
+ #
83
+ def event(event, destination_id, action = nil, cond = true)
84
+ @subject.add(Transition.new(@subject.id, destination_id, event, action, cond))
85
+ end
86
+
87
+ def on_event(event, options)
88
+ self.event(event, options[:transition_to], options[:and_perform])
89
+ end
90
+
91
+ # Declare the entry action for the state.
92
+ #
93
+ # sm = Statemachine.build do
94
+ # state :locked do
95
+ # on_entry :lock
96
+ # end
97
+ # end
98
+ #
99
+ def on_entry(entry_action)
100
+ @subject.entry_action = entry_action
101
+ end
102
+
103
+ # Declare the exit action for the state.
104
+ #
105
+ # sm = Statemachine.build do
106
+ # state :locked do
107
+ # on_exit :unlock
108
+ # end
109
+ # end
110
+ #
111
+ def on_exit(exit_action)
112
+ @subject.exit_action = exit_action
113
+ end
114
+
115
+ # Declare a default transition for the state. Any event that is not already handled
116
+ # by the state will be handled by this transition.
117
+ #
118
+ # sm = Statemachine.build do
119
+ # state :locked do
120
+ # default :unlock, :action
121
+ # end
122
+ # end
123
+ #
124
+ def default(destination_id, action = nil, cond = true)
125
+ @subject.default_transition = Transition.new(@subject.id, destination_id, nil, action, cond)
126
+ end
127
+ end
128
+
129
+ # The builder module used to declare superstates.
130
+ module SuperstateBuilding
131
+ attr_reader :subject
132
+
133
+ # Define a state within the statemachine or superstate.
134
+ #
135
+ # sm = Statemachine.build do
136
+ # state :locked do
137
+ # #define the state
138
+ # end
139
+ # end
140
+ #
141
+ def state(id, &block)
142
+ builder = StateBuilder.new(id, @subject, @statemachine)
143
+ builder.instance_eval(&block) if block
144
+ end
145
+
146
+ # Define a superstate within the statemachine or superstate.
147
+ #
148
+ # sm = Statemachine.build do
149
+ # superstate :operational do
150
+ # #define superstate
151
+ # end
152
+ # end
153
+ #
154
+ def superstate(id, &block)
155
+ builder = SuperstateBuilder.new(id, @subject, @statemachine)
156
+ builder.instance_eval(&block)
157
+ end
158
+
159
+ # Declares a transition within the superstate or statemachine.
160
+ # The +origin_id+, a Symbol, identifies the starting state for this transition. The state
161
+ # identified by +origin_id+ will be created within the statemachine or superstate which this
162
+ # transition is declared.
163
+ # The +event+ paramter should be a Symbol.
164
+ # The +destination_id+, which should also be a Symbol, is the id of the state that will
165
+ # event will transition into. This method will not create destination states within the
166
+ # current statemachine of superstate. If the state destination state should exist here,
167
+ # that declare with with the +state+ method or declare a transition starting at the state.
168
+ #
169
+ # sm = Statemachine.build do
170
+ # trans :locked, :coin, :unlocked, :unlock
171
+ # end
172
+ #
173
+ def trans(origin_id, event, destination_id, action = nil, cond = true)
174
+ origin = acquire_state_in(origin_id, @subject)
175
+ origin.add(Transition.new(origin_id, destination_id, event, action, cond))
176
+ end
177
+
178
+ def transition_from(origin_id, options)
179
+ trans(origin_id, options[:on_event], options[:transition_to], options[:and_perform])
180
+ end
181
+
182
+ # Specifies the startstate for the statemachine or superstate. The state must
183
+ # exist within the scope.
184
+ #
185
+ # sm = Statemachine.build do
186
+ # startstate :locked
187
+ # end
188
+ #
189
+ def startstate(startstate_id)
190
+ @subject.startstate_id = startstate_id
191
+ end
192
+
193
+ # Allows the declaration of entry actions without using the +state+ method. +id+ is identifies
194
+ # the state to which the entry action will be added.
195
+ #
196
+ # sm = Statemachine.build do
197
+ # trans :locked, :coin, :unlocked
198
+ # on_entry_of :unlocked, :unlock
199
+ # end
200
+ #
201
+ def on_entry_of(id, action)
202
+ @statemachine.get_state(id).entry_action = action
203
+ end
204
+
205
+ # Allows the declaration of exit actions without using the +state+ method. +id+ is identifies
206
+ # the state to which the exit action will be added.
207
+ #
208
+ # sm = Statemachine.build do
209
+ # trans :locked, :coin, :unlocked
210
+ # on_exit_of :locked, :unlock
211
+ # end
212
+ #
213
+ def on_exit_of(id, action)
214
+ @statemachine.get_state(id).exit_action = action
215
+ end
216
+
217
+ # Used to specify the default state held by the history pseudo state of the superstate.
218
+ #
219
+ # sm = Statemachine.build do
220
+ # superstate :operational do
221
+ # default_history :state_id
222
+ # end
223
+ # end
224
+ #
225
+ def default_history(id)
226
+ @subject.default_history = id
227
+ end
228
+ end
229
+
230
+ # Builder class used to define states. Creates by SuperstateBuilding#state
231
+ class StateBuilder < Builder
232
+ include StateBuilding
233
+
234
+ def initialize(id, superstate, statemachine)
235
+ super statemachine
236
+ @subject = acquire_state_in(id, superstate)
237
+ end
238
+ end
239
+
240
+ module ParallelstateBuilding
241
+ attr_reader :subject
242
+
243
+ def parallel (id, &block)
244
+ builder = ParallelStateBuilder.new(id, @subject, @statemachine)
245
+ builder.instance_eval(&block)
246
+ end
247
+ end
248
+
249
+ # Builder class used to define superstates. Creates by SuperstateBuilding#superstate
250
+ class SuperstateBuilder < Builder
251
+ include StateBuilding
252
+ include SuperstateBuilding
253
+ include ParallelstateBuilding
254
+
255
+ def initialize(id, superstate, statemachine)
256
+ super statemachine
257
+ @subject = Superstate.new(id, superstate, statemachine)
258
+ superstate.startstate_id = id if superstate.startstate_id == nil
259
+
260
+ # small patch to support redefinition of already existing states without
261
+ # loosing the already existing transformations. Used to overwrite states
262
+ # with superstates.
263
+
264
+ s = statemachine.get_state(id)
265
+ if (s)
266
+ s.transitions.each {|k,v|
267
+ @subject.add(v)
268
+ }
269
+ end
270
+ statemachine.add_state(@subject)
271
+ end
272
+ end
273
+
274
+
275
+
276
+
277
+ # Created by Statemachine.build as the root context for building the statemachine.
278
+ class StatemachineBuilder < Builder
279
+ include SuperstateBuilding
280
+ include ParallelstateBuilding
281
+
282
+ def initialize(statemachine = Statemachine.new)
283
+ super statemachine
284
+ @subject = @statemachine.root
285
+ end
286
+
287
+ # Used the set the context of the statemahine within the builder.
288
+ #
289
+ # sm = Statemachine.build do
290
+ # ...
291
+ # context MyContext.new
292
+ # end
293
+ #
294
+ # Statemachine.context may also be used.
295
+ def context(a_context)
296
+ @statemachine.context = a_context
297
+ a_context.statemachine = @statemachine if a_context.respond_to?(:statemachine=)
298
+ end
299
+
300
+ # Stubs the context. This makes statemachine immediately useable, even if functionless.
301
+ # The stub will print all the actions called so it's nice for trial runs.
302
+ #
303
+ # sm = Statemachine.build do
304
+ # ...
305
+ # stub_context :verbose => true
306
+ # end
307
+ #
308
+ # Statemachine.context may also be used.
309
+ def stub_context(options={})
310
+ require 'statemachine/stub_context'
311
+ context StubContext.new(options)
312
+ end
313
+ end
314
+
315
+
316
+ # The builder module used to declare statemachines.
317
+ module StatemachineBuilding
318
+ attr_reader :subject
319
+
320
+ def statemachine (id, &block)
321
+ builder = StatemachineBuilder.new(Statemachine.new(@subject))
322
+ #builder = StatemachineBuilder.new
323
+ builder.instance_eval(&block) if block
324
+ if not @subject.is_a? Parallelstate
325
+ # Only reset statemachine if it's the root one. Otherwise
326
+ # the inital states on_entry function would be called!
327
+ builder.statemachine.reset
328
+ end
329
+ # puts "build statemachine #{builder.statemachine.inspect}"
330
+
331
+ @subject.add_statemachine builder.statemachine
332
+ end
333
+ end
334
+
335
+ class ParallelStateBuilder < Builder
336
+ include StatemachineBuilding
337
+ def initialize(id, superstate, statemachine)
338
+ super statemachine
339
+ @subject = Parallelstate.new(id, superstate, statemachine)
340
+ superstate.startstate_id = id if superstate.startstate_id == nil
341
+ statemachine.add_state(@subject)
342
+ #puts "added #{@subject.inspect}"
343
+ end
344
+ end
345
+
346
+ # Created by Statemachine.build as the root context for building the statemachine.
347
+ class ParallelStatemachineBuilder < ParallelBuilder
348
+ include StatemachineBuilding
349
+
350
+ def initialize
351
+ super []
352
+ #@subject = @statemachine
353
+ end
354
+
355
+ # used the set the context of the statemahine within the builder.
356
+ #
357
+ # sm = Statemachine.build do
358
+ # ...
359
+ # context MyContext.new
360
+ # end
361
+ #
362
+ # Statemachine.context may also be used.
363
+ def context(a_context)
364
+ @statemachine.context = a_context
365
+ a_context.statemachine = @statemachine if a_context.respond_to?(:statemachine=)
366
+ end
367
+
368
+ # Stubs the context. This makes statemachine immediately useable, even if functionless.
369
+ # The stub will print all the actions called so it's nice for trial runs.
370
+ #
371
+ # sm = Statemachine.build do
372
+ # ...
373
+ # stub_context :verbose => true
374
+ # end
375
+ #
376
+ # Statemachine.context may also be used.
377
+ def stub_context(options={})
378
+ require 'statemachine/stub_context'
379
+ context StubContext.new(options)
380
+ end
381
+ end
382
+
383
+ end
@@ -0,0 +1 @@
1
+ require 'statemachine/generate/dot_graph/dot_graph_statemachine'
@@ -0,0 +1,127 @@
1
+ require 'statemachine/generate/util'
2
+ require 'statemachine/generate/src_builder'
3
+
4
+ module Statemachine
5
+ class Statemachine
6
+
7
+ attr_reader :states
8
+
9
+ def to_dot(options = {})
10
+ generator = Generate::DotGraph::DotGraphStatemachine.new(self, options)
11
+ generator.generate!
12
+ end
13
+
14
+ end
15
+
16
+ module Generate
17
+ module DotGraph
18
+
19
+ class DotGraphStatemachine
20
+
21
+ include Generate::Util
22
+
23
+ def initialize(sm, options)
24
+ @sm = sm
25
+ @output_dir = options[:output]
26
+ raise "Please specify an output directory. (:output => 'where/you/want/your/code')" if @output_dir.nil?
27
+ raise "Output dir '#{@output_dir}' doesn't exist." if !File.exist?(@output_dir)
28
+ end
29
+
30
+ def generate!
31
+ explore_sm
32
+ save_output(src_file("main"), build_full_graph)
33
+ @sm.states.values.each do |state|
34
+ save_output(src_file("#{state.id}"), build_state_graph(state))
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def explore_sm
41
+ @nodes = []
42
+ @transitions = []
43
+ @sm.states.values.each { |state|
44
+ state.transitions.values.each { |transition|
45
+ @nodes << transition.origin_id
46
+ @nodes << transition.destination_id
47
+ @transitions << transition
48
+ }
49
+ }
50
+ @nodes = @nodes.uniq
51
+ end
52
+
53
+ def build_full_graph
54
+ builder = Generate::SrcBuilder.new
55
+
56
+ add_graph_header(builder, "main")
57
+
58
+ @nodes.each { |node| add_node(builder, node) }
59
+ builder << endl
60
+
61
+ @transitions.each do |transition|
62
+ add_transition(builder, transition)
63
+ end
64
+
65
+ add_graph_footer(builder)
66
+
67
+ return builder.to_s
68
+ end
69
+
70
+ def build_state_graph(state)
71
+ builder = Generate::SrcBuilder.new
72
+
73
+ add_graph_header(builder, state.id)
74
+
75
+ state.transitions.values.each do |transition|
76
+ add_transition(builder, transition)
77
+ end
78
+
79
+ add_graph_footer(builder)
80
+
81
+ return builder.to_s
82
+ end
83
+
84
+ def add_graph_header(builder, graph_name)
85
+ builder << "digraph #{graph_name} {" << endl
86
+ builder.indent!
87
+ end
88
+
89
+ def add_graph_footer(builder)
90
+ builder.undent!
91
+ builder << "}" << endl
92
+ end
93
+
94
+ def add_node(builder, node)
95
+ builder << node
96
+ builder << " [ href = \"#{node}.svg\"]"
97
+ builder << endl
98
+ end
99
+
100
+ def add_transition(builder, transition)
101
+ builder << transition.origin_id
102
+ builder << " -> "
103
+ builder << transition.destination_id
104
+ builder << " [ "
105
+ builder << "label = #{transition.event} "
106
+ builder << "]"
107
+ builder << endl
108
+ end
109
+
110
+ def src_file(name)
111
+ return name if @output_dir.nil?
112
+ path = @output_dir
113
+ answer = File.join(path, "#{name}.dot")
114
+ return answer
115
+ end
116
+
117
+ def save_output(filename, content)
118
+ if @output_dir.nil?
119
+ say "Writing to file: #{filename}"
120
+ else
121
+ create_file(filename, content)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end