rumld 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|