model_graph 0.1.1
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 +0 -0
- data/README +78 -0
- data/Rakefile +101 -0
- data/bin/model_graph +483 -0
- data/doc/classes/ModelGraph/Graph.html +313 -0
- data/doc/classes/ModelGraph/Graph.src/M000003.html +20 -0
- data/doc/classes/ModelGraph/Graph.src/M000004.html +18 -0
- data/doc/classes/ModelGraph/Graph.src/M000005.html +20 -0
- data/doc/classes/ModelGraph/Graph.src/M000006.html +35 -0
- data/doc/classes/ModelGraph/Graph.src/M000007.html +31 -0
- data/doc/classes/ModelGraph.html +341 -0
- data/doc/classes/ModelGraph.src/M000001.html +18 -0
- data/doc/classes/ModelGraph.src/M000002.html +120 -0
- data/doc/created.rid +1 -0
- data/doc/dot/f_0.dot +39 -0
- data/doc/dot/f_0.png +0 -0
- data/doc/dot/m_0_0.dot +39 -0
- data/doc/dot/m_0_0.png +0 -0
- data/doc/files/model_graph_rb.html +224 -0
- data/doc/fr_class_index.html +28 -0
- data/doc/fr_file_index.html +27 -0
- data/doc/fr_method_index.html +33 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/examples/badblog.rb +17 -0
- data/examples/blog.rb +35 -0
- data/examples/goodblog.rb +17 -0
- data/examples/hello.rb +6 -0
- data/examples/magazine.rb +36 -0
- data/examples/radiant.rb +41 -0
- data/lib/model_graph/version.rb +9 -0
- data/lib/model_graph.rb +1 -0
- data/test/model_graph_test.rb +13 -0
- data/test/test_helper.rb +5 -0
- metadata +97 -0
data/CHANGELOG
ADDED
File without changes
|
data/README
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
README for model_graph
|
2
|
+
======================
|
3
|
+
|
4
|
+
When run from the trunk of a Rails project, produces
|
5
|
+
{DOT}[http://www.graphviz.org/doc/info/lang.html] output which can be rendered
|
6
|
+
into a graph by programs such as dot and neato and viewed with Graphviz (an
|
7
|
+
{Open Source}[http://www.graphviz.org/License.php] viewer). I use the
|
8
|
+
{Mac OS X version}[http://www.pixelglow.com/graphviz/], but there's a
|
9
|
+
{Darwinports}[http://darwinports.opendarwin.org/darwinports/dports/graphics/graphviz/Portfile]
|
10
|
+
(aka, Macports) version, too. Or get the
|
11
|
+
source[http://www.graphviz.org/Download.php] and build it yourself. You can
|
12
|
+
also import a DOT file with OmniGraffle, but it doesn't support all the edge
|
13
|
+
decorations that I'm using.
|
14
|
+
|
15
|
+
DOT format:: http://www.graphviz.org/doc/info/lang.html
|
16
|
+
Graphviz license:: http://www.graphviz.org/License.php
|
17
|
+
Mac OS X Graphviz:: http://www.pixelglow.com/graphviz/
|
18
|
+
Darwinports Graphviz:: http://darwinports.opendarwin.org/darwinports/dports/graphics/graphviz/Portfile
|
19
|
+
Graphviz source code:: http://www.graphviz.org/Download.php
|
20
|
+
|
21
|
+
This is *certainly* a work-in-progress.
|
22
|
+
|
23
|
+
=== Usage:
|
24
|
+
|
25
|
+
rake model_graph
|
26
|
+
|
27
|
+
then open tmp/model_graph.dot with a viewer. Using 'model_graph.rb
|
28
|
+
DEBUG=on' will write a bunch of the raw information obtained from reflecting
|
29
|
+
on the ActiveRecord model classes into the output as comments (including
|
30
|
+
some things that don't actually affect the final graph).
|
31
|
+
|
32
|
+
See the documentation for ModelGraph#do_graph for some additional options.
|
33
|
+
|
34
|
+
=== Bugs:
|
35
|
+
|
36
|
+
The ordering within DOT is based on the tail-to-head relationship of edges,
|
37
|
+
but these are somewhat arbitrarily determined by the current reflection on
|
38
|
+
ActiveRecord associations. The use of the EDGES= and NODES= options is only
|
39
|
+
a partial fix.
|
40
|
+
|
41
|
+
=== TODO:
|
42
|
+
|
43
|
+
* deal with :as in a better way (now made dashed)
|
44
|
+
* deal with :polymorphic better (now make bold and blue)
|
45
|
+
* handle indirect descendants of ActiveRecord::Base? (at least make it
|
46
|
+
clearer how they're filtered out of the graph)
|
47
|
+
* models that have no (outbound) associations are depicted in red, but
|
48
|
+
sometimes these are just confused by an overridden class (even with :as)
|
49
|
+
|
50
|
+
==== Credits:
|
51
|
+
Inspired by: Matt Biddulph at August 2, 2006 02:57 PM
|
52
|
+
URL: http://www.hackdiary.com/archives/000093.html
|
53
|
+
|
54
|
+
----
|
55
|
+
|
56
|
+
This is released under the MIT License. Please send comments or
|
57
|
+
enhanncement ideas to Rob[at]AgileConsultingLLC[dot]com or
|
58
|
+
Rob_Biedenharn[at]alum[dot]mit[dot]edu
|
59
|
+
|
60
|
+
Copyright (c) 2006 Rob Biedenharn
|
61
|
+
|
62
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
63
|
+
of this software and associated documentation files (the "Software"), to
|
64
|
+
deal in the Software without restriction, including without limitation the
|
65
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
66
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
67
|
+
furnished to do so, subject to the following conditions:
|
68
|
+
|
69
|
+
The above copyright notice and this permission notice shall be included in
|
70
|
+
all copies or substantial portions of the Software.
|
71
|
+
|
72
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
73
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
74
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
75
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
76
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
77
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
78
|
+
IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'fileutils'
|
10
|
+
include FileUtils
|
11
|
+
require File.join(File.dirname(__FILE__), 'lib', 'model_graph', 'version')
|
12
|
+
|
13
|
+
AUTHOR = "Rob Biedenharn"
|
14
|
+
EMAIL = "Rob_Biedenharn@alum.MIT.edu"
|
15
|
+
DESCRIPTION = <<eos
|
16
|
+
When run from the trunk of a Rails project, produces
|
17
|
+
# {DOT}[http://www.graphviz.org/doc/info/lang.html] output which can be
|
18
|
+
# rendered into a graph by programs such as dot and neato and viewed with
|
19
|
+
# Graphviz (an {Open Source}[http://www.graphviz.org/License.php] viewer).
|
20
|
+
eos
|
21
|
+
RUBYFORGE_PROJECT = "model-graph"
|
22
|
+
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
23
|
+
BIN_FILES = %w( model_graph )
|
24
|
+
RELEASE_TYPES = %w( gem ) # can use: gem, tar, zip
|
25
|
+
|
26
|
+
|
27
|
+
NAME = "model_graph"
|
28
|
+
REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
29
|
+
VERS = ENV['VERSION'] || (ModelGraph::VERSION::STRING + (REV ? ".#{REV}" : ""))
|
30
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
31
|
+
RDOC_OPTS = ['--quiet', '--title', "model_graph documentation",
|
32
|
+
"--opname", "index.html",
|
33
|
+
"--line-numbers",
|
34
|
+
"--main", "README",
|
35
|
+
"--inline-source"]
|
36
|
+
|
37
|
+
desc "Packages up model_graph gem."
|
38
|
+
task :default => [:test]
|
39
|
+
task :package => [:clean]
|
40
|
+
|
41
|
+
Rake::TestTask.new("test") { |t|
|
42
|
+
t.libs << "test"
|
43
|
+
t.pattern = "test/**/*_test.rb"
|
44
|
+
t.verbose = true
|
45
|
+
}
|
46
|
+
|
47
|
+
spec =
|
48
|
+
Gem::Specification.new do |s|
|
49
|
+
s.name = NAME
|
50
|
+
s.version = VERS
|
51
|
+
s.platform = Gem::Platform::RUBY
|
52
|
+
s.has_rdoc = true
|
53
|
+
s.extra_rdoc_files = ["README", "CHANGELOG"]
|
54
|
+
s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
|
55
|
+
s.summary = DESCRIPTION
|
56
|
+
s.description = DESCRIPTION
|
57
|
+
s.author = AUTHOR
|
58
|
+
s.email = EMAIL
|
59
|
+
s.homepage = HOMEPATH
|
60
|
+
s.executables = BIN_FILES
|
61
|
+
s.rubyforge_project = RUBYFORGE_PROJECT
|
62
|
+
s.bindir = "bin"
|
63
|
+
s.require_path = "lib"
|
64
|
+
s.autorequire = "model_graph"
|
65
|
+
|
66
|
+
#s.add_dependency('activesupport', '>=1.3.1')
|
67
|
+
#s.required_ruby_version = '>= 1.8.2'
|
68
|
+
|
69
|
+
s.files = %w(README CHANGELOG Rakefile) +
|
70
|
+
Dir.glob("{bin,doc,test,lib,templates,generator,extras,website,script}/**/*") +
|
71
|
+
Dir.glob("ext/**/*.{h,c,rb}") +
|
72
|
+
Dir.glob("examples/**/*.rb") +
|
73
|
+
Dir.glob("tools/*.rb")
|
74
|
+
|
75
|
+
# s.extensions = FileList["ext/**/extconf.rb"].to_a
|
76
|
+
end
|
77
|
+
|
78
|
+
Rake::GemPackageTask.new(spec) do |p|
|
79
|
+
p.need_tar = RELEASE_TYPES.include? 'tar'
|
80
|
+
p.need_zip = RELEASE_TYPES.include? 'zip'
|
81
|
+
p.gem_spec = spec
|
82
|
+
end
|
83
|
+
|
84
|
+
task :install => [ :package ] do
|
85
|
+
name = "#{NAME}-#{VERS}.gem"
|
86
|
+
sh %{sudo gem install pkg/#{name}}
|
87
|
+
end
|
88
|
+
|
89
|
+
task :uninstall => [:clean] do
|
90
|
+
sh %{sudo gem uninstall #{NAME}}
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "Publish the release files to RubyForge."
|
94
|
+
task :release => [ :package ] do
|
95
|
+
system('rubyforge login')
|
96
|
+
for ext in RELEASE_TYPES
|
97
|
+
release_command = "rubyforge add_release #{RUBYFORGE_PROJECT} #{NAME} 'REL #{VERS}' pkg/#{NAME}-#{VERS}.#{ext}"
|
98
|
+
puts release_command
|
99
|
+
system(release_command)
|
100
|
+
end
|
101
|
+
end
|
data/bin/model_graph
ADDED
@@ -0,0 +1,483 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
|
3
|
+
# When run from the trunk of a Rails project, produces
|
4
|
+
# {DOT}[http://www.graphviz.org/doc/info/lang.html] output which can be
|
5
|
+
# rendered into a graph by programs such as dot and neato and viewed with
|
6
|
+
# Graphviz (an {Open Source}[http://www.graphviz.org/License.php] viewer). I
|
7
|
+
# use the {Mac OS X version}[http://www.pixelglow.com/graphviz/], but there's
|
8
|
+
# a
|
9
|
+
# {Darwinports}[http://darwinports.opendarwin.org/darwinports/dports/graphics/graphviz/Portfile]
|
10
|
+
# (aka, Macports) version, too. Or get the
|
11
|
+
# source[http://www.graphviz.org/Download.php] and build it yourself. You can
|
12
|
+
# also import a DOT file with OmniGraffle, but it doesn't support all the edge
|
13
|
+
# decorations that I'm using.
|
14
|
+
#
|
15
|
+
# DOT format:: http://www.graphviz.org/doc/info/lang.html
|
16
|
+
# Graphviz license:: http://www.graphviz.org/License.php
|
17
|
+
# Mac OS X Graphviz:: http://www.pixelglow.com/graphviz/
|
18
|
+
# Darwinports Graphviz:: http://darwinports.opendarwin.org/darwinports/dports/graphics/graphviz/Portfile
|
19
|
+
# Graphviz source code:: http://www.graphviz.org/Download.php
|
20
|
+
#
|
21
|
+
# This is *certainly* a work-in-progress.
|
22
|
+
#
|
23
|
+
# === Usage:
|
24
|
+
#
|
25
|
+
# model_graph.rb [options]
|
26
|
+
#
|
27
|
+
# then open tmp/model_graph.dot with a viewer. Using 'model_graph.rb
|
28
|
+
# --debug' will write a bunch of the raw information obtained from reflecting
|
29
|
+
# on the ActiveRecord model classes into the output as comments (including
|
30
|
+
# some things that don't actually affect the final graph).
|
31
|
+
#
|
32
|
+
# See the documentation for ModelGraph#do_graph for some additional options.
|
33
|
+
#
|
34
|
+
# === Bugs:
|
35
|
+
#
|
36
|
+
# The ordering within DOT is based on the tail-to-head relationship of edges,
|
37
|
+
# but these are somewhat arbitrarily determined by the current reflection on
|
38
|
+
# ActiveRecord associations. The use of the
|
39
|
+
# <tt>--edges=<var>[list]</var></tt> and <tt>--nodes=<var>[list]</var></tt>
|
40
|
+
# options is only a partial fix.
|
41
|
+
#
|
42
|
+
# === TODO:
|
43
|
+
# * deal with :as in a better way (now made dashed)
|
44
|
+
# * deal with :polymorphic better (now make bold and blue)
|
45
|
+
# * handle indirect descendants of ActiveRecord::Base? (at least make it
|
46
|
+
# clearer how they're filtered out of the graph)
|
47
|
+
# * models that have no (outbound) associations are depicted in red, but
|
48
|
+
# sometimes these are just confused by an overridden class (even with :as)
|
49
|
+
#
|
50
|
+
# ==== Credits:
|
51
|
+
# Inspired by: Matt Biddulph at August 2, 2006 02:57 PM
|
52
|
+
# URL: http://www.hackdiary.com/archives/000093.html
|
53
|
+
#
|
54
|
+
# ----
|
55
|
+
#
|
56
|
+
# This is released under the MIT License. Please send comments or
|
57
|
+
# enhanncement ideas to Rob[at]AgileConsultingLLC[dot]com or
|
58
|
+
# Rob_Biedenharn[at]alum[dot]mit[dot]edu
|
59
|
+
#
|
60
|
+
# Copyright (c) 2006 Rob Biedenharn
|
61
|
+
#
|
62
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
63
|
+
# of this software and associated documentation files (the "Software"), to
|
64
|
+
# deal in the Software without restriction, including without limitation the
|
65
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
66
|
+
# sell copies of the Software, and to permit persons to whom the Software is
|
67
|
+
# furnished to do so, subject to the following conditions:
|
68
|
+
#
|
69
|
+
# The above copyright notice and this permission notice shall be included in
|
70
|
+
# all copies or substantial portions of the Software.
|
71
|
+
#
|
72
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
73
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
74
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
75
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
76
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
77
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
78
|
+
# IN THE SOFTWARE.
|
79
|
+
|
80
|
+
require 'config/environment'
|
81
|
+
|
82
|
+
require 'optparse'
|
83
|
+
require 'ostruct'
|
84
|
+
|
85
|
+
class Hash # :nodoc:
|
86
|
+
def inspect(options={})
|
87
|
+
out = ''
|
88
|
+
sep = '['
|
89
|
+
self.each { |k,v| unless ! options[:label] && k =~ /(?:head|tail)label/; out << sep << "#{k}=#{v}"; sep=', '; end }
|
90
|
+
out << ']' unless sep == '['
|
91
|
+
out
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class OpenStruct # :nodoc:
|
96
|
+
def to_h
|
97
|
+
@table
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module ModelGraph
|
102
|
+
|
103
|
+
# Should :belongs_to differ when paired with :has_one versus :has_many?
|
104
|
+
#
|
105
|
+
# Should :has_one be 'teetee' if required? (i.e., not null)
|
106
|
+
|
107
|
+
ARROW_FOR = {
|
108
|
+
:belongs_to => 'tee',
|
109
|
+
:has_many => 'crowodot',
|
110
|
+
:has_one => 'odottee',
|
111
|
+
:has_and_belongs_to_many => 'crowodot'
|
112
|
+
}
|
113
|
+
|
114
|
+
# An internal class to collect abstract nodes and edges and deliver them
|
115
|
+
# back when needed.
|
116
|
+
class Graph
|
117
|
+
attr_reader :name
|
118
|
+
|
119
|
+
# Holds information about nodes and edges that should be depicted on the
|
120
|
+
# UML-ish graph of the ActiveRecord model classes. The +name+ is optional
|
121
|
+
# and only serves to give the graph an internal name. If you had an
|
122
|
+
# application to combine model graphs from multiple applications, this
|
123
|
+
# might be useful.
|
124
|
+
def initialize(name="model_graph")
|
125
|
+
@name = name
|
126
|
+
@nodes = Hash.new # holds simple strings
|
127
|
+
@edges = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = Hash.new } }
|
128
|
+
|
129
|
+
@polymorphs = Hash.new { |h,k| h[k] = Array.new }
|
130
|
+
|
131
|
+
# OH, just write some tests for this!
|
132
|
+
|
133
|
+
# A hm B :as => Y gives edge A->B and remembers polymorph Y->A
|
134
|
+
# C hm B :as => Y gives edge C->B and remembers polymorph Y->C
|
135
|
+
# B bt Y :polymorphic => true promises B->x for x in Y
|
136
|
+
|
137
|
+
@unresolved_edges = false
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create an unattached node in this graph.
|
141
|
+
def add_node(nodename, options="")
|
142
|
+
@nodes[nodename] = options
|
143
|
+
end
|
144
|
+
|
145
|
+
# Iterates over all the nodes previously added to this graph.
|
146
|
+
def nodes # :yields: nodestring
|
147
|
+
@nodes.each do |name,options|
|
148
|
+
yield "#{name} #{options}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Create a directed edge from one node to another. If an edge between
|
153
|
+
# nodes already exists in the opposite direction, the arrow will be
|
154
|
+
# attached to the other end of the existing edge.
|
155
|
+
def add_edge(fromnode, tonode, options={})
|
156
|
+
unless @edges[tonode].has_key? fromnode
|
157
|
+
options.each do |k,v|
|
158
|
+
@edges[fromnode][tonode][case k.to_s
|
159
|
+
when 'label' : 'taillabel'
|
160
|
+
when 'midlabel' : 'label'
|
161
|
+
when /^arrow(?:head|tail)?$/ : 'arrowhead'
|
162
|
+
else k
|
163
|
+
end] = v
|
164
|
+
end
|
165
|
+
else
|
166
|
+
# reverse sense and overload existing edge
|
167
|
+
options.each do |k,v|
|
168
|
+
@edges[tonode][fromnode][case k.to_s
|
169
|
+
when 'label' : 'headlabel'
|
170
|
+
when 'midlabel' : 'label'
|
171
|
+
when /^arrow(?:head|tail)?$/ : 'arrowtail'
|
172
|
+
else k
|
173
|
+
end] = v
|
174
|
+
end
|
175
|
+
end
|
176
|
+
if options.has_key?('midlabel')
|
177
|
+
add_polymorph(options['midlabel'], fromnode, options) # i.e., fill out edge later
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Iterates over all the DOT formatted edges with nodes having the most
|
182
|
+
# edges first and the edges without a constraint attribute before those
|
183
|
+
# that do.
|
184
|
+
def edges(options={}) # :yields: edgestring
|
185
|
+
self.resolve_edges if @unresolved_edges
|
186
|
+
@edges.sort { |a,b| b[1].length <=> a[1].length }.each do |(fromnode,nh)|
|
187
|
+
nh.sort_by { |(t,a)| (a.has_key?('constraint') ^ options[:constraints_first]) ? 1 : 0 }.each do |tonode,eh|
|
188
|
+
# if @polymorphs.has_key?(tonode) ... then loop over them
|
189
|
+
e = "#{fromnode} -> #{tonode} "
|
190
|
+
e << eh.inspect(options) unless eh.nil?
|
191
|
+
yield e
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def add_polymorph(astype, fromnode, options={})
|
197
|
+
@polymorphs[astype] << fromnode
|
198
|
+
@unresolved_edges = true
|
199
|
+
end
|
200
|
+
|
201
|
+
def resolve_edges
|
202
|
+
puts "=> resolve_edges"
|
203
|
+
@polymorphs.each_pair do |fromnode, nodes|
|
204
|
+
puts " fromnode: #{fromnode.inspect} nodes: #{nodes.inspect}"
|
205
|
+
nodes.each do |tonode|
|
206
|
+
puts " tonode: #{tonode.inspect}"
|
207
|
+
add_edge(fromnode || "FUCK", tonode,
|
208
|
+
{ 'label' => :belongs_to.to_s,
|
209
|
+
'arrow' => ARROW_FOR[:belongs_to] })
|
210
|
+
end
|
211
|
+
end
|
212
|
+
@unresolved_edges = false
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# classes that should not be graphed, but are subclasses of
|
217
|
+
# ActiveRecord::Base
|
218
|
+
def self.posers
|
219
|
+
[CGI::Session::ActiveRecordStore::Session]
|
220
|
+
end
|
221
|
+
|
222
|
+
# I'm suppressing the labels for now, but this might be useful (or something
|
223
|
+
# like it) if labels are included.
|
224
|
+
# edge [labeldistance=2.5, labelangle=15]
|
225
|
+
|
226
|
+
# Examines the models and constructs a DOT formatted graph description based
|
227
|
+
# on the ActiveRecord associations that are discovered.
|
228
|
+
#
|
229
|
+
# If called with:
|
230
|
+
#
|
231
|
+
# model_graph.rb --edges=Author-Book
|
232
|
+
#
|
233
|
+
# will cause an edge between, for example, Author and Book which can alter
|
234
|
+
# the relative hierarchical rank of the two nodes (placing the first above
|
235
|
+
# the second). This can often make a dramatic improvement in the overall
|
236
|
+
# layout of the graph. Unless overridden with a normally discovered edge,
|
237
|
+
# the plain arrow will be used to connect the two nodes (so a misspelt
|
238
|
+
# node is more easily detected). Additional edges can be separated by '/'
|
239
|
+
# as in <tt>--edges=Author-Book/Book-Chapter</tt>
|
240
|
+
#
|
241
|
+
# If called with:
|
242
|
+
#
|
243
|
+
# model_graph.rb --nodes=Author
|
244
|
+
#
|
245
|
+
# will cause a node to be placed into the output early. This tends to make
|
246
|
+
# a node appear further to the left in the resulting graph and can be used
|
247
|
+
# to improve the overall layout. Typically, nodes are not specified, but
|
248
|
+
# are left to be positioned based on their edges with other nodes.
|
249
|
+
# Additional nodes can be separated by '/' as in
|
250
|
+
# <tt>--nodes=Author/Book</tt>. Highly connected nodes are less likely to
|
251
|
+
# be influenced by this option.
|
252
|
+
#
|
253
|
+
# ===== Options
|
254
|
+
#
|
255
|
+
# <tt>--name=<em>name</em>:: Change the name of the file into which the
|
256
|
+
# graph is written and the internal name that is assigned.
|
257
|
+
# <tt>--debug</tt>:: When set to _any_ value, causes comments describing the
|
258
|
+
# ActiveRecord models to be included in the DOT output.
|
259
|
+
# <tt>--edges=<em>edges</em></tt>:: With a value of <tt>N1-N2</tt>
|
260
|
+
# [<em>/N3-N4</em>...] adds a relationship between <tt>N1</tt>
|
261
|
+
# and <tt>N2</tt> (and <tt>N3</tt> and <tt>N4</tt>, etc.) as
|
262
|
+
# described above.
|
263
|
+
# <tt>--nodes=<em>nodes</em></tt>:: Adds extra +nodes+ early in the DOT
|
264
|
+
# output to influence placement.
|
265
|
+
# <tt>--test</tt>:: Graphs an internal set of model classes rather than
|
266
|
+
# what's in <tt>app/models/*.rb</tt>
|
267
|
+
# <tt>--shape==<em>type</em></tt>:: Changes the default shape of the nodes
|
268
|
+
# in the graph from +plaintext+ to any
|
269
|
+
# {valid DOT value}[http://www.graphviz.org/doc/info/shapes.html] is
|
270
|
+
# acceptable (try +rectangle+ or +ellipse+)
|
271
|
+
#
|
272
|
+
def self.do_graph(options)
|
273
|
+
output = ""
|
274
|
+
graph = Graph.new(options.name)
|
275
|
+
|
276
|
+
if options.edges
|
277
|
+
options.edges.scan(%r{(\w+)-(\w+)/?}) do |f,t|
|
278
|
+
graph.add_edge(f, t, 'style' => 'solid')
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
if options.nodes
|
283
|
+
options.nodes.scan(%r{(\w+)/?}) do |n|
|
284
|
+
graph.add_node(n)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
version = ActiveRecord::Migrator.current_version
|
289
|
+
if version > 0
|
290
|
+
output << "// Schema version: #{version}\n"
|
291
|
+
end
|
292
|
+
|
293
|
+
# except that I'm spitting out the debugging, this could certainly go right
|
294
|
+
# before the Graph.edges part:
|
295
|
+
output << "digraph #{graph.name} {\n"
|
296
|
+
output << " graph [overlap=scale, nodesep=0.5, ranksep=0.5, separation=0.25]\n"
|
297
|
+
output << " node [shape=#{options.shape.downcase}]\n"
|
298
|
+
|
299
|
+
nodes = Hash.new { |h,k| h[k] = Hash.new }
|
300
|
+
|
301
|
+
for klass in ActiveRecord::Base.send(:subclasses)
|
302
|
+
next if posers.include?(klass)
|
303
|
+
|
304
|
+
# node
|
305
|
+
if options.debug
|
306
|
+
output << "// #{klass.name}"
|
307
|
+
output << " (#{klass.class_name})" unless klass.name == klass.class_name
|
308
|
+
output << "\n"
|
309
|
+
end
|
310
|
+
|
311
|
+
standalone = true
|
312
|
+
for a in klass.reflect_on_all_associations
|
313
|
+
# edge
|
314
|
+
if options.debug
|
315
|
+
output << " //"
|
316
|
+
output << " through #{a.through_reflection.class_name}" if a.through_reflection
|
317
|
+
output << " #{a.macro} #{a.class_name}"
|
318
|
+
output << " as #{a.options[:as].to_s.camelize.singularize}" if a.options[:as]
|
319
|
+
output << " polymorphic" if a.options[:polymorphic]
|
320
|
+
output << "\n"
|
321
|
+
end
|
322
|
+
|
323
|
+
next unless a.class_name == a.name.to_s.camelize.singularize
|
324
|
+
|
325
|
+
opts = { 'label' => a.macro.to_s, 'arrow' => ARROW_FOR[a.macro] }
|
326
|
+
opts.merge!('style' => 'dotted', 'constraint' => 'false') if a.through_reflection
|
327
|
+
opts.merge!('style' => 'dashed', 'midlabel' => a.options[:as].to_s.camelize.singularize) if a.options[:as]
|
328
|
+
opts.merge!('color' => 'blue') if a.options[:polymorphic]
|
329
|
+
|
330
|
+
fromnodename = klass.name
|
331
|
+
tonodename = a.name.to_s.camelize.singularize
|
332
|
+
|
333
|
+
if a.macro == :has_and_belongs_to_many
|
334
|
+
tonodename = [fromnodename, tonodename].sort.join('_')
|
335
|
+
myopts = opts.merge('arrow' => ARROW_FOR[:belongs_to])
|
336
|
+
myopts.merge!('constraint' => 'false') if tonodename > fromnodename
|
337
|
+
graph.add_node(tonodename, %{[shape=diamond, label="", height=0.2, width=0.3]})
|
338
|
+
graph.add_edge(tonodename, fromnodename, myopts)
|
339
|
+
standalone = false
|
340
|
+
end
|
341
|
+
if a.options[:polymorphic]
|
342
|
+
graph.add_edge(fromnodename, tonodename, opts)
|
343
|
+
elsif klass.name == klass.class_name
|
344
|
+
graph.add_edge(fromnodename, tonodename, opts)
|
345
|
+
standalone = false
|
346
|
+
elsif options.debug
|
347
|
+
output << " // !! skipping edge #{fromnodename} -> #{tonodename} #{opts.inspect}\n"
|
348
|
+
opts.merge!('midlabel' => klass.class_name)
|
349
|
+
graph.add_edge(fromnodename, tonodename, opts)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
graph.add_node(klass.name, %{[color=red, fontcolor=red]}) if standalone
|
353
|
+
end
|
354
|
+
|
355
|
+
graph.nodes { |n| output << n << "\n" }
|
356
|
+
|
357
|
+
graph.edges(options.to_h) { |e| output << e << "\n" }
|
358
|
+
|
359
|
+
output << "}\n"
|
360
|
+
|
361
|
+
begin
|
362
|
+
File.open(File.join('tmp', graph.name+'.dot'), 'w') do |f|
|
363
|
+
f.puts output
|
364
|
+
end
|
365
|
+
rescue
|
366
|
+
File.open(graph.name+'.dot', 'w') do |f|
|
367
|
+
f.puts output
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
|
373
|
+
options = OpenStruct.new(:name => 'model',
|
374
|
+
:shape => 'plaintext',
|
375
|
+
:debug => false,
|
376
|
+
:test => false,
|
377
|
+
:label => false,
|
378
|
+
:constraints_first => false)
|
379
|
+
|
380
|
+
OptionParser.new do |opts|
|
381
|
+
opts.on("-t[=FILE]", "--test[=FILE]", String) do |val|
|
382
|
+
puts "test: #{val}"
|
383
|
+
if val && File.exists?(val)
|
384
|
+
puts "getting #{val}..."
|
385
|
+
require val
|
386
|
+
options.test = val
|
387
|
+
else
|
388
|
+
options.test = true
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
opts.on("--sample=WHICH", String) do |val|
|
393
|
+
puts "sample: #{val}"
|
394
|
+
puts "__FILE__ = #{__FILE__}"
|
395
|
+
|
396
|
+
sample = File.join(File.dirname(__FILE__), '..', 'sample',
|
397
|
+
File.basename(val, ".rb") + '.rb')
|
398
|
+
if File.exists?(sample)
|
399
|
+
puts "getting #{sample} ..."
|
400
|
+
require sample
|
401
|
+
options.test = sample
|
402
|
+
options.name = File.basename(val, ".rb") if options.name == 'model'
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
opts.on("-d", '--debug',
|
407
|
+
"Include debugging comments in the graph") do |val|
|
408
|
+
puts "debug: #{val}"
|
409
|
+
options.debug = true
|
410
|
+
end
|
411
|
+
opts.on("--nodes=NODELIST",
|
412
|
+
"Add named nodes to graph") do |val|
|
413
|
+
puts "nodes: #{val}"
|
414
|
+
options.nodes = val
|
415
|
+
end
|
416
|
+
opts.on("--edges=EDGELIST",
|
417
|
+
"Add edges to graph (to affect node rank)") do |val|
|
418
|
+
puts "edges: #{val}"
|
419
|
+
options.edges = val
|
420
|
+
end
|
421
|
+
opts.on("--name=NAME",
|
422
|
+
"Give the graph an internal name and use tmp/NAME.dot for the output") do |val|
|
423
|
+
puts "name: #{val}"
|
424
|
+
options.name = val
|
425
|
+
end
|
426
|
+
opts.on("--label", "-l", "show edge labels") { |val| options.label = true }
|
427
|
+
opts.on("--constraints-first", "--cf", "output constrained edges first (normally last)") do |val|
|
428
|
+
options.constraints_first = true
|
429
|
+
end
|
430
|
+
|
431
|
+
end.parse!
|
432
|
+
|
433
|
+
puts options.to_s
|
434
|
+
|
435
|
+
# unless (__FILE__ == $0 ? ARGV.include?('--test') : ENV.include?('TEST'))
|
436
|
+
unless options.test
|
437
|
+
for f in Dir.glob(File.join(RAILS_ROOT || '.', "app/models", "*.rb"))
|
438
|
+
puts "getting #{f}..."
|
439
|
+
require f
|
440
|
+
end
|
441
|
+
else
|
442
|
+
# Testing
|
443
|
+
if TrueClass === options.test
|
444
|
+
eval <<-"SAMPLE"
|
445
|
+
class A < ActiveRecord::Base # :nodoc:
|
446
|
+
has_many :bs
|
447
|
+
has_one :c
|
448
|
+
end
|
449
|
+
class B < ActiveRecord::Base # :nodoc:
|
450
|
+
belongs_to :a
|
451
|
+
end
|
452
|
+
class C < ActiveRecord::Base # :nodoc:
|
453
|
+
belongs_to :a
|
454
|
+
end
|
455
|
+
class One < ActiveRecord::Base # :nodoc:
|
456
|
+
has_and_belongs_to_many :twos
|
457
|
+
end
|
458
|
+
class Two < ActiveRecord::Base # :nodoc:
|
459
|
+
has_and_belongs_to_many :ones
|
460
|
+
end
|
461
|
+
class Alpha < ActiveRecord::Base # :nodoc:
|
462
|
+
has_many :betas
|
463
|
+
has_many :gammas, :through => :betas
|
464
|
+
end
|
465
|
+
class Beta < ActiveRecord::Base # :nodoc:
|
466
|
+
belongs_to :alpha
|
467
|
+
belongs_to :gamma
|
468
|
+
end
|
469
|
+
class Gamma < ActiveRecord::Base # :nodoc:
|
470
|
+
has_many :betas
|
471
|
+
has_many :alphas, :through => :betas
|
472
|
+
end
|
473
|
+
class Selfish < ActiveRecord::Base # :nodoc:
|
474
|
+
has_many :selfishes, :foreign_key => :solo_id
|
475
|
+
end
|
476
|
+
SAMPLE
|
477
|
+
puts 'doing the SAMPLE'
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
do_graph(options)
|
482
|
+
|
483
|
+
end
|