rumld 0.4.0
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/CHANGELOG +2 -0
- data/README +1 -0
- data/Rakefile +102 -0
- data/TODO +16 -0
- data/bin/rumld +50 -0
- data/lib/rumld/graph.rb +66 -0
- data/lib/rumld/node.rb +151 -0
- data/lib/rumld/options_struct.rb +59 -0
- data/lib/rumld/rdoc_inspector.rb +70 -0
- data/lib/rumld/tree.rb +62 -0
- data/lib/rumld.rb +18 -0
- data/spec/rumld_graph_spec.rb +43 -0
- data/spec/rumld_node_spec.rb +101 -0
- data/spec/rumld_rdoc_inspector_spec.rb +91 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +78 -0
- data/test/rumld_graph_test.rb +44 -0
- data/test/rumld_node_test.rb +121 -0
- data/test/rumld_rdoc_inspector_test.rb +90 -0
- data/test/rumld_tree_test.rb +59 -0
- data/test/test_helper.rb +15 -0
- metadata +102 -0
data/CHANGELOG
ADDED
data/README
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Ruby UML Diagrammer
|
data/Rakefile
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'spec/rake/spectask'
|
7
|
+
require 'lib/rumld'
|
8
|
+
|
9
|
+
NAME = 'rumld'
|
10
|
+
VERS = '0.4.0'
|
11
|
+
RDOC_OPTS = ['--all', '--quiet', '--title', 'Ruby UML Diagrammer', '--main', 'README', '--inline-source']
|
12
|
+
|
13
|
+
desc "Does a full compile, test run"
|
14
|
+
task :default => [:test]
|
15
|
+
|
16
|
+
desc "Run all examples"
|
17
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
18
|
+
t.spec_files = FileList['spec/**/*.rb']
|
19
|
+
end
|
20
|
+
|
21
|
+
Rake::RDocTask.new do |rdoc|
|
22
|
+
rdoc.rdoc_dir = 'doc/rdoc'
|
23
|
+
rdoc.options += RDOC_OPTS
|
24
|
+
rdoc.main = "README"
|
25
|
+
rdoc.rdoc_files.add ['README', 'CHANGELOG', 'lib/**/*.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
spec =
|
29
|
+
Gem::Specification.new do |s|
|
30
|
+
s.name = NAME
|
31
|
+
s.add_dependency('mocha', '>=0.5.2')
|
32
|
+
s.add_dependency('activesupport', '>=1.3.1')
|
33
|
+
s.add_dependency('hpricot', '>=0.6')
|
34
|
+
s.bindir = 'bin'
|
35
|
+
s.executables = ['rumld']
|
36
|
+
|
37
|
+
s.version = VERS
|
38
|
+
s.platform = Gem::Platform::RUBY
|
39
|
+
s.has_rdoc = true
|
40
|
+
s.rdoc_options += RDOC_OPTS
|
41
|
+
s.extra_rdoc_files = ["README", "CHANGELOG", "TODO"]
|
42
|
+
s.summary = "A simple UML Grapher for Rails objects"
|
43
|
+
s.description = s.summary
|
44
|
+
s.author = "Jeremy Friesen"
|
45
|
+
s.email = 'jeremyf@lightsky.com'
|
46
|
+
s.homepage = 'http://www.lightsky.com/'
|
47
|
+
|
48
|
+
s.files = %w(README Rakefile CHANGELOG TODO) +
|
49
|
+
Dir.glob("{doc,test,lib,spec,bin}/**/*") +
|
50
|
+
|
51
|
+
s.require_path = ["lib"]
|
52
|
+
end
|
53
|
+
|
54
|
+
Rake::GemPackageTask.new(spec) do |p|
|
55
|
+
p.need_tar = true
|
56
|
+
p.gem_spec = spec
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Run all the tests"
|
60
|
+
Rake::TestTask.new do |t|
|
61
|
+
t.libs << "test"
|
62
|
+
t.test_files = FileList['test/**/*_test.rb']
|
63
|
+
t.verbose = true
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
namespace :doc do
|
68
|
+
desc "Generate documentation for the application"
|
69
|
+
Rake::RDocTask.new("app") { |rdoc|
|
70
|
+
rdoc.rdoc_dir = 'doc/'
|
71
|
+
rdoc.title = "Rails Application Documentation"
|
72
|
+
rdoc.options << '--line-numbers'
|
73
|
+
rdoc.rdoc_files.include('lib/rumld/**/*.rb')
|
74
|
+
}
|
75
|
+
|
76
|
+
task :graph => :reapp do
|
77
|
+
|
78
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'test', '**', '*.rb')).each do |f|
|
79
|
+
require f
|
80
|
+
end
|
81
|
+
file_list = ARGV.grep(/^FILE(S)=/)
|
82
|
+
files =
|
83
|
+
if file_list.empty?
|
84
|
+
['*.rb']
|
85
|
+
else
|
86
|
+
file_list.pop.sub(/^FILE(S)=/, '').split(/(,\ )/).collect{|f| "#{f}#{'.rb' unless f.match(/\.rb$/)}"}
|
87
|
+
end
|
88
|
+
doc_folder = (ARGV.grep(/^DOC_DIR=(.*)/).pop.sub(/^DOC_DIR=/, '') rescue File.join(File.dirname(__FILE__), 'doc'))
|
89
|
+
|
90
|
+
source_folder_list = ARGV.grep(/^SOURCE_DIR(S)=/)
|
91
|
+
source_folders =
|
92
|
+
if source_folder_list.empty?
|
93
|
+
[File.join(File.dirname(__FILE__), 'lib' ), File.join(File.dirname(__FILE__), 'app', 'models' )]
|
94
|
+
else
|
95
|
+
source_folder_list.pop.sub(/^SOURCE_DIR(S)=/, '').split(/(,\ )/).collect{|f| "#{f}"}
|
96
|
+
end
|
97
|
+
|
98
|
+
File.open(File.join(File.dirname(__FILE__), 'my_graph.dot'), 'w') do |f|
|
99
|
+
f.puts Rumld::Graph.new( doc_folder, source_folders , files ).to_dot
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
✓ Better define how to layout a Module that has methods as well as sub-Modules
|
2
|
+
✓ Draw edges for included Modules as Interface
|
3
|
+
✓ Draw edges for ActiveRecord::Association
|
4
|
+
✓ Subclass edges
|
5
|
+
✓ Try to determine polymorphic relations
|
6
|
+
✓ Generate a command line interface for rumld
|
7
|
+
✓ Need to run the process against a rails project
|
8
|
+
✓ Need to switch on whether rumld is running against a rails project
|
9
|
+
- Refactor/rethink collection vs. render
|
10
|
+
- Convert to RSpec
|
11
|
+
- Check for referenced modules inside methods to infer additional relations
|
12
|
+
- Exception Nodes should appear differently
|
13
|
+
- Research ruby new gem
|
14
|
+
|
15
|
+
Bug?
|
16
|
+
- Check generator for like named constants in different module spaces (i.e. A::E & B::E)
|
data/bin/rumld
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
3
|
+
|
4
|
+
class RailsLoader
|
5
|
+
class << self
|
6
|
+
# Prevents Rails application from writing to STDOUT
|
7
|
+
def disable_stdout
|
8
|
+
@old_stdout = STDOUT.dup
|
9
|
+
STDOUT.reopen(PLATFORM =~ /mswin/ ? "NUL" : "/dev/null")
|
10
|
+
end
|
11
|
+
|
12
|
+
# Restore STDOUT
|
13
|
+
def enable_stdout
|
14
|
+
STDOUT.reopen(@old_stdout)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_environment
|
18
|
+
begin
|
19
|
+
disable_stdout
|
20
|
+
puts 'Loading Rails...'
|
21
|
+
require "config/environment"
|
22
|
+
enable_stdout
|
23
|
+
rescue LoadError
|
24
|
+
enable_stdout
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
RailsLoader.load_environment
|
31
|
+
|
32
|
+
require 'rumld'
|
33
|
+
require 'rumld/options_struct'
|
34
|
+
RUMLD_DEFAULT_FILES = ['*.rb']
|
35
|
+
options = Rumld::OptionsStruct.new
|
36
|
+
|
37
|
+
options.parse ARGV
|
38
|
+
|
39
|
+
|
40
|
+
grapher = Rumld::Graph.new( options.doc_dir, options.source_dirs , options.files )
|
41
|
+
|
42
|
+
|
43
|
+
`rm -rf #{options.doc_dir}`
|
44
|
+
|
45
|
+
rdoc = "rdoc --op #{options.doc_dir} #{grapher.files_to_process.join(' ')}"
|
46
|
+
`#{rdoc}`
|
47
|
+
|
48
|
+
File.open(options.outfile, 'w') do |f|
|
49
|
+
f.puts grapher.to_dot
|
50
|
+
end
|
data/lib/rumld/graph.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
class Rumld::Graph
|
2
|
+
extend Forwardable
|
3
|
+
attr_accessor :files, :doc_dir, :source_dirs
|
4
|
+
|
5
|
+
def initialize(doc_dir, source_dirs = [], files = [])
|
6
|
+
self.doc_dir = doc_dir
|
7
|
+
self.source_dirs = source_dirs
|
8
|
+
self.files = files
|
9
|
+
end
|
10
|
+
def_delegator :tree, :to_dot
|
11
|
+
|
12
|
+
def files_to_process
|
13
|
+
unless @files_to_process
|
14
|
+
@files_to_process = []
|
15
|
+
source_dirs.each do |dir|
|
16
|
+
files.each { |file| @files_to_process += Dir.glob(File.join(dir, '**', file)) } if files
|
17
|
+
end
|
18
|
+
@files_to_process.uniq!
|
19
|
+
@files_to_process.each{|f| require f}
|
20
|
+
end
|
21
|
+
@files_to_process
|
22
|
+
end
|
23
|
+
|
24
|
+
def tree
|
25
|
+
@tree ||= Rumld::Tree.new(self, defined_constant_names)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
# Get a list of constants that RDoc says have been defined
|
31
|
+
def defined_constant_names
|
32
|
+
@defined_constant_names ||=
|
33
|
+
files_to_process.collect do |filename|
|
34
|
+
collect_possible_constant_names(constants_defined(File.readlines(filename))).select do |constant_name|
|
35
|
+
File.exists?(Rumld::RDocInspector.doc_file_for(doc_dir, constant_name))
|
36
|
+
end
|
37
|
+
end.flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# This is a little on the ugly. Psuedo-combinatorially collect the constants
|
42
|
+
# You'll want to check the tests to see what I mean
|
43
|
+
def collect_possible_constant_names(constant_names = [], prefix = nil)
|
44
|
+
work = []
|
45
|
+
constant_names.each_with_index do |c1, i|
|
46
|
+
constant_name ="#{(prefix + '::') if prefix}#{c1}"
|
47
|
+
work << constant_name
|
48
|
+
work += collect_possible_constant_names(constant_names[(i+1)..-1], constant_name).flatten
|
49
|
+
end
|
50
|
+
work.uniq.sort
|
51
|
+
end
|
52
|
+
|
53
|
+
# This method makes no effort to properly establish the constant hierarchy
|
54
|
+
# It just tries to determine the constants that are being defined
|
55
|
+
def constants_defined(strings = [])
|
56
|
+
constants = strings.grep(/^\W*(class|module)/).collect do |constant|
|
57
|
+
constant.sub(/^\W*(class\W*<<|class|module)/, '').strip.sub(/\W\<.*/, '')
|
58
|
+
end
|
59
|
+
|
60
|
+
# only keeping constants
|
61
|
+
constants.select do |c|
|
62
|
+
c.match(/[A-Z]/)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/lib/rumld/node.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
class Rumld::Node
|
2
|
+
extend Forwardable
|
3
|
+
attr_reader :constant_name, :tree, :constant
|
4
|
+
|
5
|
+
def_delegator :tree, :doc_dir
|
6
|
+
def_delegator :rdoc_inspector, :doc_is_for_module?, :constant_is_module?
|
7
|
+
def_delegator :rdoc_inspector, :methods_by_section
|
8
|
+
def_delegator :rdoc_inspector, :doc_file_for
|
9
|
+
|
10
|
+
def initialize(tree, constant_name)
|
11
|
+
@constant_name = constant_name
|
12
|
+
@tree = tree
|
13
|
+
end
|
14
|
+
|
15
|
+
def constant
|
16
|
+
@constant ||= constant_name.constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def node_to_dot
|
20
|
+
if children?
|
21
|
+
children.sort!{|a,b| a.constant_name <=> b.constant_name}
|
22
|
+
%(subgraph cluster#{detached_constant_name} {
|
23
|
+
label = "#{detached_constant_name}";
|
24
|
+
color = grey;
|
25
|
+
#{self_to_dot(:style=>"dotted")}
|
26
|
+
#{children.collect{|n| n.node_to_dot}.join("\n")}
|
27
|
+
})
|
28
|
+
else
|
29
|
+
self_to_dot
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def inheritence_edge_to_dot
|
34
|
+
if constant.respond_to?(:superclass) && ancestor_node = tree.node_for(constant.superclass)
|
35
|
+
%("#{constant_name}" -> "#{ancestor_node.constant_name}" [arrowhead=empty])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def included_associations_to_dot
|
40
|
+
active_record_associations.collect do |assoc|
|
41
|
+
case assoc.macro.to_s
|
42
|
+
when 'belongs_to' then to_dot_for_belongs_to(assoc)
|
43
|
+
when 'has_one' then has_one_to_dot(assoc)
|
44
|
+
when 'has_many' then has_many_to_dot(assoc)
|
45
|
+
when 'has_and_belongs_to_many' then habtm_to_dot(assoc)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def included_modules_to_dot
|
51
|
+
included_modules.collect do |mod|
|
52
|
+
%("#{constant_name}" -> "#{mod}" [style=dotted, arrowhead=empty])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self_and_descendants
|
57
|
+
([self] + children.collect{|n| n.self_and_descendants}).flatten
|
58
|
+
end
|
59
|
+
|
60
|
+
def children
|
61
|
+
@children ||= []
|
62
|
+
end
|
63
|
+
|
64
|
+
def children?
|
65
|
+
!children.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def included_modules
|
69
|
+
rdoc_inspector.all_included_modules & tree.constant_names
|
70
|
+
end
|
71
|
+
|
72
|
+
# I only want constants that are one module level
|
73
|
+
# deeper than the current constant_name
|
74
|
+
def should_be_parent_of?(other_constant_name)
|
75
|
+
if match = other_constant_name.match(/^#{Regexp.escape(constant_name + '::')}/)
|
76
|
+
%(#{match}#{other_constant_name.split('::').last}) == other_constant_name
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
# We can skip this one because, for now its un-needed
|
85
|
+
def to_dot_for_belongs_to(assoc)
|
86
|
+
if assoc.options[:polymorphic]
|
87
|
+
%(
|
88
|
+
"#{assoc.class_name}" [shape = "record"]
|
89
|
+
"#{constant_name}" -> "#{assoc.class_name}" [headlabel="*", taillabel="1"]
|
90
|
+
)
|
91
|
+
else
|
92
|
+
''
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def has_one_to_dot(assoc)
|
97
|
+
if assoc.options[:as]
|
98
|
+
%("#{constant_name}" -> "#{assoc.options[:as].to_s.classify}" [arrowhead=empty, style=dotted, label="by :#{assoc.name}"])
|
99
|
+
elsif tree.constant_names.include?(assoc.class_name)
|
100
|
+
%("#{constant_name}" -> "#{assoc.class_name}" [headlabel="1", taillabel="1"])
|
101
|
+
else
|
102
|
+
''
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def has_many_to_dot(assoc)
|
107
|
+
if assoc.options[:as]
|
108
|
+
%("#{constant_name}" -> "#{assoc.options[:as].to_s.classify}" [arrowhead=empty, style=dotted, label="by :#{assoc.name}"])
|
109
|
+
elsif tree.constant_names.include?(assoc.class_name)
|
110
|
+
%("#{constant_name}" -> "#{assoc.class_name}" [headlabel="1", taillabel="*"])
|
111
|
+
else
|
112
|
+
''
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def habtm_to_dot(assoc)
|
117
|
+
''
|
118
|
+
end
|
119
|
+
|
120
|
+
def active_record_associations
|
121
|
+
constant.respond_to?(:reflect_on_all_associations) ? constant.reflect_on_all_associations : []
|
122
|
+
end
|
123
|
+
|
124
|
+
def self_to_dot(options = {})
|
125
|
+
default_options = { :shape => "record", :label => "{#{dot_formatted_node_header}||#{format_methods(methods_by_section[:public_class], '.')}#{format_methods(methods_by_section[:public_instance], '#')}}"}
|
126
|
+
options = default_options.merge(options)
|
127
|
+
%("#{constant_name}" [#{options.collect{|key, value| key.to_s + '="' + value.to_s + '"'}.join(', ') }] )
|
128
|
+
end
|
129
|
+
|
130
|
+
def detached_constant_name
|
131
|
+
constant_name.split('::').last
|
132
|
+
end
|
133
|
+
|
134
|
+
def rdoc_inspector
|
135
|
+
@rdoc_inspector ||= Rumld::RDocInspector.new( self, constant_name )
|
136
|
+
end
|
137
|
+
|
138
|
+
# Add a << module >> to the doc if its a Module
|
139
|
+
def dot_formatted_node_header
|
140
|
+
"#{'\\<\\< module \\>\\>\\l' if constant_is_module?}#{detached_constant_name}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def format_methods(methods = [], prefix = '')
|
144
|
+
if (methods && !methods.empty?)
|
145
|
+
methods.collect do |method|
|
146
|
+
prefix.to_s + method.gsub(/([{}>\|])/){|match| "\\#{match.to_s}"}
|
147
|
+
end.join('\\l') + '\\l'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Rumld
|
4
|
+
class OptionsStruct < OpenStruct
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
default_options = { :doc_dir => './doc',
|
10
|
+
:files => RUMLD_DEFAULT_FILES,
|
11
|
+
:outfile => './ruml.dot',
|
12
|
+
:source_dirs => ['./lib', './app/models'] }
|
13
|
+
super(default_options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(args)
|
17
|
+
@opt_parser = OptionParser.new do |opts|
|
18
|
+
opts.banner = "Usage: rumld [options]"
|
19
|
+
opts.separator ""
|
20
|
+
opts.separator "Options:"
|
21
|
+
opts.on("-d", "--doc_dir ./ruml_doc", "Directory to generate rumld's", "working RDoc") do |value|
|
22
|
+
self.doc_dir = value
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("-o", "--outfile ruml.dot", "Output file") do |value|
|
26
|
+
self.outfile = value
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-s", "--source_dirs lib,app/models", Array, "Directories for files") do |value|
|
30
|
+
self.source_dirs = value
|
31
|
+
end
|
32
|
+
opts.on("-f", "--files file1[,fileN]", Array, "Files to graph", "defaults to #{RUMLD_DEFAULT_FILES.join(',')}") do |value|
|
33
|
+
self.files = value
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
begin
|
39
|
+
@opt_parser.parse!(args)
|
40
|
+
rescue OptionParser::AmbiguousOption
|
41
|
+
option_error "Ambiguous option"
|
42
|
+
rescue OptionParser::InvalidOption
|
43
|
+
option_error "Invalid option"
|
44
|
+
rescue OptionParser::InvalidArgument
|
45
|
+
option_error "Invalid argument"
|
46
|
+
rescue OptionParser::MissingArgument
|
47
|
+
option_error "Missing argument"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def option_error(msg)
|
54
|
+
STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Rumld::RDocInspector
|
2
|
+
|
3
|
+
class << self
|
4
|
+
def doc_file_for(doc_dir, constant_name)
|
5
|
+
File.join(doc_dir, 'classes', "#{constant_name.gsub('::', '/')}.html")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
XPATH_TO_METHOD_SECTION_HEADER = '#methods > h3'
|
10
|
+
METHOD_HEADING_TABLE = {'Public Class methods' => :public_class,
|
11
|
+
'Public Instance methods' => :public_instance,
|
12
|
+
'Protected Instance methods' => :protected_instance,
|
13
|
+
'Protected Class methods' => :protected_class
|
14
|
+
}
|
15
|
+
attr_reader :node, :constant_name
|
16
|
+
def initialize(node, constant_name)
|
17
|
+
@node = node
|
18
|
+
@constant_name = constant_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def doc
|
22
|
+
@doc ||= open( doc_file_for ) {|f| Hpricot(f)}
|
23
|
+
end
|
24
|
+
|
25
|
+
def doc_file_for
|
26
|
+
self.class.doc_file_for(node.doc_dir, constant_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_included_modules
|
30
|
+
self.doc.search('#includes #includes-list .include-name').collect{|n| n.inner_text}
|
31
|
+
end
|
32
|
+
|
33
|
+
def doc_is_for_module?
|
34
|
+
if @doc_is_for_module.nil?
|
35
|
+
@doc_is_for_module = (self.doc.search('#classHeader table.header-table tr td strong')[0].inner_html == 'Module')
|
36
|
+
end
|
37
|
+
@doc_is_for_module
|
38
|
+
end
|
39
|
+
|
40
|
+
# Given an RDoc class.html file, kick back a hash of methods
|
41
|
+
def methods_by_section
|
42
|
+
unless @methods_by_section
|
43
|
+
@methods_by_section = {}
|
44
|
+
(self.doc/XPATH_TO_METHOD_SECTION_HEADER).each do |method_section|
|
45
|
+
if collector_key = METHOD_HEADING_TABLE[method_section.inner_html]
|
46
|
+
@methods_by_section[collector_key] = find_methods_in_section(method_section)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
@methods_by_section
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_methods_in_section(method_section)
|
54
|
+
collector = []
|
55
|
+
# Oh why did method_section lose the following method?
|
56
|
+
child = method_section.next_sibling
|
57
|
+
while child && !child.css_path.include?(XPATH_TO_METHOD_SECTION_HEADER)
|
58
|
+
# We got to a new section, so drop out of this silly non-sense
|
59
|
+
break if child.css_path.include?(XPATH_TO_METHOD_SECTION_HEADER)
|
60
|
+
(child/'.method-heading a').each do |signature|
|
61
|
+
if string = signature.inner_text.strip.chomp
|
62
|
+
collector << string
|
63
|
+
end
|
64
|
+
end
|
65
|
+
child = child.next_sibling
|
66
|
+
end
|
67
|
+
collector.sort!
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/lib/rumld/tree.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
class Rumld::Tree
|
2
|
+
extend Forwardable
|
3
|
+
def_delegator :graph, :doc_dir
|
4
|
+
attr_reader :constant_names, :graph
|
5
|
+
def initialize( graph, constant_names = [])
|
6
|
+
@graph = graph
|
7
|
+
@constant_names = constant_names.uniq.sort
|
8
|
+
build_nodes
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_dot
|
12
|
+
%(digraph uml_diagram {\n graph[rotate=landscape, ratio=fill, overlap=false, splines=true]
|
13
|
+
#{draw_nodes}
|
14
|
+
#{draw_included_modules}
|
15
|
+
#{draw_associations}
|
16
|
+
#{draw_inheritence_edges}
|
17
|
+
}
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def nodes
|
22
|
+
@nodes ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def all_nodes
|
26
|
+
nodes.collect{|n| n.self_and_descendants}.flatten
|
27
|
+
end
|
28
|
+
|
29
|
+
def node_for( constant )
|
30
|
+
all_nodes.detect{|node| node.constant == constant || node.constant_name == constant}
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def draw_inheritence_edges
|
36
|
+
all_nodes.collect{|n| n.inheritence_edge_to_dot}.flatten.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
def draw_nodes
|
40
|
+
nodes.collect{|n| n.node_to_dot}.flatten.join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
def draw_associations
|
44
|
+
all_nodes.collect{|n| n.included_associations_to_dot}.flatten.join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw_included_modules
|
48
|
+
all_nodes.collect{|n| n.included_modules_to_dot}.flatten.join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_nodes
|
52
|
+
constant_names.each do |constant_name|
|
53
|
+
if node = all_nodes.detect{|node| node.should_be_parent_of?(constant_name)}
|
54
|
+
node.children << Rumld::Node.new(self, constant_name)
|
55
|
+
else
|
56
|
+
nodes << Rumld::Node.new(self, constant_name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
nodes.sort!{|a,b| a.constant_name <=> b.constant_name}
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/lib/rumld.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'hpricot'
|
3
|
+
|
4
|
+
require 'hpricot'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
unless defined? RAILS_ENV
|
8
|
+
gem 'activesupport'
|
9
|
+
require 'active_support/core_ext/string'
|
10
|
+
end
|
11
|
+
|
12
|
+
module Rumld
|
13
|
+
end
|
14
|
+
|
15
|
+
require File.join(File.dirname(__FILE__), 'rumld','graph')
|
16
|
+
require File.join(File.dirname(__FILE__), 'rumld','tree')
|
17
|
+
require File.join(File.dirname(__FILE__), 'rumld','node')
|
18
|
+
require File.join(File.dirname(__FILE__), 'rumld','rdoc_inspector')
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
module Rumld
|
4
|
+
describe Graph do
|
5
|
+
before(:each) do
|
6
|
+
@graph = Rumld::Graph.new('', [],[])
|
7
|
+
end
|
8
|
+
it 'delegates to_dot to tree' do
|
9
|
+
@graph.expects(:tree).returns(stub('Tree', :to_dot => :to_dot))
|
10
|
+
@graph.to_dot.should == :to_dot
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'collects possible constant names should recursively build a larger constant list' do
|
14
|
+
@graph.send(:collect_possible_constant_names, ['One', 'Two::A', 'Three']).should == ['One', 'One::Two::A', 'One::Two::A::Three', 'Three', 'Two::A', 'Two::A::Three']
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'collects all of the demodulized constants that are declared' do
|
18
|
+
file = <<-EOV
|
19
|
+
class One
|
20
|
+
class Two::Three < One
|
21
|
+
include Eight
|
22
|
+
extend Nine
|
23
|
+
def function(one)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
module Four
|
27
|
+
end
|
28
|
+
class << self
|
29
|
+
end
|
30
|
+
|
31
|
+
class << Five
|
32
|
+
end
|
33
|
+
end
|
34
|
+
EOV
|
35
|
+
@graph.send(:constants_defined, file.split("\n")).should == ['One', 'Two::Three', 'Four', 'Five']
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'uniquely determines all the matching files to process' do
|
39
|
+
@graph = Rumld::Graph.new(nil, [File.dirname(__FILE__),File.dirname(__FILE__)], [File.basename(__FILE__)])
|
40
|
+
@graph.send(:files_to_process).should == [__FILE__]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|