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 @@
1
+ require 'statemachine/generate/java/java_statemachine'
@@ -0,0 +1,265 @@
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_java(options = {})
10
+ generator = Generate::Java::JavaStatemachine.new(self, options)
11
+ generator.generate!
12
+ end
13
+
14
+ end
15
+
16
+ module Generate
17
+ module Java
18
+ class JavaStatemachine
19
+
20
+ include Generate::Util
21
+
22
+ HEADER1 = "// This file was generated by the Ruby Statemachine Library (http://slagyr.github.com/statemachine)."
23
+ HEADER2 = "// Generated at "
24
+
25
+ def initialize(sm, options)
26
+ @sm = sm
27
+ @output_dir = options[:output]
28
+ @classname = options[:name]
29
+ @context_classname = "#{@classname}Context"
30
+ @package = options[:package]
31
+ raise "Please specify an output directory. (:output => 'where/you/want/your/code')" if @output_dir.nil?
32
+ raise "Output dir '#{@output_dir}' doesn't exist." if !File.exist?(@output_dir)
33
+ raise "Please specify a name for the statemachine. (:name => 'SomeName')" if @classname.nil?
34
+ end
35
+
36
+ def generate!
37
+ explore_sm
38
+ create_file(src_file(@classname), build_statemachine_src)
39
+ create_file(src_file(@context_classname), build_context_src)
40
+ say "Statemachine generated."
41
+ end
42
+
43
+ private ###########################################
44
+
45
+ def explore_sm
46
+ events = []
47
+ actions = []
48
+ @sm.states.values.each do |state|
49
+ state.transitions.values.each do |transition|
50
+ events << transition.event
51
+ add_action(actions, transition.action)
52
+ end
53
+ end
54
+ @event_names = events.uniq.map {|e| e.to_s.camalized(:lower)}.sort
55
+
56
+ @sm.states.values.each do |state|
57
+ add_action(actions, state.entry_action)
58
+ add_action(actions, state.exit_action)
59
+ end
60
+ @action_names = actions.uniq.map {|e| e.to_s.camalized(:lower)}.sort
61
+
62
+ @startstate = @sm.get_state(@sm.startstate).resolve_startstate
63
+ end
64
+
65
+ def add_action(actions, action)
66
+ return if action.nil?
67
+ raise "Actions must be symbols in order to generation Java code. (#{action})" unless action.is_a?(Symbol)
68
+ actions << action
69
+ end
70
+
71
+ def build_statemachine_src
72
+ src = begin_src
73
+ src << "public class #{@classname}" << endl
74
+ begin_scope(src)
75
+
76
+ add_instance_variables(src)
77
+ add_constructor(src)
78
+ add_statemachine_boilerplate_code(src)
79
+ add_event_delegation(src)
80
+ add_statemachine_exception(src)
81
+ add_base_state(src)
82
+ add_state_implementations(src)
83
+
84
+ end_scope(src)
85
+ return src.to_s
86
+ end
87
+
88
+ def add_instance_variables (src)
89
+ src << "// Instance variables" << endl
90
+ concrete_states = @sm.states.values.reject { |state| state.id.nil? || !state.concrete? }.sort { |a, b| a.id <=> b.id }
91
+ concrete_states.each do |state|
92
+ name = state.id.to_s
93
+ src << "public final State #{name.upcase} = new #{name.camalized}State(this);" << endl
94
+ end
95
+ superstates = @sm.states.values.reject { |state| state.concrete? }.sort { |a, b| a.id <=> b.id }
96
+ superstates.each do |superstate|
97
+ startstate = superstate.resolve_startstate
98
+ src << "public final State #{superstate.id.to_s.upcase} = #{startstate.id.to_s.upcase};" << endl
99
+ end
100
+ src << "private State state = #{@startstate.id.to_s.upcase};" << endl
101
+ src << endl
102
+ src << "private #{@context_classname} context;" << endl
103
+ src << endl
104
+ end
105
+
106
+ def add_constructor(src)
107
+ src << "// Statemachine constructor" << endl
108
+ add_method(src, nil, @classname, "#{@context_classname} context") do
109
+ src << "this.context = context;" << endl
110
+ entered_states = []
111
+ entry_state = @startstate
112
+ while entry_state != @sm.root
113
+ entered_states << entry_state
114
+ entry_state = entry_state.superstate
115
+ end
116
+ entered_states.reverse.each do |state|
117
+ src << "context.#{state.entry_action.to_s.camalized(:lower)}();" << endl if state.entry_action
118
+ end
119
+ end
120
+ end
121
+
122
+ def add_statemachine_boilerplate_code(src)
123
+ src << "// The following is boiler plate code standard to all statemachines" << endl
124
+ add_one_liner(src, @context_classname, "getContext", nil, "return context")
125
+ add_one_liner(src, "State", "getState", nil, "return state")
126
+ add_one_liner(src, "void", "setState", "State newState", "state = newState")
127
+ end
128
+
129
+ def add_event_delegation(src)
130
+ src << "// Event delegation" << endl
131
+ @event_names.each do |event|
132
+ add_one_liner(src, "void", event, nil, "state.#{event}()")
133
+ end
134
+ end
135
+
136
+ def add_statemachine_exception(src)
137
+ src << "// Standard exception class added to all statemachines." << endl
138
+ src << "public static class StatemachineException extends RuntimeException" << endl
139
+ begin_scope(src)
140
+ src << "public StatemachineException(State state, String event)" << endl
141
+ begin_scope(src)
142
+ src << "super(\"Missing transition from '\" + state.getClass().getSimpleName() + \"' with the '\" + event + \"' event.\");" << endl
143
+ end_scope(src)
144
+ end_scope(src)
145
+ src << endl
146
+ end
147
+
148
+ def add_base_state(src)
149
+ src << "// The base state" << endl
150
+ src << "public static abstract class State" << endl
151
+ begin_scope(src)
152
+ src << "protected #{@classname} statemachine;" << endl
153
+ src << endl
154
+ add_one_liner(src, nil, "State", "#{@classname} statemachine", "this.statemachine = statemachine")
155
+ @event_names.each do |event|
156
+ add_one_liner(src, "void", event, nil, "throw new StatemachineException(this, \"#{event}\")")
157
+ end
158
+ end_scope(src)
159
+ src << endl
160
+ end
161
+
162
+ def add_state_implementations(src)
163
+ src << "// State implementations" << endl
164
+ @sm.states.keys.reject{|k| k.nil? }.sort.each do |state_id|
165
+ state = @sm.states[state_id]
166
+ state_name = state.id.to_s.camalized
167
+ base_class = state.superstate == @sm.root ? "State" : state.superstate.id.to_s.camalized
168
+
169
+ add_concrete_state_class(src, state, state_name, base_class) if state_id
170
+ end
171
+ end
172
+
173
+ def add_concrete_state_class(src, state, state_name, base_class)
174
+ src << "public static class #{state_name}State extends State" << endl
175
+ src << "{" << endl
176
+ src.indent!
177
+ add_one_liner(src, nil, "#{state_name}State", "#{@classname} statemachine", "super(statemachine)")
178
+ state.transitions.keys.sort.each do |event_id|
179
+ transition = state.transitions[event_id]
180
+ add_state_event_handler(transition, src)
181
+ end
182
+ src.undent!
183
+ src << "}" << endl
184
+ src << endl
185
+ end
186
+
187
+ def add_state_event_handler(transition, src)
188
+ event_name = transition.event.to_s.camalized(:lower)
189
+ exits, entries = transition.exits_and_entries(@sm.get_state(transition.origin_id), @sm.get_state(transition.destination_id))
190
+ add_method(src, "void", event_name, nil) do
191
+ exits.each do |exit|
192
+ src << "statemachine.getContext().#{exit.exit_action.to_s.camalized(:lower)}();" << endl if exit.exit_action
193
+ end
194
+ src << "statemachine.getContext().#{transition.action.to_s.camalized(:lower)}();" << endl if transition.action
195
+ src << "statemachine.setState(statemachine.#{transition.destination_id.to_s.upcase});" << endl
196
+ entries.each do |entry|
197
+ src << "statemachine.getContext().#{entry.entry_action.to_s.camalized(:lower)}();" << endl if entry.entry_action
198
+ end
199
+ end
200
+ end
201
+
202
+ def add_one_liner(src, return_type, name, params, body)
203
+ add_method(src, return_type, name, params) do
204
+ src << "#{body};" << endl
205
+ end
206
+ end
207
+
208
+ def add_method(src, return_type, name, params)
209
+ src << "public #{return_type} #{name}(#{params})".sub(' ' * 2, ' ') << endl
210
+ begin_scope(src)
211
+ yield
212
+ end_scope(src)
213
+ src << endl
214
+ end
215
+
216
+ def begin_scope(src)
217
+ src << "{" << endl
218
+ src.indent!
219
+ end
220
+
221
+ def end_scope(src)
222
+ src.undent! << "}" << endl
223
+ end
224
+
225
+ def build_context_src
226
+ src = begin_src
227
+ src << "public interface #{@context_classname}" << endl
228
+ begin_scope(src)
229
+ src << "// Actions" << endl
230
+ @action_names.each do |event|
231
+ src << "void #{event}();" << endl
232
+ end
233
+ end_scope(src)
234
+ return src.to_s
235
+ end
236
+
237
+ def begin_src
238
+ src = SrcBuilder.new
239
+ src << HEADER1 << endl
240
+ src << HEADER2 << timestamp << endl
241
+ src << "package #{@package};" << endl
242
+ src << endl
243
+ return src
244
+ end
245
+
246
+ def create_file(filename, content)
247
+ establish_directory(File.dirname(filename))
248
+ say "Writing to file: #{filename}"
249
+ File.open(filename, 'w') do |file|
250
+ file.write(content)
251
+ end
252
+ end
253
+
254
+ def src_file(name)
255
+ path = @output_dir
256
+ if @package
257
+ @package.split(".").each { |segment| path = File.join(path, segment) }
258
+ end
259
+ return File.join(path, "#{name}.java")
260
+ end
261
+
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,48 @@
1
+ module Statemachine
2
+ module Generate
3
+ class SrcBuilder
4
+
5
+ def initialize
6
+ @src = ""
7
+ @is_newline = true
8
+ @indents = 0
9
+ @indent_size = 2
10
+ end
11
+
12
+ def <<(content)
13
+ if content == :endl
14
+ newline!
15
+ else
16
+ add_indents if @is_newline
17
+ @src += content.to_s
18
+ end
19
+ return self
20
+ end
21
+
22
+ def newline!
23
+ @src += "\n"
24
+ @is_newline = true
25
+ end
26
+
27
+ def to_s
28
+ return @src
29
+ end
30
+
31
+ def indent!
32
+ @indents += 1
33
+ return self
34
+ end
35
+
36
+ def undent!
37
+ @indents -= 1
38
+ return self
39
+ end
40
+
41
+ def add_indents
42
+ @src += (" " * (@indent_size * @indents))
43
+ @is_newline = false
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,50 @@
1
+ require 'date'
2
+
3
+ module Statemachine
4
+ module Generate
5
+ module Util
6
+
7
+ def create_file(filename, content)
8
+ establish_directory(File.dirname(filename))
9
+ File.open(filename, 'w') do |file|
10
+ file.write(content)
11
+ end
12
+ end
13
+
14
+ def establish_directory(path)
15
+ return if File.exist?(path)
16
+ establish_directory(File.dirname(path))
17
+ Dir.mkdir(path)
18
+ end
19
+
20
+ def timestamp
21
+ return DateTime.now.strftime("%H:%M:%S %B %d, %Y")
22
+ end
23
+
24
+ def endl
25
+ return :endl
26
+ end
27
+
28
+ def say(message)
29
+ if !defined?($IS_TEST)
30
+ puts message
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+
38
+ class String
39
+ def camalized(starting_case = :upper)
40
+ value = self.downcase.gsub(/[_| |\-][a-z]/) { |match| match[-1..-1].upcase }
41
+ value = value[0..0].upcase + value[1..-1] if starting_case == :upper
42
+ return value
43
+ end
44
+ end
45
+
46
+ class Symbol
47
+ def <=>(other)
48
+ return to_s <=> other.to_s
49
+ end
50
+ end
@@ -0,0 +1,196 @@
1
+ module Statemachine
2
+
3
+ class Parallelstate< Superstate
4
+
5
+ attr_accessor :parallel_statemachines, :id
6
+ attr_reader :startstate_ids
7
+
8
+ def initialize(id, superstate, statemachine)
9
+ super(id, superstate, statemachine)
10
+ @parallel_statemachines=[]
11
+ @startstate_ids=[]
12
+ end
13
+
14
+ # def startstate_id= id
15
+ # if @parallel_statemachines.size>0
16
+ # @startstate_ids[@parallel_statemachines.size-1]= id
17
+ # end
18
+ # end
19
+ #
20
+ # def startstate_id
21
+ # if (@parallel_statemachines.size>0 and @parallel_statemachines.size==@startstate_ids.size)
22
+ # return true
23
+ # end
24
+ # nil
25
+ # end
26
+
27
+ def context= c
28
+ @parallel_statemachines.each do |s|
29
+ s.context=c
30
+ end
31
+ end
32
+
33
+ def activate(terminal_state = nil)
34
+ @statemachine.state = self
35
+
36
+ @parallel_statemachines.each_with_index do |s,i|
37
+ s.activation = @statemachine.activation
38
+ s.reset(@startstate_ids[i])
39
+ end
40
+ @parallel_statemachines.each do |s|
41
+ next if terminal_state and s.has_state(terminal_state)
42
+ @statemachine.activation.call(s.state,self.abstract_states,self.states) if @statemachine.activation
43
+ end
44
+
45
+ end
46
+
47
+ def add_statemachine(statemachine)
48
+ statemachine.is_parallel=self
49
+ @parallel_statemachines.push(statemachine)
50
+ statemachine.context = @statemachine.context
51
+ @startstate_ids << @startstate_id
52
+ @startstate_id = nil
53
+ end
54
+
55
+ def get_statemachine_with(id)
56
+ @parallel_statemachines.each do |s|
57
+ return s if s.has_state(id)
58
+ end
59
+ end
60
+
61
+ def non_default_transition_for(event)
62
+ p "check parallel for #{event}"
63
+ transition = @transitions[event]
64
+ return transition if transition
65
+
66
+ transition = transition_for(event)
67
+
68
+ transition = @superstate.non_default_transition_for(event) if @superstate and not transition
69
+ return transition
70
+ end
71
+
72
+ def In(id)
73
+ @parallel_statemachines.each do |s|
74
+ return true if s.In(id.to_sym)
75
+ end
76
+ return false
77
+ end
78
+
79
+ def state= id
80
+ @parallel_statemachines.each do |s|
81
+ if s.has_state(id)
82
+ s.state=id
83
+ return true
84
+ end
85
+ end
86
+ return false
87
+ end
88
+
89
+ def has_state(id)
90
+ @parallel_statemachines.each do |s|
91
+ if s.has_state(id)
92
+ return true
93
+ end
94
+ end
95
+ return false
96
+ end
97
+
98
+ def get_state(id)
99
+ # if state = @statemachine.get_state(id)
100
+ # return state
101
+ # end
102
+ @parallel_statemachines.each do |s|
103
+ if state = s.get_state(id)
104
+ return state
105
+ end
106
+ end
107
+ return nil
108
+ end
109
+
110
+ def process_event(event, *args)
111
+ exceptions = []
112
+ result = false
113
+ # TODO fix needed: respond_to checks superstates lying out of the parallel state as well, in case an event is
114
+ # defined outside the parallel statemachine it gets processed twice!
115
+
116
+ @parallel_statemachines.each_with_index do |s,i|
117
+ if s.respond_to? event
118
+ s.process_event(event,*args)
119
+ result = true
120
+ end
121
+ end
122
+ result
123
+ end
124
+
125
+ # Resets all of the statemachines back to theirs starting state.
126
+ def reset
127
+ @parallel_statemachines.each_with_index do |s,i|
128
+ s.reset(@startstate_ids[i])
129
+ end
130
+ end
131
+
132
+ def concrete?
133
+ return true
134
+ end
135
+
136
+ def startstate
137
+ return @statemachine.get_state(@startstate_id)
138
+ end
139
+
140
+ def resolve_startstate
141
+ return self
142
+ end
143
+
144
+ def substate_exiting(substate)
145
+ @history_id = substate.id
146
+ end
147
+
148
+ def add_substates(*substate_ids)
149
+ do_substate_adding(substate_ids)
150
+ end
151
+
152
+ def default_history=(state_id)
153
+ @history_id = @default_history_id = state_id
154
+ end
155
+
156
+ def states
157
+ return @parallel_statemachines.map &:state
158
+ end
159
+
160
+ def transition_for(event)
161
+ @parallel_statemachines.each do |s|
162
+ # puts "checke parallel #{s.id} for #{event}"
163
+ transition = s.get_state(s.state).non_default_transition_for(event)
164
+ transition = s.get_state(s.state).default_transition if not transition
165
+ return transition if transition
166
+ end
167
+ return @superstate.default_transition if @superstate
168
+
169
+ # super.transition_for(event)
170
+ end
171
+
172
+ def enter(args=[])
173
+ # reset
174
+ super(args)
175
+ end
176
+ def to_s
177
+ return "'#{id}' parallel"
178
+ end
179
+
180
+ def abstract_states
181
+ abstract_states=[]
182
+
183
+ if (@superstate)
184
+ abstract_states=@superstate.abstract_states
185
+ end
186
+ @parallel_statemachines.each do |s|
187
+ abstract_states += s.abstract_states
188
+ end
189
+ abstract_states.uniq
190
+ end
191
+ def is_parallel
192
+ true
193
+ end
194
+ end
195
+
196
+ end