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.
@@ -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