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,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