ruby-uml 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +61 -0
- data/additional/aspectr-0-4-0.patch +53 -0
- data/bin/sequence_diagram_diff +183 -0
- data/examples/class_diagram_example.rb +69 -0
- data/examples/highlevel_backtracer_example.rb +47 -0
- data/examples/sequence_diagram_example +13 -0
- data/examples/sequence_diagram_generator.rb +41 -0
- data/examples/temperature_new.rb +25 -0
- data/examples/temperature_old.rb +24 -0
- data/lib/uml/class_diagram.rb +208 -0
- data/lib/uml/class_diagram_helper.rb +61 -0
- data/lib/uml/graphviz_helper.rb +90 -0
- data/lib/uml/highlevel_backtracer.rb +73 -0
- data/lib/uml/lowlevel_backtracer.rb +56 -0
- data/lib/uml/method_helper.rb +138 -0
- data/lib/uml/sequence_diagram.rb +157 -0
- data/lib/uml/sequence_diagram_helper.rb +100 -0
- data/tests/ts_all.rb +5 -0
- metadata +75 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
class TempSensor
|
4
|
+
include Observable
|
5
|
+
|
6
|
+
def run(start = 26)
|
7
|
+
start.upto(27) do |temp|
|
8
|
+
changed
|
9
|
+
notify_observers temp
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class TempAlarm
|
15
|
+
|
16
|
+
def initialize(sensor)
|
17
|
+
sensor.add_observer self
|
18
|
+
end
|
19
|
+
|
20
|
+
def update(temp)
|
21
|
+
$stderr.puts temp if temp > 26
|
22
|
+
temp
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
class TempSensor
|
4
|
+
include Observable
|
5
|
+
|
6
|
+
def run(start = 26)
|
7
|
+
start.upto(27) do |temp|
|
8
|
+
changed
|
9
|
+
notify_observers temp
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class TempAlarm
|
15
|
+
|
16
|
+
def initialize(sensor)
|
17
|
+
sensor.add_observer self
|
18
|
+
end
|
19
|
+
|
20
|
+
def update(temp)
|
21
|
+
temp
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'uml/lowlevel_backtracer'
|
2
|
+
require 'uml/class_diagram_helper'
|
3
|
+
|
4
|
+
module UML
|
5
|
+
|
6
|
+
# Generates dot representation out of program execution.
|
7
|
+
#
|
8
|
+
# Starts with information gathering as soon as it is created.
|
9
|
+
#
|
10
|
+
# TODO Parser in to_dot who checks double directed association and substitutes with undirected.
|
11
|
+
#
|
12
|
+
# TODO When cluster_packages is true, module is printed as subgraph and node.
|
13
|
+
class ClassDiagram
|
14
|
+
include Graphviz
|
15
|
+
|
16
|
+
# Configuration options:
|
17
|
+
# show_private_methods:: If +true+, includes private instance methods in Nodes.
|
18
|
+
#
|
19
|
+
# Default is +false+.
|
20
|
+
# show_protected_methods:: If +true+, includes protected instance methods in Nodes.
|
21
|
+
#
|
22
|
+
# Default is +false+.
|
23
|
+
# show_public_methods:: If +true+, includes public instance methods in Nodes.
|
24
|
+
#
|
25
|
+
# Default is +true+.
|
26
|
+
# cluster_packages:: If +true+, namespaces are clustered into a package-like representation.
|
27
|
+
#
|
28
|
+
# Graphviz can't plot UML-package with tab, so there is just the package name in
|
29
|
+
# the top-left corner of a box. Defaults to +false+.
|
30
|
+
# exclude:: Array which contains information for classes to exclude from graph.
|
31
|
+
#
|
32
|
+
# Can contain regular expressions, strings, symbols, or Constants.
|
33
|
+
#
|
34
|
+
# Defaults to empty Array.
|
35
|
+
# include:: Array which contains information for desired classes.
|
36
|
+
#
|
37
|
+
# Can contain regular expressions, strings, symbols, or Constants.
|
38
|
+
#
|
39
|
+
# Defaults to empty Array.
|
40
|
+
def initialize(config = {})
|
41
|
+
@config = {
|
42
|
+
:show_private_methods => false,
|
43
|
+
:show_protected_methods => false,
|
44
|
+
:show_public_methods => true,
|
45
|
+
:cluster_packages => false,
|
46
|
+
:exclude => [],
|
47
|
+
:include => []
|
48
|
+
}.update(config)
|
49
|
+
|
50
|
+
@graph = Graph.new('digraph', 'class_diagram')
|
51
|
+
@graph.default_node_attributes[:shape] = 'record'
|
52
|
+
@graph.default_graph_attributes[:labelloc] = 't'
|
53
|
+
@graph.default_graph_attributes[:labeljust] = 'l'
|
54
|
+
|
55
|
+
@tracer = LowlevelBacktracer.instance
|
56
|
+
@tracer.add_observer self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns a dot representation of gathered information
|
60
|
+
def to_dot
|
61
|
+
@graph.to_dot
|
62
|
+
end
|
63
|
+
|
64
|
+
# Manually add a klass with its included modules and superclasses to graph.
|
65
|
+
#
|
66
|
+
# klass has to be accepted by configuration options.
|
67
|
+
def include(klass)
|
68
|
+
generate_node_with_supers(inheritance(klass))
|
69
|
+
end
|
70
|
+
|
71
|
+
def update(event, tracer) # :nodoc:
|
72
|
+
return if event != :call
|
73
|
+
|
74
|
+
right = tracer.call_stack[-1]
|
75
|
+
return if not accepted(right[:klass])
|
76
|
+
|
77
|
+
left = called_by_interesting(tracer.call_stack)
|
78
|
+
|
79
|
+
include(right[:klass])
|
80
|
+
if left
|
81
|
+
include(left[:klass])
|
82
|
+
@graph.edges[[left[:klass], right[:klass]]] ||= Dependency.new(left[:klass], right[:klass]) if left[:klass] != right[:klass]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Creates parent subgraphs and returns needed one if package clustering is activated.
|
89
|
+
# If not, returns just toplevel graph.
|
90
|
+
def get_graph(klass)
|
91
|
+
return @graph if not @config[:cluster_packages]
|
92
|
+
result = @graph
|
93
|
+
namespaces = klass.to_s.split('::')
|
94
|
+
object = namespaces.pop
|
95
|
+
namespaces.each do |namespace|
|
96
|
+
unless result.subgraphs[namespace]
|
97
|
+
result.subgraphs[namespace] = Graph.new('subgraph', "cluster_#{namespace}")
|
98
|
+
result.subgraphs[namespace].default_graph_attributes[:label] = namespace
|
99
|
+
end
|
100
|
+
result = result.subgraphs[namespace]
|
101
|
+
end
|
102
|
+
return result
|
103
|
+
end
|
104
|
+
|
105
|
+
# When package clustering is activated, node labels contain just name of module.
|
106
|
+
# If not, the full qualified name is returned.
|
107
|
+
def get_nodelabel(klass)
|
108
|
+
@config[:cluster_packages] ? klass.to_s.split('::').last : klass
|
109
|
+
end
|
110
|
+
|
111
|
+
def called_by_interesting(stack)
|
112
|
+
stack.reverse_each do |element|
|
113
|
+
next if element == stack.last
|
114
|
+
return element if accepted(element[:klass])
|
115
|
+
end
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
|
119
|
+
# Checks if klass is wanted or not according to +:include+ and +:exclude+ configuration options.
|
120
|
+
# If +:include+ is empty, everything is accepted at first place.
|
121
|
+
# Exclusions are checked at second.
|
122
|
+
def accepted(klass)
|
123
|
+
(@config[:include].empty? ? true : test_inclusion(@config[:include], klass)) and not test_inclusion(@config[:exclude], klass)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Is called by accepted.
|
127
|
+
# Makes it possible to use regular expressions, constants, strings or symbols
|
128
|
+
# in +:include+ and +:exclude+ configuration options.
|
129
|
+
def test_inclusion(array, klass)
|
130
|
+
array.each do |element|
|
131
|
+
case element
|
132
|
+
when Regexp
|
133
|
+
return true if element =~ klass.to_s
|
134
|
+
when String, Symbol
|
135
|
+
return true if element.to_s == klass.to_s
|
136
|
+
else
|
137
|
+
return true if element == klass
|
138
|
+
end
|
139
|
+
end
|
140
|
+
false
|
141
|
+
end
|
142
|
+
|
143
|
+
def generate_public_method_hash(object)
|
144
|
+
object.public_instance_methods(false).inject({}) do |result, m|
|
145
|
+
result[m] = '+'
|
146
|
+
result
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def generate_protected_method_hash(object)
|
151
|
+
object.protected_instance_methods(false).inject({}) do |result, m|
|
152
|
+
result[m] = '#'
|
153
|
+
result
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def generate_private_method_hash(object)
|
158
|
+
object.private_instance_methods(false).inject({}) do |result, m|
|
159
|
+
result[m] = '-'
|
160
|
+
result
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Add or update a node in graph with desired informations.
|
165
|
+
# Node is integrated in right graph, according to +:cluster_packages+
|
166
|
+
def generate_node(klass)
|
167
|
+
target_graph = get_graph(klass)
|
168
|
+
target_graph.nodes[klass] ||= ClassNode.new(klass, get_nodelabel(klass))
|
169
|
+
target_graph.nodes[klass].methods.update(generate_public_method_hash(klass)) if @config[:show_public_methods]
|
170
|
+
target_graph.nodes[klass].methods.update(generate_protected_method_hash(klass)) if @config[:show_protected_methods]
|
171
|
+
target_graph.nodes[klass].methods.update(generate_private_method_hash(klass)) if @config[:show_private_methods]
|
172
|
+
end
|
173
|
+
|
174
|
+
# Uses hash from +inheritance+ to generate needed nodes with generalization edges
|
175
|
+
def generate_node_with_supers(hash, subclass = nil)
|
176
|
+
hash.each do |key, value|
|
177
|
+
generate_node(key)
|
178
|
+
generate_node_with_supers(value, key)
|
179
|
+
|
180
|
+
# Generalization always has high priority. That means an existing edge at
|
181
|
+
# [subclass, key] is overwritten.
|
182
|
+
# Edges are always integrated in top graph.
|
183
|
+
@graph.edges[[subclass, key]] = Generalization.new(subclass, key) if subclass
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns recursive hash with included modules and superclasses.
|
188
|
+
# Objects that are not accepted by configuration options and their predecessors
|
189
|
+
# are omitted.
|
190
|
+
# Modules are included once in deepest object that included it.
|
191
|
+
def inheritance(klass, excludes = [])
|
192
|
+
result = {}
|
193
|
+
if klass and (accepted(klass) and not excludes.include?(klass))
|
194
|
+
|
195
|
+
result[klass] = klass.respond_to?(:superclass) ? inheritance(klass.superclass, excludes) : {}
|
196
|
+
excludes << klass
|
197
|
+
|
198
|
+
klass.included_modules.each do |mod|
|
199
|
+
result[klass].update(inheritance(mod, excludes))
|
200
|
+
excludes << mod
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
result
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'uml/graphviz_helper'
|
2
|
+
|
3
|
+
module UML
|
4
|
+
module Graphviz
|
5
|
+
|
6
|
+
class ClassNode < Node
|
7
|
+
|
8
|
+
attr_accessor :methods
|
9
|
+
|
10
|
+
def initialize(name, title)
|
11
|
+
super(name)
|
12
|
+
@title = title
|
13
|
+
# key = method_symbol, value = visibility
|
14
|
+
@methods = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_dot(indent = 0)
|
18
|
+
@attributes[:label] = label
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def label
|
25
|
+
result = "{#{@title}"
|
26
|
+
result << '|' if not @methods.empty?
|
27
|
+
@methods.each do |symbol, visibility|
|
28
|
+
result << "#{visibility} #{escape_symbol(symbol)}()\\l"
|
29
|
+
end
|
30
|
+
result << '}'
|
31
|
+
end
|
32
|
+
|
33
|
+
def escape_symbol(symbol)
|
34
|
+
symbol.gsub(/</, '\<').gsub(/>/, '\>').gsub(/\|/, '\|')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Generalization < Edge
|
39
|
+
def initialize(tail, head)
|
40
|
+
super
|
41
|
+
@attributes[:arrowhead] = 'onormal'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# aka directed association
|
46
|
+
class Dependency < Edge
|
47
|
+
def initialize(tail, head)
|
48
|
+
super
|
49
|
+
@attributes[:arrowhead] = 'vee'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Association < Edge
|
54
|
+
def initialize(tail, head)
|
55
|
+
super
|
56
|
+
@attributes[:arrowhead] = 'none'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module UML
|
2
|
+
module Graphviz
|
3
|
+
|
4
|
+
class Attributes < Hash
|
5
|
+
def to_dot
|
6
|
+
return '' if empty?
|
7
|
+
result = inject([]) do |tmp, value|
|
8
|
+
tmp << "#{value[0]} = \"#{value[1]}\""
|
9
|
+
end
|
10
|
+
'[' + result.join(', ') + ']'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Graph
|
15
|
+
|
16
|
+
attr_accessor :subgraphs, :nodes, :edges
|
17
|
+
attr_reader :default_graph_attributes,
|
18
|
+
:default_node_attributes,
|
19
|
+
:default_edge_attributes
|
20
|
+
|
21
|
+
def initialize(type, name)
|
22
|
+
@name = name
|
23
|
+
@type = type
|
24
|
+
@subgraphs = {}
|
25
|
+
@nodes = {}
|
26
|
+
@edges = {}
|
27
|
+
@default_graph_attributes = Attributes.new
|
28
|
+
@default_node_attributes = Attributes.new
|
29
|
+
@default_edge_attributes = Attributes.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_dot(indent = 0)
|
33
|
+
result = ''
|
34
|
+
result << ' ' * indent << "#{@type} #{@name} {\n"
|
35
|
+
result << ' ' * (indent + 1) << "graph #{@default_graph_attributes.to_dot};\n" unless @default_graph_attributes.empty?
|
36
|
+
result << ' ' * (indent + 1) << "node #{@default_node_attributes.to_dot};\n" unless @default_node_attributes.empty?
|
37
|
+
result << ' ' * (indent + 1) << "edge #{@default_edge_attributes.to_dot};\n" unless @default_edge_attributes.empty?
|
38
|
+
result << collection_to_dot(@subgraphs.values, indent)
|
39
|
+
result << collection_to_dot(@nodes.values, indent)
|
40
|
+
result << collection_to_dot(@edges.values, indent)
|
41
|
+
result << ' ' * indent << '}'
|
42
|
+
end
|
43
|
+
|
44
|
+
def name_to_id(object)
|
45
|
+
self.class.name_to_id object
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.name_to_id(object)
|
49
|
+
'_' << object.to_s.gsub(/::/, '_')
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def collection_to_dot(collection, indent)
|
54
|
+
collection.inject([]) { |result, element| result << "#{element.to_dot(indent + 1)}\n" }.to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Node
|
59
|
+
|
60
|
+
attr_reader :attributes, :name
|
61
|
+
|
62
|
+
def initialize(name)
|
63
|
+
@name = name
|
64
|
+
@attributes = Attributes.new
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_dot(indent = 0)
|
68
|
+
"#{' ' * indent}#{Graph::name_to_id(name)} #{@attributes.to_dot};"
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
class Edge
|
74
|
+
|
75
|
+
attr_reader :attributes
|
76
|
+
|
77
|
+
def initialize(tail, head)
|
78
|
+
@tail = tail
|
79
|
+
@head = head
|
80
|
+
@attributes = Attributes.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_dot(indent = 0)
|
84
|
+
"#{' ' * indent}#{Graph::name_to_id(@tail)} -> #{Graph::name_to_id(@head)} #{@attributes.to_dot};"
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'observer'
|
2
|
+
require 'aspectr'
|
3
|
+
require 'uml/method_helper'
|
4
|
+
|
5
|
+
module UML
|
6
|
+
|
7
|
+
class HighlevelBacktracer < AspectR::Aspect
|
8
|
+
include Observable
|
9
|
+
include MethodHelper
|
10
|
+
|
11
|
+
# +call_stack+ is an array with hashes as elements.
|
12
|
+
# Each element represents a call to a wrapped method with topmost element at last.
|
13
|
+
#
|
14
|
+
# Element has following keys as symbols:
|
15
|
+
# args:: holds an array of the arguments to method
|
16
|
+
# object:: reference to receiver of method
|
17
|
+
# method_symbol:: symbol of called method
|
18
|
+
# real_object:: the real receiver of method in inheritance tree
|
19
|
+
# returns:: return value of method. Only valid if method returned already
|
20
|
+
attr_reader :call_stack
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
|
24
|
+
@call_stack = []
|
25
|
+
end
|
26
|
+
|
27
|
+
# Wrap given methods of class or instance.
|
28
|
+
#
|
29
|
+
# each wrapped method will notify observers with <tt>(symbol, self)</tt>
|
30
|
+
# where symbol can be +:call+ or +:return+
|
31
|
+
def include(klass, *methods)
|
32
|
+
methods.each do |method_symbol|
|
33
|
+
add_advice klass, PRE, method_symbol, :before
|
34
|
+
add_advice klass, POST, method_symbol, :after
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def before(method_string, object, return_value, *args)
|
41
|
+
method_symbol = method_string.to_sym
|
42
|
+
|
43
|
+
actual_information = {
|
44
|
+
:args => args,
|
45
|
+
:object => object,
|
46
|
+
:method_symbol => method_symbol,
|
47
|
+
:real_object => get_real_receiver_of_method(object, method_symbol)
|
48
|
+
}
|
49
|
+
|
50
|
+
@call_stack.push actual_information
|
51
|
+
|
52
|
+
changed
|
53
|
+
notify_observers :call, self
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def after(method_string, object, return_value, *args)
|
58
|
+
method_symbol = method_string.to_sym
|
59
|
+
|
60
|
+
actual_information = {
|
61
|
+
:returns => return_value
|
62
|
+
}
|
63
|
+
|
64
|
+
@call_stack.last.update actual_information
|
65
|
+
|
66
|
+
changed
|
67
|
+
notify_observers :return, self
|
68
|
+
|
69
|
+
@call_stack.pop
|
70
|
+
end
|
71
|
+
|
72
|
+
end # class Backtracer
|
73
|
+
end # module UML
|