le1t0-dige 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +0 -0
- data/bin/dige +85 -0
- data/lib/dige/association.rb +25 -0
- data/lib/dige/associations.rb +39 -0
- data/lib/dige/class_base.rb +7 -0
- data/lib/dige/dige.rb +53 -0
- data/lib/dige/errors.rb +1 -0
- data/lib/dige/graphviz_diagram/entity_relationship_diagram.rb +18 -0
- data/lib/dige/graphviz_diagram/uml_class_diagram.rb +24 -0
- data/lib/dige/graphviz_diagram.rb +94 -0
- data/lib/dige/instantiator/data_mapper.rb +11 -0
- data/lib/dige/instantiator/rails2.rb +99 -0
- data/lib/dige/instantiator/rails3.rb +99 -0
- data/lib/dige/instantiator.rb +15 -0
- data/lib/dige/klass.rb +13 -0
- data/lib/dige/klasses.rb +21 -0
- data/lib/dige/list.rb +19 -0
- data/lib/dige/parent_child_relation.rb +13 -0
- data/lib/dige/parent_child_relations.rb +17 -0
- data/lib/dige/project_type_recognizer/data_mapper.rb +7 -0
- data/lib/dige/project_type_recognizer/rails2.rb +7 -0
- data/lib/dige/project_type_recognizer/rails3.rb +7 -0
- data/lib/dige/project_type_recognizer.rb +11 -0
- data/lib/dige.rb +13 -0
- metadata +79 -0
data/README
ADDED
File without changes
|
data/bin/dige
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
$diagram_types=[]
|
6
|
+
$project_type=:unknown
|
7
|
+
|
8
|
+
def action(diagram_type, project_type)
|
9
|
+
if $project_type == :unknown
|
10
|
+
"generate_#{diagram_type}"
|
11
|
+
else
|
12
|
+
"generate_#{project_type}_#{diagram_type}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def env
|
17
|
+
ENV.select { |k,v| [ "NO_RM", "DEBUG", "FILE", "IGNORE_CLASSES" ].include?(k) }.collect do |k,v|
|
18
|
+
"#{k}=#{v}"
|
19
|
+
end.compact.join(' ')
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(runner, call_style)
|
23
|
+
$diagram_types.each do |diagram_type|
|
24
|
+
cmd = case call_style
|
25
|
+
when :argument
|
26
|
+
"#{env} #{runner} \"require 'dige' ; Dige.#{action(diagram_type, $project_type)}\""
|
27
|
+
when :piped
|
28
|
+
"echo Dige.#{action(diagram_type, $project_type)} | #{env} #{runner}"
|
29
|
+
end
|
30
|
+
puts cmd if ENV['DEBUG'] and ENV['DEBUG'].upcase == 'TRUE'
|
31
|
+
raise unless IO.popen(cmd) do |p|
|
32
|
+
while !p.eof? && l = p.readline do
|
33
|
+
puts l
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: #{File.basename($0)} [path]"
|
41
|
+
|
42
|
+
opts.on("-t", "--type TYPE", String, "Explicitly set project type to one of <rails3, rails2, datamapper>") do |v|
|
43
|
+
$project_type=v.to_sym
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("-c", "Generate UML Class Diagram") do
|
47
|
+
$diagram_types << :uml_class_diagram
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-e", "Generate Entity Relationship Diagram") do
|
51
|
+
$diagram_types << :entity_relationship_diagram
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-h", "--help", "Displays this help info") do
|
55
|
+
puts opts
|
56
|
+
exit 0
|
57
|
+
end
|
58
|
+
|
59
|
+
begin
|
60
|
+
opts.parse!(ARGV)
|
61
|
+
rescue OptionParser::ParseError => e
|
62
|
+
warn e.message
|
63
|
+
puts opts
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
unless [
|
70
|
+
[ [ File.join(File.expand_path('.'), "script", "rails"), "runner" ], :argument ], # rails 3
|
71
|
+
[ File.join(File.expand_path('.'), "script", "runner"), :argument ], # rails 2 and other projects which provide a runner
|
72
|
+
[ File.join(File.expand_path('.'), "script", "console"), :piped ] # non-rails, but something that has console
|
73
|
+
].inject(false) do |has_run, runner|
|
74
|
+
if File.exists?([runner.first].flatten.first)
|
75
|
+
run([runner.first].flatten.join(' '), runner.last)
|
76
|
+
true
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
abort "Can't find anything (script/runner, script/console, ..) to run Dige in your project"
|
82
|
+
end
|
83
|
+
rescue
|
84
|
+
abort "Failed to generate diagram(s)"
|
85
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Dige::Association
|
2
|
+
include Dige::ClassBase
|
3
|
+
attr_accessor :from_class_name, :from_table_name, :to_class_name, :to_table_name, :from_arity, :to_arity, :type, :table_name, :macro
|
4
|
+
|
5
|
+
def debug_inspect
|
6
|
+
"#{from_class_name}(#{from_table_name}) #{from_arity} =#{table_name ? "[#{table_name}]" : ""}> #{to_arity} #{to_class_name}(#{to_table_name})"
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(objekt)
|
10
|
+
self.type == objekt.type && (
|
11
|
+
(
|
12
|
+
self.from_class_name == objekt.from_class_name &&
|
13
|
+
self.from_table_name == objekt.from_table_name &&
|
14
|
+
self.to_class_name == objekt.to_class_name &&
|
15
|
+
self.to_table_name == objekt.to_table_name
|
16
|
+
) ||
|
17
|
+
(
|
18
|
+
self.from_class_name == objekt.to_class_name &&
|
19
|
+
self.from_table_name == objekt.to_table_name &&
|
20
|
+
self.to_class_name == objekt.from_class_name &&
|
21
|
+
self.to_table_name == objekt.from_table_name
|
22
|
+
)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Dige::Associations
|
2
|
+
include Dige::List
|
3
|
+
|
4
|
+
def register(from_class_name, from_table_name, to_class_name, to_table_name, type, assoc_macro, table_name)
|
5
|
+
# assoc_macro can be : :has_many, :belongs_to, :has_one
|
6
|
+
|
7
|
+
tail_class_name = [ :belongs_to ].include?(assoc_macro) ? from_class_name : to_class_name
|
8
|
+
tail_table_name = [ :belongs_to ].include?(assoc_macro) ? from_table_name : to_table_name
|
9
|
+
head_class_name = [ :belongs_to ].include?(assoc_macro) ? to_class_name : from_class_name
|
10
|
+
head_table_name = [ :belongs_to ].include?(assoc_macro) ? to_table_name : from_table_name
|
11
|
+
|
12
|
+
objekt = Dige::Association.new({
|
13
|
+
:from_class_name => tail_class_name,
|
14
|
+
:from_table_name => tail_table_name,
|
15
|
+
:to_class_name => head_class_name,
|
16
|
+
:to_table_name => head_table_name,
|
17
|
+
:type => type,
|
18
|
+
:table_name => table_name
|
19
|
+
})
|
20
|
+
objekt.macro = assoc_macro if type == :polymorphic
|
21
|
+
unless return_objekt = find(objekt)
|
22
|
+
list << objekt
|
23
|
+
return_objekt = objekt
|
24
|
+
end
|
25
|
+
if assoc_macro == :has_many || assoc_macro == :has_and_belongs_to_many
|
26
|
+
return_objekt.from_arity = '0..*'
|
27
|
+
elsif assoc_macro == :has_one
|
28
|
+
return_objekt.from_arity = '1'
|
29
|
+
elsif assoc_macro == :belongs_to
|
30
|
+
return_objekt.to_arity = '1'
|
31
|
+
end
|
32
|
+
return_objekt
|
33
|
+
end
|
34
|
+
|
35
|
+
def all(type = :normal)
|
36
|
+
list.select { |list_item| type == :all || list_item.type == type }
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/lib/dige/dige.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
class Dige
|
2
|
+
|
3
|
+
IGNORE_CLASSES = (ENV['IGNORE_CLASSES'] || "").split(/,/) + [ 'ActiveRecord::Base' ]
|
4
|
+
|
5
|
+
DIAGRAM_TYPES = {
|
6
|
+
:entity_relationship_diagram => "Dige::GraphvizDiagram::EntityRelationshipDiagram",
|
7
|
+
:uml_class_diagram => "Dige::GraphvizDiagram::UMLClassDiagram"
|
8
|
+
}
|
9
|
+
|
10
|
+
PROJECT_TYPES = {
|
11
|
+
:rails2 => "Rails2",
|
12
|
+
:rails3 => "Rails3",
|
13
|
+
:data_mapper => "DataMapper"
|
14
|
+
}
|
15
|
+
|
16
|
+
DIAGRAM_TYPES.each do |diagram_type, diagram_klass|
|
17
|
+
class_eval("
|
18
|
+
def self.generate_#{diagram_type}(project_type = :unknown) ; generate_diagram(#{diagram_klass}, project_type) ; end
|
19
|
+
")
|
20
|
+
PROJECT_TYPES.each_key do |project_type|
|
21
|
+
class_eval("
|
22
|
+
def self.generate_#{project_type}_#{diagram_type} ; generate_#{diagram_type}(:#{project_type}) ; end
|
23
|
+
")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def self.induce_project_type
|
30
|
+
PROJECT_TYPES.values.collect { |v| Dige::Recognizer.const_get(v) }.inject(nil) do |recognized_type, klass|
|
31
|
+
recognized_type || klass.new.recognize
|
32
|
+
end || :unknown
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.initialize_instantiator(project_type)
|
36
|
+
project_type = induce_project_type if project_type == :unknown
|
37
|
+
instantiator = PROJECT_TYPES[project_type]
|
38
|
+
if instantiator.nil?
|
39
|
+
raise UnrecognizedProjectTypeError
|
40
|
+
else
|
41
|
+
Dige::Instantiator.const_get(instantiator).new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.generate_diagram(klass, project_type)
|
46
|
+
instantiator = initialize_instantiator(project_type)
|
47
|
+
instantiator.load_models
|
48
|
+
diagram = klass.new(instantiator.load_klasses)
|
49
|
+
puts diagram.debug_inspect.join("\n") if ENV['DEBUG'] and ENV['DEBUG'].upcase == 'TRUE'
|
50
|
+
diagram.generate
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/dige/errors.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
class UnrecognizedProjectTypeError < RuntimeError ; end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Dige::GraphvizDiagram::EntityRelationshipDiagram < Dige::GraphvizDiagram
|
2
|
+
def generate(file = ENV['FILE'], type = 'png')
|
3
|
+
generate_diagram(file + "_entity_relationship_model", type) do
|
4
|
+
assocs = associations.all.collect do |assoc|
|
5
|
+
clr = random_color
|
6
|
+
if assoc.table_name
|
7
|
+
[{ :from => assoc.table_name, :to => assoc.from_table_name }, { :from => assoc.table_name, :to => assoc.to_table_name }]
|
8
|
+
else
|
9
|
+
[{ :from => assoc.from_table_name, :to => assoc.to_table_name }]
|
10
|
+
end
|
11
|
+
end.flatten
|
12
|
+
|
13
|
+
klasses.all.collect { |klass| node(klass.table_name, :associations_count => klass.associations_count) }.uniq.sort +
|
14
|
+
associations.all.collect { |assoc| assoc.table_name ? node(assoc.table_name) : nil }.compact.uniq.sort +
|
15
|
+
assocs.uniq.collect { |ass| edge(ass[:from], ass[:to], true, { :color => random_color}) }.uniq.sort
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Dige::GraphvizDiagram::UMLClassDiagram < Dige::GraphvizDiagram
|
2
|
+
def edge_settings
|
3
|
+
super.merge(
|
4
|
+
{
|
5
|
+
:arrowhead => :none
|
6
|
+
})
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate(file = ENV['FILE'], type = 'png')
|
10
|
+
generate_diagram(file + "_uml_class_diagram", type) do
|
11
|
+
assocs = associations.all.collect do |assoc|
|
12
|
+
{ :from => assoc.from_class_name, :to => assoc.to_class_name, :from_arity => assoc.from_arity, :to_arity => assoc.to_arity }
|
13
|
+
end
|
14
|
+
|
15
|
+
pcrs = parent_child_relations.all.collect do |pcr|
|
16
|
+
{ :from => pcr.child_class_name, :to => pcr.parent_class_name}
|
17
|
+
end
|
18
|
+
|
19
|
+
klasses.all.collect { |klass| node(klass.class_name, :associations_count => klass.associations_count) }.uniq.sort +
|
20
|
+
pcrs.collect { |pcr| edge(pcr[:from], pcr[:to], true, {:color => random_color, :arrowhead => :empty}) }.uniq.sort +
|
21
|
+
assocs.collect { |ass| edge(ass[:from], ass[:to], true, { :color => random_color, :headlabel => ass[:to_arity], :taillabel => ass[:from_arity]}) }.uniq.sort
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class Dige::GraphvizDiagram
|
2
|
+
include Dige::ClassBase
|
3
|
+
attr_accessor :klasses, :parent_child_relations, :associations
|
4
|
+
|
5
|
+
DEFAULT_NODE_WIDTH = 3.0
|
6
|
+
DEFAULT_NODE_HEIGHT = 2.0
|
7
|
+
|
8
|
+
def generate_diagram(file = nil, type = 'png', graphtype = :digraph, &block)
|
9
|
+
output = [header(*([graphtype, file].compact)), settings, yield, "}"].flatten.compact.join("\n")
|
10
|
+
puts output if ENV['DEBUG'] and ENV['DEBUG'].upcase == 'TRUE'
|
11
|
+
|
12
|
+
unless file.nil?
|
13
|
+
File.open("#{file}.dot", "w") do |f| f.write output end
|
14
|
+
unless system("dot -T#{type} -o#{file}.#{type} #{file}.dot") && ((ENV['NO_RM'] && ENV['NO_RM'].upcase == 'TRUE') || FileUtils.rm_f("#{file}.dot"))
|
15
|
+
$stderr.puts "error: failed to generate png diagram from dot file #{file}.dot, file is left existing for investigation."
|
16
|
+
end
|
17
|
+
else
|
18
|
+
puts output
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def header(type, name = "my_diagram")
|
23
|
+
"#{type} #{name} {"
|
24
|
+
end
|
25
|
+
|
26
|
+
def settings
|
27
|
+
[
|
28
|
+
(!graph_settings.empty? ? "graph [" + graph_settings.collect { |k,v| "#{k}=#{quote(v)}" }.join(",") + "];" : nil),
|
29
|
+
(!edge_settings.empty? ? "edge [" + edge_settings.collect { |k,v| "#{k}=#{quote(v)}" }.join(",") + "];" : nil),
|
30
|
+
(!node_settings.empty? ? "node [" + node_settings.collect { |k,v| "#{k}=#{quote(v)}" }.join(",") + "];" : nil)
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
def graph_settings
|
35
|
+
{
|
36
|
+
:outputorder => :nodesfirst,
|
37
|
+
:concentrate => true,
|
38
|
+
:ranksep => 2.5
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def node_settings
|
43
|
+
{
|
44
|
+
:shape => :box,
|
45
|
+
:fontsize => 36.0
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def edge_settings
|
50
|
+
{
|
51
|
+
:arrowsize => 1.0,
|
52
|
+
:penwidth => 2.0
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def node(name, options={})
|
57
|
+
associations_count = options.delete(:associations_count)
|
58
|
+
options[:width] = DEFAULT_NODE_WIDTH
|
59
|
+
options[:height] = DEFAULT_NODE_HEIGHT
|
60
|
+
node_options = options.empty? ? "" : "[" + options.collect { |k,v| "#{k}=#{quote(v)}" }.join(",") + "]"
|
61
|
+
"#{quote(name)}#{node_options};"
|
62
|
+
end
|
63
|
+
|
64
|
+
def edge(from, to, directed=true, options={})
|
65
|
+
edge_options = options.empty? ? "" : "[" + options.collect { |k,v| "#{k}=#{quote(v)}" }.join(",") + "]"
|
66
|
+
"#{quote(from)} #{directed ? "->" : "--"} #{quote(to)}#{edge_options};"
|
67
|
+
end
|
68
|
+
|
69
|
+
def quote(string)
|
70
|
+
"\"#{string}\""
|
71
|
+
end
|
72
|
+
|
73
|
+
def debug_inspect
|
74
|
+
[
|
75
|
+
klasses.debug_inspect,
|
76
|
+
associations.debug_inspect,
|
77
|
+
parent_child_relations.debug_inspect
|
78
|
+
].flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
def random_color
|
82
|
+
minimum = 10
|
83
|
+
maximum = 210
|
84
|
+
r = ((rand*maximum) + minimum).to_i
|
85
|
+
g = ((rand*maximum) + minimum).to_i
|
86
|
+
b = ((rand*maximum) + minimum).to_i
|
87
|
+
|
88
|
+
"#" + r.to_s(16).rjust(2, '0') + g.to_s(16).rjust(2, '0') + b.to_s(16).rjust(2, '0')
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
require 'dige/graphviz_diagram/entity_relationship_diagram'
|
94
|
+
require 'dige/graphviz_diagram/uml_class_diagram'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Dige::Instantiator::DataMapper < Dige::Instantiator
|
2
|
+
|
3
|
+
def load_models
|
4
|
+
raise NotImplementedError.new("This method should be overridden in a subclass")
|
5
|
+
end
|
6
|
+
|
7
|
+
def load_klasses
|
8
|
+
raise NotImplementedError.new("This method should be overridden in a subclass")
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class Dige::Instantiator::Rails2 < Dige::Instantiator
|
2
|
+
|
3
|
+
def load_models
|
4
|
+
models = Dir["app/models/**/*.rb"].collect do |model| File.expand_path(model) end
|
5
|
+
retries = {}
|
6
|
+
while !models.empty? do
|
7
|
+
model = models.shift
|
8
|
+
begin
|
9
|
+
require model
|
10
|
+
retries = {}
|
11
|
+
rescue Exception => e
|
12
|
+
retries[model] ||= 0
|
13
|
+
retries[model] += 1
|
14
|
+
break if retries[model] > 1
|
15
|
+
models.push model
|
16
|
+
end
|
17
|
+
end
|
18
|
+
$stderr.puts "warning: Failed to load models: #{retries.keys.collect { |m| m.gsub(/^#{RAILS_ROOT}\/app\/models\//, '') }.join(', ')}." unless retries.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_klasses
|
22
|
+
klasses = Dige::Klasses.new
|
23
|
+
parent_child_relations = Dige::ParentChildRelations.new
|
24
|
+
associations = Dige::Associations.new
|
25
|
+
|
26
|
+
Class.subclasses_of(ActiveRecord::Base).each do |klass|
|
27
|
+
columns = ActiveRecord::Base.connection.table_exists?(klass.table_name) ? klass.columns.collect { |column| column.name.to_s } : []
|
28
|
+
klasses.register(klass.name, klass.table_name, columns) unless Dige::IGNORE_CLASSES.include?(klass.name)
|
29
|
+
parent_child_relations.register(klass.superclass.name, klass.name) unless Dige::IGNORE_CLASSES.include?(klass.superclass.name) || Dige::IGNORE_CLASSES.include?(klass.name)
|
30
|
+
end.each do |klass|
|
31
|
+
klass.reflect_on_all_associations.each do |assoc|
|
32
|
+
type = assoc.options[:polymorphic] ? :polymorphic : :normal
|
33
|
+
|
34
|
+
next if assoc.options[:through] || Dige::IGNORE_CLASSES.include?(klass.name)
|
35
|
+
|
36
|
+
if type == :polymorphic &&
|
37
|
+
(!klasses[klass.name].columns.include?((assoc.options[:foreign_key] || "#{assoc.name.to_s}_id").to_s) ||
|
38
|
+
!klasses[klass.name].columns.include?((assoc.options[:foreign_type] || "#{assoc.name.to_s}_type").to_s))
|
39
|
+
$stderr.puts "warning: class #{klass.name} defines a polymorphic relationship #{assoc.name.to_s}, for which there are not the necessary columns in the DB."
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
assoc_class_name = if type == :polymorphic
|
44
|
+
assoc.name.to_s
|
45
|
+
elsif assoc.options[:class_name]
|
46
|
+
assoc.options[:class_name].to_s
|
47
|
+
else
|
48
|
+
if assoc.macro == :has_many || assoc.macro == :has_and_belongs_to_many
|
49
|
+
assoc.name.to_s.singularize.camelize
|
50
|
+
else
|
51
|
+
assoc.name.to_s.camelize
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
next if Dige::IGNORE_CLASSES.include?(assoc_class_name)
|
56
|
+
|
57
|
+
if klasses[assoc_class_name].nil? && type != :polymorphic
|
58
|
+
polymorphic_name = (assoc.options[:foreign_key] || assoc.options[:foreign_type] || "").to_s.gsub(/_(id|type)$/, '')
|
59
|
+
if !polymorphic_name.empty? &&
|
60
|
+
(klasses[klass.name].columns.include?("#{polymorphic_name}_id")) &&
|
61
|
+
(klasses[klass.name].columns.include?("#{polymorphic_name}_type"))
|
62
|
+
assoc_class_name = polymorphic_name
|
63
|
+
type = :polymorphic
|
64
|
+
else
|
65
|
+
$stderr.puts "warning: class #{klass.name} references non-existing class #{assoc_class_name}" unless assoc.options[:through]
|
66
|
+
next
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
assoc_table_name = klasses[assoc_class_name].table_name unless type == :polymorphic
|
71
|
+
|
72
|
+
associations.register(klass.name, klass.table_name, assoc_class_name, assoc_table_name, type, assoc.macro, assoc.options[:join_table])
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
associations.all(:polymorphic).each do |assoc|
|
78
|
+
polymorphic_klasses = ActiveRecord::Base.connection.select_values("SELECT DISTINCT #{assoc.to_class_name}_type FROM #{assoc.from_table_name};").compact
|
79
|
+
|
80
|
+
klasses.all.each do |klass|
|
81
|
+
next unless polymorphic_klasses.include?(klass.class_name)
|
82
|
+
associations.register(assoc.from_class_name, assoc.from_table_name, klass.class_name, klass.table_name, :normal, assoc.macro, assoc.table_name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
associations.all.each do |assoc|
|
87
|
+
klasses[assoc.from_class_name].associations_count += 1
|
88
|
+
klasses[assoc.to_class_name].associations_count += 1
|
89
|
+
end
|
90
|
+
|
91
|
+
{
|
92
|
+
:klasses => klasses,
|
93
|
+
:parent_child_relations => parent_child_relations,
|
94
|
+
:associations => associations
|
95
|
+
}
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class Dige::Instantiator::Rails3 < Dige::Instantiator
|
2
|
+
|
3
|
+
def load_models
|
4
|
+
models = Dir["app/models/**/*.rb"].collect do |model| File.expand_path(model) end
|
5
|
+
retries = {}
|
6
|
+
while !models.empty? do
|
7
|
+
model = models.shift
|
8
|
+
begin
|
9
|
+
require model
|
10
|
+
retries = {}
|
11
|
+
rescue Exception => e
|
12
|
+
retries[model] ||= 0
|
13
|
+
retries[model] += 1
|
14
|
+
break if retries[model] > 1
|
15
|
+
models.push model
|
16
|
+
end
|
17
|
+
end
|
18
|
+
$stderr.puts "warning: Failed to load models: #{retries.keys.collect { |m| m.gsub(/^#{RAILS_ROOT}\/app\/models\//, '') }.join(', ')}." unless retries.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_klasses
|
22
|
+
klasses = Dige::Klasses.new
|
23
|
+
parent_child_relations = Dige::ParentChildRelations.new
|
24
|
+
associations = Dige::Associations.new
|
25
|
+
|
26
|
+
ActiveRecord::Base.subclasses.each do |klass|
|
27
|
+
columns = ActiveRecord::Base.connection.table_exists?(klass.table_name) ? klass.columns.collect { |column| column.name.to_s } : []
|
28
|
+
klasses.register(klass.name, klass.table_name, columns) unless Dige::IGNORE_CLASSES.include?(klass.name)
|
29
|
+
parent_child_relations.register(klass.superclass.name, klass.name) unless Dige::IGNORE_CLASSES.include?(klass.superclass.name) || Dige::IGNORE_CLASSES.include?(klass.name)
|
30
|
+
end.each do |klass|
|
31
|
+
klass.reflect_on_all_associations.each do |assoc|
|
32
|
+
type = assoc.options[:polymorphic] ? :polymorphic : :normal
|
33
|
+
|
34
|
+
next if assoc.options[:through] || Dige::IGNORE_CLASSES.include?(klass.name)
|
35
|
+
|
36
|
+
if type == :polymorphic &&
|
37
|
+
(!klasses[klass.name].columns.include?((assoc.options[:foreign_key] || "#{assoc.name.to_s}_id").to_s) ||
|
38
|
+
!klasses[klass.name].columns.include?((assoc.options[:foreign_type] || "#{assoc.name.to_s}_type").to_s))
|
39
|
+
$stderr.puts "warning: class #{klass.name} defines a polymorphic relationship #{assoc.name.to_s}, for which there are not the necessary columns in the DB."
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
assoc_class_name = if type == :polymorphic
|
44
|
+
assoc.name.to_s
|
45
|
+
elsif assoc.options[:class_name]
|
46
|
+
assoc.options[:class_name].to_s
|
47
|
+
else
|
48
|
+
if assoc.macro == :has_many || assoc.macro == :has_and_belongs_to_many
|
49
|
+
assoc.name.to_s.singularize.camelize
|
50
|
+
else
|
51
|
+
assoc.name.to_s.camelize
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
next if Dige::IGNORE_CLASSES.include?(assoc_class_name)
|
56
|
+
|
57
|
+
if klasses[assoc_class_name].nil? && type != :polymorphic
|
58
|
+
polymorphic_name = (assoc.options[:foreign_key] || assoc.options[:foreign_type] || "").to_s.gsub(/_(id|type)$/, '')
|
59
|
+
if !polymorphic_name.empty? &&
|
60
|
+
(klasses[klass.name].columns.include?("#{polymorphic_name}_id")) &&
|
61
|
+
(klasses[klass.name].columns.include?("#{polymorphic_name}_type"))
|
62
|
+
assoc_class_name = polymorphic_name
|
63
|
+
type = :polymorphic
|
64
|
+
else
|
65
|
+
$stderr.puts "warning: class #{klass.name} references non-existing class #{assoc_class_name}" unless assoc.options[:through]
|
66
|
+
next
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
assoc_table_name = klasses[assoc_class_name].table_name unless type == :polymorphic
|
71
|
+
|
72
|
+
associations.register(klass.name, klass.table_name, assoc_class_name, assoc_table_name, type, assoc.macro, assoc.options[:join_table])
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
associations.all(:polymorphic).each do |assoc|
|
78
|
+
polymorphic_klasses = ActiveRecord::Base.connection.select_values("SELECT DISTINCT #{assoc.to_class_name}_type FROM #{assoc.from_table_name};").compact
|
79
|
+
|
80
|
+
klasses.all.each do |klass|
|
81
|
+
next unless polymorphic_klasses.include?(klass.class_name)
|
82
|
+
associations.register(assoc.from_class_name, assoc.from_table_name, klass.class_name, klass.table_name, :normal, assoc.macro, assoc.table_name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
associations.all.each do |assoc|
|
87
|
+
klasses[assoc.from_class_name].associations_count += 1
|
88
|
+
klasses[assoc.to_class_name].associations_count += 1
|
89
|
+
end
|
90
|
+
|
91
|
+
{
|
92
|
+
:klasses => klasses,
|
93
|
+
:parent_child_relations => parent_child_relations,
|
94
|
+
:associations => associations
|
95
|
+
}
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Dige::Instantiator
|
2
|
+
|
3
|
+
def load_models
|
4
|
+
raise NotImplementedError.new("This method should be overridden in a subclass")
|
5
|
+
end
|
6
|
+
|
7
|
+
def load_klasses
|
8
|
+
raise NotImplementedError.new("This method should be overridden in a subclass")
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'dige/instantiator/data_mapper'
|
14
|
+
require 'dige/instantiator/rails2'
|
15
|
+
require 'dige/instantiator/rails3'
|
data/lib/dige/klass.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class Dige::Klass
|
2
|
+
include Dige::ClassBase
|
3
|
+
attr_accessor :class_name, :table_name, :columns, :associations_count
|
4
|
+
|
5
|
+
def debug_inspect
|
6
|
+
"#{class_name}(#{table_name})[ASSOCIATIONS_COUNT: #{associations_count}, COLUMNS: #{columns.join(",")}]"
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(objekt)
|
10
|
+
self.class_name == objekt.class_name &&
|
11
|
+
self.table_name == objekt.table_name
|
12
|
+
end
|
13
|
+
end
|
data/lib/dige/klasses.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Dige::Klasses
|
2
|
+
include Dige::List
|
3
|
+
|
4
|
+
def [](name)
|
5
|
+
@list.detect { |list_item| list_item.send("#{name.to_s =~ /^[A-Z]/ ? "class_name" : "table_name"}".to_sym).to_s == name.to_s }
|
6
|
+
end
|
7
|
+
|
8
|
+
def register(class_name, table_name, columns)
|
9
|
+
objekt = Dige::Klass.new({ :class_name => class_name, :table_name => table_name, :columns => columns, :associations_count => 0 })
|
10
|
+
unless return_objekt = find(objekt)
|
11
|
+
list << objekt
|
12
|
+
return_objekt = objekt
|
13
|
+
end
|
14
|
+
return_objekt
|
15
|
+
end
|
16
|
+
|
17
|
+
def all
|
18
|
+
list
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/lib/dige/list.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dige::List
|
2
|
+
def self.included(base)
|
3
|
+
base.send(:include, InstanceMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
module InstanceMethods
|
7
|
+
def find(objekt)
|
8
|
+
list.detect { |list_item| list_item == objekt }
|
9
|
+
end
|
10
|
+
|
11
|
+
def list
|
12
|
+
@list ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def debug_inspect
|
16
|
+
[ "\n\n#{self.class.name}:\n" ] + all.collect { |objekt| objekt.debug_inspect }.flatten.sort
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Dige::ParentChildRelation
|
2
|
+
include Dige::ClassBase
|
3
|
+
attr_accessor :parent_class_name, :child_class_name
|
4
|
+
|
5
|
+
def debug_inspect
|
6
|
+
"#{parent_class_name} < #{child_class_name}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(objekt)
|
10
|
+
self.parent_class_name == objekt.parent_class_name &&
|
11
|
+
self.child_class_name == objekt.child_class_name
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Dige::ParentChildRelations
|
2
|
+
include Dige::List
|
3
|
+
|
4
|
+
def register(parent_class_name, child_class_name)
|
5
|
+
objekt = Dige::ParentChildRelation.new({ :parent_class_name => parent_class_name, :child_class_name => child_class_name })
|
6
|
+
unless return_objekt = find(objekt)
|
7
|
+
list << objekt
|
8
|
+
return_objekt = objekt
|
9
|
+
end
|
10
|
+
return_objekt
|
11
|
+
end
|
12
|
+
|
13
|
+
def all
|
14
|
+
list
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Dige::ProjectTypeRecognizer
|
2
|
+
|
3
|
+
def recognize
|
4
|
+
raise NotImplementedError.new("This method should be overridden in a subclass")
|
5
|
+
end
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'dige/project_type_recognizer/data_mapper'
|
10
|
+
require 'dige/project_type_recognizer/rails2'
|
11
|
+
require 'dige/project_type_recognizer/rails3'
|
data/lib/dige.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'dige/dige'
|
2
|
+
require 'dige/errors'
|
3
|
+
require 'dige/class_base'
|
4
|
+
require 'dige/klass'
|
5
|
+
require 'dige/association'
|
6
|
+
require 'dige/parent_child_relation'
|
7
|
+
require 'dige/list'
|
8
|
+
require 'dige/klasses'
|
9
|
+
require 'dige/associations'
|
10
|
+
require 'dige/parent_child_relations'
|
11
|
+
require 'dige/project_type_recognizer'
|
12
|
+
require 'dige/instantiator'
|
13
|
+
require 'dige/graphviz_diagram'
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: le1t0-dige
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.9.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Le1t0
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-03-03 00:00:00 +01:00
|
14
|
+
default_executable: dige
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: " This project provides code for generating ERD and class diagrams from rails projects.\n"
|
18
|
+
email: dev@ewout.to
|
19
|
+
executables:
|
20
|
+
- dige
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- bin/dige
|
27
|
+
- lib/dige/association.rb
|
28
|
+
- lib/dige/associations.rb
|
29
|
+
- lib/dige/class_base.rb
|
30
|
+
- lib/dige/dige.rb
|
31
|
+
- lib/dige/errors.rb
|
32
|
+
- lib/dige/graphviz_diagram/entity_relationship_diagram.rb
|
33
|
+
- lib/dige/graphviz_diagram/uml_class_diagram.rb
|
34
|
+
- lib/dige/graphviz_diagram.rb
|
35
|
+
- lib/dige/instantiator/data_mapper.rb
|
36
|
+
- lib/dige/instantiator/rails2.rb
|
37
|
+
- lib/dige/instantiator/rails3.rb
|
38
|
+
- lib/dige/instantiator.rb
|
39
|
+
- lib/dige/klass.rb
|
40
|
+
- lib/dige/klasses.rb
|
41
|
+
- lib/dige/list.rb
|
42
|
+
- lib/dige/parent_child_relation.rb
|
43
|
+
- lib/dige/parent_child_relations.rb
|
44
|
+
- lib/dige/project_type_recognizer/data_mapper.rb
|
45
|
+
- lib/dige/project_type_recognizer/rails2.rb
|
46
|
+
- lib/dige/project_type_recognizer/rails3.rb
|
47
|
+
- lib/dige/project_type_recognizer.rb
|
48
|
+
- lib/dige.rb
|
49
|
+
- README
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/le1t0/dige
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.5.2
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: (rails) diagram generator
|
78
|
+
test_files: []
|
79
|
+
|