allenwei-railroad 0.7.8
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/COPYING +340 -0
- data/ChangeLog +85 -0
- data/History.txt +42 -0
- data/Manifest.txt +19 -0
- data/README.txt +175 -0
- data/Rakefile +16 -0
- data/bin/railroad +47 -0
- data/init.rb +0 -0
- data/lib/railroad.rb +12 -0
- data/lib/railroad/aasm_diagram.rb +110 -0
- data/lib/railroad/app_diagram.rb +94 -0
- data/lib/railroad/controllers_diagram.rb +98 -0
- data/lib/railroad/diagram_graph.rb +189 -0
- data/lib/railroad/models_diagram.rb +225 -0
- data/lib/railroad/options_struct.rb +193 -0
- data/lib/railroad/tasks/diagrams.rake +48 -0
- data/lib/railroad/tasks/diagrams.rb +1 -0
- metadata +91 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# RailRoad - RoR diagrams generator
|
|
2
|
+
# http://railroad.rubyforge.org
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
|
|
5
|
+
# See COPYING for more details
|
|
6
|
+
|
|
7
|
+
# RailRoad controllers diagram
|
|
8
|
+
class ControllersDiagram < AppDiagram
|
|
9
|
+
|
|
10
|
+
def initialize(options)
|
|
11
|
+
#options.exclude.map! {|e| "app/controllers/" + e}
|
|
12
|
+
super options
|
|
13
|
+
@graph.diagram_type = 'Controllers'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Process controller files
|
|
17
|
+
def generate
|
|
18
|
+
STDERR.print "Generating controllers diagram\n" if @options.verbose
|
|
19
|
+
|
|
20
|
+
files = Dir.glob("app/controllers/**/*_controller.rb") - @options.exclude
|
|
21
|
+
files.each do |f|
|
|
22
|
+
class_name = extract_class_name('app/controllers/', f)
|
|
23
|
+
process_class class_name.constantize
|
|
24
|
+
end
|
|
25
|
+
end # generate
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Load controller classes
|
|
30
|
+
def load_classes
|
|
31
|
+
begin
|
|
32
|
+
disable_stdout
|
|
33
|
+
files = Dir.glob("app/controllers/**/*_controller.rb") - @options.exclude
|
|
34
|
+
files.each {|file| get_controller_class(file) }
|
|
35
|
+
enable_stdout
|
|
36
|
+
rescue LoadError
|
|
37
|
+
enable_stdout
|
|
38
|
+
print_error "controller classes"
|
|
39
|
+
raise
|
|
40
|
+
end
|
|
41
|
+
end # load_classes
|
|
42
|
+
|
|
43
|
+
# This method is taken from the annotate models gem
|
|
44
|
+
# http://github.com/ctran/annotate_models/tree/master
|
|
45
|
+
#
|
|
46
|
+
# Retrieve the classes belonging to the controller names we're asked to process
|
|
47
|
+
# Check for namespaced controllers in subdirectories as well as controllers
|
|
48
|
+
# in subdirectories without namespacing.
|
|
49
|
+
def get_controller_class(file)
|
|
50
|
+
model = file.sub(/^.*app\/controllers\//, '').sub(/\.rb$/, '').camelize
|
|
51
|
+
parts = model.split('::')
|
|
52
|
+
begin
|
|
53
|
+
parts.inject(Object) {|klass, part| klass.const_get(part) }
|
|
54
|
+
rescue LoadError
|
|
55
|
+
Object.const_get(parts.last)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Proccess a controller class
|
|
60
|
+
def process_class(current_class)
|
|
61
|
+
|
|
62
|
+
STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
|
|
63
|
+
|
|
64
|
+
if @options.brief
|
|
65
|
+
@graph.add_node ['controller-brief', current_class.name]
|
|
66
|
+
elsif current_class.is_a? Class
|
|
67
|
+
# Collect controller's methods
|
|
68
|
+
node_attribs = {:public => [],
|
|
69
|
+
:protected => [],
|
|
70
|
+
:private => []}
|
|
71
|
+
current_class.public_instance_methods(false).sort.each { |m|
|
|
72
|
+
process_method(node_attribs[:public], m)
|
|
73
|
+
} unless @options.hide_public
|
|
74
|
+
current_class.protected_instance_methods(false).sort.each { |m|
|
|
75
|
+
process_method(node_attribs[:protected], m)
|
|
76
|
+
} unless @options.hide_protected
|
|
77
|
+
current_class.private_instance_methods(false).sort.each { |m|
|
|
78
|
+
process_method(node_attribs[:private], m)
|
|
79
|
+
} unless @options.hide_private
|
|
80
|
+
@graph.add_node ['controller', current_class.name, node_attribs]
|
|
81
|
+
elsif @options.modules && current_class.is_a?(Module)
|
|
82
|
+
@graph.add_node ['module', current_class.name]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Generate the inheritance edge (only for ApplicationControllers)
|
|
86
|
+
if @options.inheritance &&
|
|
87
|
+
(ApplicationController.subclasses.include? current_class.name)
|
|
88
|
+
@graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
|
|
89
|
+
end
|
|
90
|
+
end # process_class
|
|
91
|
+
|
|
92
|
+
# Process a method
|
|
93
|
+
def process_method(attribs, method)
|
|
94
|
+
return if @options.hide_underscore && method[0..0] == '_'
|
|
95
|
+
attribs << method
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end # class ControllersDiagram
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# RailRoad - RoR diagrams generator
|
|
2
|
+
# http://railroad.rubyforge.org
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
|
|
5
|
+
# See COPYING for more details
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# RailRoad diagram structure
|
|
9
|
+
class DiagramGraph
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@diagram_type = ''
|
|
13
|
+
@show_label = false
|
|
14
|
+
@nodes = []
|
|
15
|
+
@edges = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add_node(node)
|
|
19
|
+
@nodes << node
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_edge(edge)
|
|
23
|
+
@edges << edge
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def diagram_type= (type)
|
|
27
|
+
@diagram_type = type
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def show_label= (value)
|
|
31
|
+
@show_label = value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Generate DOT graph
|
|
36
|
+
def to_dot
|
|
37
|
+
return dot_header +
|
|
38
|
+
@nodes.map{|n| dot_node *n}.join +
|
|
39
|
+
@edges.map{|e| dot_edge *e}.join +
|
|
40
|
+
dot_footer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Generate XMI diagram (not yet implemented)
|
|
44
|
+
def to_xmi
|
|
45
|
+
STDERR.print "Sorry. XMI output not yet implemented.\n\n"
|
|
46
|
+
return ""
|
|
47
|
+
end
|
|
48
|
+
def to_yuml
|
|
49
|
+
classes={}
|
|
50
|
+
parts=[]
|
|
51
|
+
parts<<@nodes.collect{|node|
|
|
52
|
+
type=node[0]
|
|
53
|
+
name=node[1]
|
|
54
|
+
attributes=node[2]
|
|
55
|
+
case type
|
|
56
|
+
when 'model'
|
|
57
|
+
"[#{name}|#{attributes.join(';')}|#{methods.join(';')}]"
|
|
58
|
+
else
|
|
59
|
+
"[#{name}]"
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
parts<<@edges.collect{|edge|
|
|
63
|
+
type=edge[0]
|
|
64
|
+
from=edge[1]
|
|
65
|
+
to=edge[2]
|
|
66
|
+
name=edge[3]
|
|
67
|
+
from_edg=edge[4]
|
|
68
|
+
to_edg=edge[5]
|
|
69
|
+
association=case type
|
|
70
|
+
when 'is-a'
|
|
71
|
+
"^-"
|
|
72
|
+
else
|
|
73
|
+
yuml_association(from_edg,to_edg)
|
|
74
|
+
#when 'event'
|
|
75
|
+
# options += "fontsize=10"
|
|
76
|
+
end
|
|
77
|
+
"[#{from}]#{association}[#{to}]"
|
|
78
|
+
}
|
|
79
|
+
parts.join(",")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def yuml_association(from_edg,to_edg)
|
|
86
|
+
if from_edg == '0-n' && to_edg == "0-n"
|
|
87
|
+
"*-*"
|
|
88
|
+
elsif from_edg == "0-1" && to_edg == "0-n"
|
|
89
|
+
"1-*"
|
|
90
|
+
elsif from_edg == "0-1" && to_edg == "0-1"
|
|
91
|
+
"1-1"
|
|
92
|
+
else
|
|
93
|
+
raise "Can't find association from #{from_edg} to #{to_edg}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Build DOT diagram header
|
|
98
|
+
def dot_header
|
|
99
|
+
result = "digraph #{@diagram_type.downcase}_diagram {\n" +
|
|
100
|
+
"\tgraph[overlap=false, splines=true]\n"
|
|
101
|
+
result += dot_label if @show_label
|
|
102
|
+
return result
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Build DOT diagram footer
|
|
106
|
+
def dot_footer
|
|
107
|
+
return "}\n"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Build diagram label
|
|
111
|
+
def dot_label
|
|
112
|
+
return "\t_diagram_info [shape=\"plaintext\", " +
|
|
113
|
+
"label=\"#{@diagram_type} diagram\\l" +
|
|
114
|
+
"Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l" +
|
|
115
|
+
"Migration version: " +
|
|
116
|
+
"#{ActiveRecord::Migrator.current_version}\\l" +
|
|
117
|
+
"Generated by #{APP_HUMAN_NAME} #{APP_VERSION.join('.')}"+
|
|
118
|
+
"\\l\", fontsize=14]\n"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build a DOT graph node
|
|
122
|
+
def dot_node(type, name, attributes=nil)
|
|
123
|
+
case type
|
|
124
|
+
when 'model'
|
|
125
|
+
options = 'shape=Mrecord, label="{' + name + '|'
|
|
126
|
+
options += attributes.join('\l')
|
|
127
|
+
options += '\l}"'
|
|
128
|
+
when 'model-brief'
|
|
129
|
+
options = ''
|
|
130
|
+
when 'class'
|
|
131
|
+
options = 'shape=record, label="{' + name + '|}"'
|
|
132
|
+
when 'class-brief'
|
|
133
|
+
options = 'shape=box'
|
|
134
|
+
when 'controller'
|
|
135
|
+
options = 'shape=Mrecord, label="{' + name + '|'
|
|
136
|
+
public_methods = attributes[:public].join('\l')
|
|
137
|
+
protected_methods = attributes[:protected].join('\l')
|
|
138
|
+
private_methods = attributes[:private].join('\l')
|
|
139
|
+
options += public_methods + '\l|' + protected_methods + '\l|' +
|
|
140
|
+
private_methods + '\l'
|
|
141
|
+
options += '}"'
|
|
142
|
+
when 'controller-brief'
|
|
143
|
+
options = ''
|
|
144
|
+
when 'module'
|
|
145
|
+
options = 'shape=box, style=dotted, label="' + name + '"'
|
|
146
|
+
when 'aasm'
|
|
147
|
+
# Return subgraph format
|
|
148
|
+
return "subgraph cluster_#{name.downcase} {\n\tlabel = #{quote(name)}\n\t#{attributes.join("\n ")}}"
|
|
149
|
+
end # case
|
|
150
|
+
return "\t#{quote(name)} [#{options}]\n"
|
|
151
|
+
end # dot_node
|
|
152
|
+
|
|
153
|
+
@@arrow_types = {
|
|
154
|
+
'0-n' => 'crowodot',
|
|
155
|
+
'1-n' => 'crowtee',
|
|
156
|
+
'1-1' => 'teetee',
|
|
157
|
+
'0-1' => 'teeodot',
|
|
158
|
+
'' => 'dot'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Build a DOT graph edge
|
|
162
|
+
def dot_edge(type, from, to, name = '', from_type = '', to_type = '')
|
|
163
|
+
options = (name.blank? ? "" : "label=#{name}, ")
|
|
164
|
+
case type
|
|
165
|
+
when 'n-n'
|
|
166
|
+
options += "arrowtail=#{@@arrow_types[from_type]}, arrowhead=#{@@arrow_types[to_type]}, dir=both"
|
|
167
|
+
when 'one-one'
|
|
168
|
+
#options += 'taillabel="1"'
|
|
169
|
+
options += 'arrowtail=odot, arrowhead=dot, dir=both'
|
|
170
|
+
when 'one-many'
|
|
171
|
+
#options += 'taillabel="n"'
|
|
172
|
+
options += 'arrowtail=crow, arrowhead=dot, dir=both'
|
|
173
|
+
when 'many-many'
|
|
174
|
+
#options += 'taillabel="n", headlabel="n", arrowtail="normal"'
|
|
175
|
+
options += 'arrowtail=crow, arrowhead=crow, dir=both'
|
|
176
|
+
when 'is-a'
|
|
177
|
+
options += 'arrowhead="none", arrowtail="onormal"'
|
|
178
|
+
when 'event'
|
|
179
|
+
options += "fontsize=10"
|
|
180
|
+
end
|
|
181
|
+
return "\t#{quote(from)} -> #{quote(to)} [#{options}]\n"
|
|
182
|
+
end # dot_edge
|
|
183
|
+
|
|
184
|
+
# Quotes a class name
|
|
185
|
+
def quote(name)
|
|
186
|
+
'"' + name.to_s + '"'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
end # class DiagramGraph
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# RailRoad - RoR diagrams generator
|
|
2
|
+
# http://railroad.rubyforge.org
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
|
|
5
|
+
# See COPYING for more details
|
|
6
|
+
|
|
7
|
+
# RailRoad models diagram
|
|
8
|
+
class ModelsDiagram < AppDiagram
|
|
9
|
+
|
|
10
|
+
def initialize(options)
|
|
11
|
+
#options.exclude.map! {|e| "app/models/" + e}
|
|
12
|
+
super options
|
|
13
|
+
@graph.diagram_type = 'Models'
|
|
14
|
+
# Processed habtm associations
|
|
15
|
+
@habtm = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Process model files
|
|
19
|
+
def generate
|
|
20
|
+
STDERR.print "Generating models diagram\n" if @options.verbose
|
|
21
|
+
base = "(app/models/|lib/)"
|
|
22
|
+
files = Dir.glob("app/models/**/*.rb")
|
|
23
|
+
files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
|
|
24
|
+
files += Dir.glob("lib/**/*.rb") if @options.libraries
|
|
25
|
+
|
|
26
|
+
files -= @options.exclude
|
|
27
|
+
(files + ['filter_condition_type']).each do |file|
|
|
28
|
+
model_name =
|
|
29
|
+
@options.classes_by_files[file] ||
|
|
30
|
+
(model_path = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\2')).camelize
|
|
31
|
+
STDERR.print "Processing #{file} ...\n" if @options.verbose
|
|
32
|
+
# Hack to skip all xxx_related.rb files
|
|
33
|
+
next if /_related/i =~ model_name
|
|
34
|
+
|
|
35
|
+
klass = begin
|
|
36
|
+
model_name.constantize
|
|
37
|
+
rescue LoadError
|
|
38
|
+
STDERR.print "\t#{model_name} raised LoadError.\n" if @options.verbose
|
|
39
|
+
oldlen = model_path.length
|
|
40
|
+
model_path.gsub!(/.*[\/\\]/, '')
|
|
41
|
+
model_name = model_path.camelize
|
|
42
|
+
if oldlen > model_path.length
|
|
43
|
+
retry
|
|
44
|
+
end
|
|
45
|
+
STDERR.print "\tDone trying to remove slashes, skipping this model.\n" if @options.verbose
|
|
46
|
+
next
|
|
47
|
+
rescue NameError
|
|
48
|
+
STDERR.print "\t#{model_name} raised NameError, skipping this model.\n" if @options.verbose
|
|
49
|
+
next
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
process_class klass
|
|
53
|
+
STDERR.print "Done #{file}\n" if @options.verbose
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Load model classes
|
|
60
|
+
def load_classes
|
|
61
|
+
begin
|
|
62
|
+
disable_stdout
|
|
63
|
+
files = Dir.glob("app/models/**/*.rb")
|
|
64
|
+
files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
|
|
65
|
+
files -= @options.exclude
|
|
66
|
+
require 'ruby-debug';debugger
|
|
67
|
+
@models = files.map{|file| get_model_class(file) }
|
|
68
|
+
enable_stdout
|
|
69
|
+
@models
|
|
70
|
+
rescue LoadError
|
|
71
|
+
enable_stdout
|
|
72
|
+
print_error "model classes"
|
|
73
|
+
raise
|
|
74
|
+
end
|
|
75
|
+
end # load_classes
|
|
76
|
+
|
|
77
|
+
# This method is taken from the annotate models gem
|
|
78
|
+
# http://github.com/ctran/annotate_models/tree/master
|
|
79
|
+
#
|
|
80
|
+
# Retrieve the classes belonging to the model names we're asked to process
|
|
81
|
+
# Check for namespaced models in subdirectories as well as models
|
|
82
|
+
# in subdirectories without namespacing.
|
|
83
|
+
def get_model_class(file)
|
|
84
|
+
begin
|
|
85
|
+
model = file.sub(/^.*app\/models\//, '').sub(/\.rb$/, '').camelize
|
|
86
|
+
rescue
|
|
87
|
+
STDERR.print "Can't find class #{model}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Process a model class
|
|
92
|
+
def process_class(current_class)
|
|
93
|
+
|
|
94
|
+
STDERR.print "\tProcessing #{current_class} ...\n" if @options.verbose
|
|
95
|
+
|
|
96
|
+
generated = false
|
|
97
|
+
|
|
98
|
+
# Is current_clas derived from ActiveRecord::Base?
|
|
99
|
+
if current_class.respond_to?'reflect_on_all_associations'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
node_attribs = []
|
|
103
|
+
if @options.brief || current_class.abstract_class? || current_class.superclass != ActiveRecord::Base
|
|
104
|
+
node_type = 'model-brief'
|
|
105
|
+
else
|
|
106
|
+
node_type = 'model'
|
|
107
|
+
|
|
108
|
+
# Collect model's content columns
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
content_columns = current_class.content_columns
|
|
112
|
+
rescue
|
|
113
|
+
STDERR.print "Table #{current_class.table_name} not exist"
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if @options.hide_magic
|
|
118
|
+
magic_fields = [
|
|
119
|
+
# Restful Authentication
|
|
120
|
+
"login", "crypted_password", "salt", "remember_token", "remember_token_expires_at", "activation_code", "activated_at",
|
|
121
|
+
# From patch #13351
|
|
122
|
+
# http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
|
|
123
|
+
"created_at", "created_on", "updated_at", "updated_on",
|
|
124
|
+
"lock_version", "type", "id", "position", "parent_id", "lft",
|
|
125
|
+
"rgt", "quote", "template"
|
|
126
|
+
]
|
|
127
|
+
magic_fields << current_class.table_name + "_count" if current_class.respond_to? 'table_name'
|
|
128
|
+
content_columns = current_class.content_columns.select {|c| ! magic_fields.include? c.name}
|
|
129
|
+
else
|
|
130
|
+
content_columns = current_class.content_columns
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
content_columns.each do |a|
|
|
134
|
+
content_column = a.name
|
|
135
|
+
content_column += ' :' + a.type.to_s unless @options.hide_types
|
|
136
|
+
node_attribs << content_column
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
@graph.add_node [node_type, current_class.name, node_attribs]
|
|
140
|
+
generated = true
|
|
141
|
+
# Process class associations
|
|
142
|
+
associations = current_class.reflect_on_all_associations
|
|
143
|
+
if @options.inheritance && ! @options.transitive
|
|
144
|
+
superclass_associations = current_class.superclass.reflect_on_all_associations
|
|
145
|
+
|
|
146
|
+
associations = associations.select{|a| ! superclass_associations.include? a}
|
|
147
|
+
# This doesn't works!
|
|
148
|
+
# associations -= current_class.superclass.reflect_on_all_associations
|
|
149
|
+
end
|
|
150
|
+
associations.each do |a|
|
|
151
|
+
begin
|
|
152
|
+
process_association current_class, a
|
|
153
|
+
rescue
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
elsif @options.all && (current_class.is_a? Class)
|
|
157
|
+
# Not ActiveRecord::Base model
|
|
158
|
+
node_type = @options.brief ? 'class-brief' : 'class'
|
|
159
|
+
@graph.add_node [node_type, current_class.name]
|
|
160
|
+
generated = true
|
|
161
|
+
elsif @options.modules && (current_class.is_a? Module)
|
|
162
|
+
@graph.add_node ['module', current_class.name]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Only consider meaningful inheritance relations for generated classes
|
|
166
|
+
if @options.inheritance && generated &&
|
|
167
|
+
(current_class.superclass != ActiveRecord::Base) &&
|
|
168
|
+
(current_class.superclass != Object)
|
|
169
|
+
@graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
STDERR.print "\tDone #{current_class}\n" if @options.verbose
|
|
173
|
+
end # process_class
|
|
174
|
+
|
|
175
|
+
def determine_minimum_association(klass, column)
|
|
176
|
+
if klass.respond_to?(:reflect_on_validations_for)
|
|
177
|
+
return 1 if klass.reflect_on_validations_for(column).detect { |r|
|
|
178
|
+
[:validates_presence_of, :validates_existence_of, :validates_length_of, :validates_associated].include?(r.macro)
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
return 0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Process a model association
|
|
185
|
+
def process_association(klass, assoc)
|
|
186
|
+
begin
|
|
187
|
+
STDERR.print "\t\tProcessing model association #{assoc.name.to_s} ..." if @options.verbose
|
|
188
|
+
|
|
189
|
+
# Skip "belongs_to" associations
|
|
190
|
+
return if assoc.macro.to_s == 'belongs_to'
|
|
191
|
+
|
|
192
|
+
# Only non standard association names needs a label
|
|
193
|
+
|
|
194
|
+
# from patch #12384
|
|
195
|
+
# if assoc.class_name == assoc.name.to_s.singularize.camelize
|
|
196
|
+
assoc_class_name = (assoc.class_name.respond_to? 'underscore') ? assoc.class_name.underscore.singularize.camelize : assoc.class_name
|
|
197
|
+
if assoc_class_name == assoc.name.to_s.singularize.camelize
|
|
198
|
+
assoc_name = ''
|
|
199
|
+
else
|
|
200
|
+
assoc_name = assoc.name.to_s
|
|
201
|
+
end
|
|
202
|
+
return unless @models.include? assoc_class_name
|
|
203
|
+
# STDERR.print "#{assoc_name}\n"
|
|
204
|
+
if assoc.macro.to_s == 'has_one'
|
|
205
|
+
from_max='1'
|
|
206
|
+
to_max='1'
|
|
207
|
+
elsif assoc.macro.to_s == 'has_many' && (! assoc.options[:through])
|
|
208
|
+
from_max='1'
|
|
209
|
+
to_max='n'
|
|
210
|
+
else # habtm or has_many, :through
|
|
211
|
+
return if @habtm.include? [assoc.class_name, klass.name, assoc_name]
|
|
212
|
+
from_max='n'
|
|
213
|
+
to_max='n'
|
|
214
|
+
@habtm << [klass.name, assoc.class_name, assoc_name]
|
|
215
|
+
end
|
|
216
|
+
to_min = determine_minimum_association(klass, assoc.name)
|
|
217
|
+
# have to guess at name for association in other direction
|
|
218
|
+
from_min = determine_minimum_association(assoc.klass, assoc.primary_key_name.to_s.gsub(/_id$/, ""))
|
|
219
|
+
@graph.add_edge ["n-n", klass.name, assoc_class_name, assoc_name, "#{from_min}-#{from_max}", "#{to_min}-#{to_max}"]
|
|
220
|
+
ensure
|
|
221
|
+
STDERR.print " done.\n" if @options.verbose
|
|
222
|
+
end
|
|
223
|
+
end # process_association
|
|
224
|
+
|
|
225
|
+
end # class ModelsDiagram
|