golem_statemachine 0.9.5 → 1.1.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
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 makes it much easier to implement and keep track of complicated, stateful behaviour,
11
- and the DSL you use to define your state machine in Ruby is specifically designed to make translation to and from UML
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
@@ -1,7 +1,7 @@
1
1
 
2
2
  $gemspec = Gem::Specification.new do |s|
3
3
  s.name = 'golem_statemachine'
4
- s.version = '0.9.5'
4
+ s.version = '1.1.0.pre2'
5
5
  s.authors = ["Matt Zukowski"]
6
6
  s.email = ["matt@roughest.net"]
7
7
  s.homepage = 'http://github.com/zuk/golem_statemachine'
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
- require 'ruby-debug'
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, "#{self} is in an unrecognized state (#{state.inspect})" unless statemachine.states[state]
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
 
@@ -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
@@ -6,6 +6,8 @@ module Golem
6
6
  attr_reader :name
7
7
  attr_reader :callbacks
8
8
  attr_reader :transitions_on_event
9
+
10
+ attr_accessor :comment
9
11
 
10
12
  def initialize(name)
11
13
  name = name.to_sym unless name.is_a?(Symbol)
@@ -8,6 +8,8 @@ module Golem
8
8
  attr_reader :to
9
9
  attr_accessor :guards
10
10
  attr_accessor :callbacks
11
+
12
+ attr_accessor :comment
11
13
 
12
14
  def initialize(from, to, guards = [], callbacks = {})
13
15
  @from = from
@@ -24,6 +24,10 @@ module Golem
24
24
  def each
25
25
  @collection.values.each{|v| yield v}
26
26
  end
27
+
28
+ def empty?
29
+ @collection.empty?
30
+ end
27
31
 
28
32
  def values
29
33
  @collection.values
@@ -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
@@ -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 :start, :to => :running
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: false
4
+ prerelease: true
5
5
  segments:
6
+ - 1
7
+ - 1
6
8
  - 0
7
- - 9
8
- - 5
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: 2011-12-07 00:00:00 -05:00
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
- - 0
112
- version: "0"
113
+ - 1
114
+ - 3
115
+ - 1
116
+ version: 1.3.1
113
117
  requirements: []
114
118
 
115
119
  rubyforge_project: