ruby-uml 0.2.2
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 +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
|