golem_statemachine 0.9.5 → 1.1.0.pre2
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.
- data/README.rdoc +2 -3
- data/golem_statemachine.gemspec +1 -1
- data/lib/golem.rb +2 -3
- data/lib/golem/dsl/state_def.rb +12 -0
- data/lib/golem/dsl/transition_def.rb +13 -1
- data/lib/golem/model/state.rb +2 -0
- data/lib/golem/model/transition.rb +2 -0
- data/lib/golem/util/element_collection.rb +4 -0
- data/lib/golem/visualizer.rb +190 -0
- data/test/problematic_test.rb +53 -2
- metadata +12 -8
data/README.rdoc
CHANGED
@@ -7,9 +7,8 @@ any Ruby object.
|
|
7
7
|
|
8
8
|
The Finite State Machine pattern has many potential uses, but in practice you'll probably find it most useful in
|
9
9
|
implementing complex business logic -- the kind that requires multi-page UML diagrams describing an entity's behavior
|
10
|
-
over a series of events. Golem
|
11
|
-
|
12
|
-
easy.
|
10
|
+
over a series of events. Golem's DSL is specifically designed to have close correspondence with UML diagrams. Golem
|
11
|
+
also includes the ability to automatically generate UML sequence diagrams from statemachines using GraphViz.
|
13
12
|
|
14
13
|
|
15
14
|
==== Contents
|
data/golem_statemachine.gemspec
CHANGED
data/lib/golem.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'active_support/all'
|
2
|
-
|
3
2
|
require 'golem/dsl/state_machine_def'
|
4
|
-
|
3
|
+
|
5
4
|
module Golem
|
6
5
|
|
7
6
|
def self.included(mod)
|
@@ -99,7 +98,7 @@ module Golem
|
|
99
98
|
state ||= statemachine.initial_state
|
100
99
|
state = state.to_sym if state.is_a?(String)
|
101
100
|
|
102
|
-
raise InvalidStateError, "#{
|
101
|
+
raise InvalidStateError, "#{state} is in an unrecognized state (#{state.inspect})" unless statemachine.states[state]
|
103
102
|
|
104
103
|
state = statemachine.states[state].name
|
105
104
|
|
data/lib/golem/dsl/state_def.rb
CHANGED
@@ -22,6 +22,10 @@ module Golem
|
|
22
22
|
@state.callbacks[:on_exit] = Golem::Model::Callback.new(options[:exit])
|
23
23
|
end
|
24
24
|
|
25
|
+
if options[:comment]
|
26
|
+
@state.comment = options[:comment]
|
27
|
+
end
|
28
|
+
|
25
29
|
instance_eval(&block) if block
|
26
30
|
end
|
27
31
|
|
@@ -40,6 +44,14 @@ module Golem
|
|
40
44
|
raise Golem::DefinitionSyntaxError, "Provide either a callback method or a block, not both." if callback && block
|
41
45
|
@state.callbacks[:on_exit] = Golem::Model::Callback.new(block || callback)
|
42
46
|
end
|
47
|
+
|
48
|
+
def comment(comment)
|
49
|
+
if @state.comment
|
50
|
+
@state.comment += "\n#{comment}"
|
51
|
+
else
|
52
|
+
@state.comment = comment
|
53
|
+
end
|
54
|
+
end
|
43
55
|
end
|
44
56
|
end
|
45
57
|
end
|
@@ -14,12 +14,16 @@ module Golem
|
|
14
14
|
else
|
15
15
|
@to = @machine.get_or_define_state(options[:to])
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
callbacks = {}
|
19
19
|
callbacks[:on_transition] = options[:action] if options[:action]
|
20
20
|
|
21
21
|
@transition = Golem::Model::Transition.new(@from, @to || @from, options[:guards], callbacks)
|
22
22
|
|
23
|
+
if options[:comment]
|
24
|
+
@transition.comment = options[:comment]
|
25
|
+
end
|
26
|
+
|
23
27
|
instance_eval(&block) if block
|
24
28
|
|
25
29
|
@from.transitions_on_event[@event] ||= []
|
@@ -46,6 +50,14 @@ module Golem
|
|
46
50
|
|
47
51
|
@transition.callbacks[:on_transition] = Golem::Model::Callback.new(callback)
|
48
52
|
end
|
53
|
+
|
54
|
+
def comment(comment)
|
55
|
+
if @transition.comment
|
56
|
+
@transition.comment += "\n#{comment}"
|
57
|
+
else
|
58
|
+
@transition.comment = comment
|
59
|
+
end
|
60
|
+
end
|
49
61
|
end
|
50
62
|
end
|
51
63
|
end
|
data/lib/golem/model/state.rb
CHANGED
@@ -0,0 +1,190 @@
|
|
1
|
+
begin
|
2
|
+
require 'graphviz'
|
3
|
+
rescue LoadError
|
4
|
+
$stderr.puts "You must install the 'graphviz' gem to use this visualizer."
|
5
|
+
exit
|
6
|
+
end
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'htmlentities'
|
10
|
+
rescue LoadError
|
11
|
+
$stderr.puts "You must install the 'htmlentities' gem to use this visualizer."
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
|
15
|
+
module Golem
|
16
|
+
class Visualizer
|
17
|
+
def initialize(statemachine)
|
18
|
+
@statemachine = statemachine
|
19
|
+
end
|
20
|
+
|
21
|
+
def visualize(format, filename)
|
22
|
+
@state_nodes = {}
|
23
|
+
@current_path = []
|
24
|
+
|
25
|
+
@graph = GraphViz.new(:G,
|
26
|
+
:type => :digraph,
|
27
|
+
:fontname => "Verdana",
|
28
|
+
:concentrate => true)
|
29
|
+
|
30
|
+
state = @statemachine.states[@statemachine.initial_state]
|
31
|
+
visualize_state(state)
|
32
|
+
|
33
|
+
@graph.output(format => filename)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
def visualize_state(state)
|
38
|
+
if @current_path.include?(state.name)
|
39
|
+
return
|
40
|
+
else
|
41
|
+
@current_path << state.name
|
42
|
+
#puts @current_path.join(" / ")
|
43
|
+
end
|
44
|
+
|
45
|
+
@current_path << state.name
|
46
|
+
|
47
|
+
html = HTMLEntities.new
|
48
|
+
|
49
|
+
if @state_nodes[state.name]
|
50
|
+
n = @state_nodes[state.name]
|
51
|
+
else
|
52
|
+
n = @graph.add_nodes(state.name.to_s)
|
53
|
+
|
54
|
+
actions = []
|
55
|
+
if state.callbacks[:on_enter]
|
56
|
+
action_code = format_callback_code(state.callbacks[:on_enter])
|
57
|
+
unless action_code.nil? || action_code.strip.blank?
|
58
|
+
actions << "<br /><font face=''>enter/ </font><font face='Courier' point-size='11'>#{html.encode(action_code).gsub("\n","<br align='left' /> ")}</font>"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
if state.callbacks[:on_exit]
|
62
|
+
action_code = format_callback_code(state.callbacks[:on_exit])
|
63
|
+
unless action_code.nil? || action_code.strip.blank?
|
64
|
+
actions << "<br /><font face=''>exit/ </font><font face='Courier' point-size='11'>#{html.encode(action_code).gsub("\n","<br align='left' /> ")}</font>"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
n[:fontname] = "Verdana"
|
69
|
+
n[:shape] = "box"
|
70
|
+
n[:style] = "rounded,filled"
|
71
|
+
|
72
|
+
if @current_path.first == state.name
|
73
|
+
n[:fillcolor] = "palegreen"
|
74
|
+
elsif state.transitions_on_event.empty?
|
75
|
+
n[:fillcolor] = "red3"
|
76
|
+
else
|
77
|
+
n[:fillcolor] = "lightblue"
|
78
|
+
end
|
79
|
+
|
80
|
+
comment = nil
|
81
|
+
if state.comment
|
82
|
+
comment = "<br /><font color=\"indigo\" font-face=\"Verdana-Italic\" point-size=\"11\">#{html.encode(state.comment).gsub(/\n/,'<br />')}</font>"
|
83
|
+
end
|
84
|
+
|
85
|
+
n[:label] = "<<font>#{state.name}</font>#{comment}#{actions.join("<br align=\"left\" />")}>"
|
86
|
+
@state_nodes[state.name] = n
|
87
|
+
end
|
88
|
+
|
89
|
+
tos = []
|
90
|
+
puts state.name.to_s
|
91
|
+
|
92
|
+
@statemachine.events.each do |ev|
|
93
|
+
transitions = state.transitions_on_event[ev.name] || []
|
94
|
+
|
95
|
+
if transitions.size > 1
|
96
|
+
dn = @graph.add_nodes("#{state.name}_#{ev.name}")
|
97
|
+
dn[:fontname] = "Verdana"
|
98
|
+
dn[:shape] = "diamond"
|
99
|
+
dn[:style] = "filled"
|
100
|
+
dn[:fillcolor] = "khaki1"
|
101
|
+
dn[:label] = ""
|
102
|
+
|
103
|
+
de = @graph.add_edges(n, dn)
|
104
|
+
de[:label] = "<<font face=\"Verdana-Bold\">#{ev.name}</font>>"
|
105
|
+
else
|
106
|
+
dn = false
|
107
|
+
de = false
|
108
|
+
end
|
109
|
+
|
110
|
+
transitions.each do |transition|
|
111
|
+
puts " --[ #{ev.name} ]--> #{transition.to.name}"
|
112
|
+
edge = @graph.add_edges(dn || n, transition.to.name.to_s)
|
113
|
+
|
114
|
+
guard = nil
|
115
|
+
unless transition.guards.empty?
|
116
|
+
guard = transition.guards.collect do |g|
|
117
|
+
format_callback_code(g)
|
118
|
+
end.join(" and ")
|
119
|
+
|
120
|
+
guard = "[#{guard.strip}]\n"
|
121
|
+
end
|
122
|
+
|
123
|
+
action = nil
|
124
|
+
on_transition = transition.callbacks[:on_transition]
|
125
|
+
if on_transition
|
126
|
+
action = format_callback_code(on_transition)
|
127
|
+
end
|
128
|
+
|
129
|
+
if action
|
130
|
+
action = "<br /> / <font face=\"Courier\" point-size=\"11\">#{html.encode(action).gsub(/\n/,'<br align="left" />')}</font> "
|
131
|
+
end
|
132
|
+
|
133
|
+
if guard
|
134
|
+
guard = "<font face=\"Courier\" point-size=\"11\">#{html.encode(guard).gsub(/\n/,'<br />')}</font><br align=\"left\" />"
|
135
|
+
end
|
136
|
+
|
137
|
+
comment = transition.comment
|
138
|
+
if comment
|
139
|
+
comment = "<font color=\"indigo\" font-face=\"Verdana-Italic\" point-size=\"11\">#{html.encode(comment).gsub(/\n/,'<br />')}</font>"
|
140
|
+
end
|
141
|
+
|
142
|
+
if dn
|
143
|
+
edge[:label] = "<<font face=\"Verdana\">#{guard} #{action}#{comment}</font>>"
|
144
|
+
else
|
145
|
+
edge[:label] = "<<font face=\"Verdana\">#{guard}<font face=\"Verdana-Bold\"> #{html.encode(ev.name)} </font> #{action}#{comment}</font>>"
|
146
|
+
end
|
147
|
+
|
148
|
+
tos << transition.to
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
tos.each do |state|
|
153
|
+
visualize_state(state)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def format_callback_code(cb)
|
158
|
+
raise ArgumentError, "Callback must be of type Golem::Model::Callback but is a #{callback.type}" unless
|
159
|
+
cb.kind_of?(Golem::Model::Callback)
|
160
|
+
|
161
|
+
if cb.callback.kind_of?(Symbol)
|
162
|
+
action = cb.callback.to_s
|
163
|
+
else
|
164
|
+
callback_info = cb.to_s.match(/@(.*):([\d]+)/)
|
165
|
+
file = callback_info[1]
|
166
|
+
line = callback_info[2].to_i - 1
|
167
|
+
puts "#{file}:#{line}"
|
168
|
+
code = IO.readlines(file)[line].strip
|
169
|
+
if /\{(?:\|.+\|)?(.*)\}/ =~ code
|
170
|
+
callback_code = $~[1].strip
|
171
|
+
elsif /(do\s+\|.+\|)|(\{\s+\|)/ =~ code
|
172
|
+
matching = $~[0].include?("do") ? /end/ : /^\s*\}/ # FIXME: this is awful
|
173
|
+
line_code = ""
|
174
|
+
callback_code = ""
|
175
|
+
lines = IO.readlines(file)
|
176
|
+
first_indent = lines[line+1].match(/(^\s+)/)[1]
|
177
|
+
while true
|
178
|
+
line += 1
|
179
|
+
line_code = lines[line]
|
180
|
+
break if line_code.match(matching) # FIXME, looking for end of block
|
181
|
+
next if line_code.match(/log/) # FIXME, trying to ignore log lines
|
182
|
+
callback_code << line_code.rstrip.gsub(/^#{first_indent}/,'') + "\n "
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
return callback_code
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/test/problematic_test.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require 'ruby-debug'
|
3
2
|
|
4
3
|
# Tests specifically designed to address bugs/problems discovered along the way.
|
5
4
|
class ProblematicTest < Test::Unit::TestCase
|
@@ -77,7 +76,7 @@ class ProblematicTest < Test::Unit::TestCase
|
|
77
76
|
enter do |engine|
|
78
77
|
engine.off = false
|
79
78
|
end
|
80
|
-
on :
|
79
|
+
on :put_in_gear, :to => :running
|
81
80
|
end
|
82
81
|
end
|
83
82
|
end
|
@@ -91,5 +90,57 @@ class ProblematicTest < Test::Unit::TestCase
|
|
91
90
|
|
92
91
|
assert_equal false, widget.off
|
93
92
|
end
|
93
|
+
|
94
|
+
def test_state_enter_and_exit_actions
|
95
|
+
@klass.instance_eval do
|
96
|
+
class_eval do
|
97
|
+
attr_accessor :off
|
98
|
+
attr_accessor :lights
|
99
|
+
end
|
100
|
+
|
101
|
+
define_statemachine(:engine) do
|
102
|
+
initial_state :stopped
|
103
|
+
state :stopped do
|
104
|
+
enter do |engine|
|
105
|
+
engine.off = true
|
106
|
+
engine.lights = :off
|
107
|
+
end
|
108
|
+
exit do |engine|
|
109
|
+
engine.lights = :on
|
110
|
+
end
|
111
|
+
on :start, :to => :idle
|
112
|
+
end
|
113
|
+
state :idle do
|
114
|
+
enter do |engine|
|
115
|
+
engine.off = false
|
116
|
+
end
|
117
|
+
on :put_in_gear, :to => :running
|
118
|
+
end
|
119
|
+
state :running do
|
120
|
+
on :stop, :to => :stopped
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
widget = @klass.new
|
126
|
+
|
127
|
+
assert_equal :off, widget.lights
|
128
|
+
|
129
|
+
widget.start!
|
130
|
+
|
131
|
+
assert_equal :on, widget.lights
|
132
|
+
|
133
|
+
widget.put_in_gear!
|
134
|
+
|
135
|
+
assert_equal :on, widget.lights
|
136
|
+
|
137
|
+
widget.stop!
|
138
|
+
|
139
|
+
assert_equal :off, widget.lights
|
140
|
+
|
141
|
+
widget.start!
|
142
|
+
|
143
|
+
assert_equal :on, widget.lights
|
144
|
+
end
|
94
145
|
end
|
95
146
|
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: golem_statemachine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
4
|
+
prerelease: true
|
5
5
|
segments:
|
6
|
+
- 1
|
7
|
+
- 1
|
6
8
|
- 0
|
7
|
-
-
|
8
|
-
|
9
|
-
version: 0.9.5
|
9
|
+
- pre2
|
10
|
+
version: 1.1.0.pre2
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Matt Zukowski
|
@@ -14,7 +15,7 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date:
|
18
|
+
date: 2012-02-13 00:00:00 -05:00
|
18
19
|
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
@@ -70,6 +71,7 @@ files:
|
|
70
71
|
- lib/golem/model/state_machine.rb
|
71
72
|
- lib/golem/model/transition.rb
|
72
73
|
- lib/golem/util/element_collection.rb
|
74
|
+
- lib/golem/visualizer.rb
|
73
75
|
- tasks/golem_statemachine_tasks.rake
|
74
76
|
- test/active_record_test.rb
|
75
77
|
- test/dsl_test.rb
|
@@ -105,11 +107,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
107
|
version: "0"
|
106
108
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
109
|
requirements:
|
108
|
-
- - "
|
110
|
+
- - ">"
|
109
111
|
- !ruby/object:Gem::Version
|
110
112
|
segments:
|
111
|
-
-
|
112
|
-
|
113
|
+
- 1
|
114
|
+
- 3
|
115
|
+
- 1
|
116
|
+
version: 1.3.1
|
113
117
|
requirements: []
|
114
118
|
|
115
119
|
rubyforge_project:
|