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