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