model-graph 0.1.4
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 +444 -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/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/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/graph.rb +71 -0
- data/lib/model_graph.rb +1 -0
- data/lib/model_graph/version.rb +9 -0
- data/test/model_graph_test.rb +13 -0
- data/test/test_helper.rb +5 -0
- metadata +106 -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,2007,2008 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,444 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# -*- ruby -*-
|
|
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
|
|
6
|
+
# rendered into a graph by programs such as dot and neato and viewed with
|
|
7
|
+
# Graphviz (an {Open Source}[http://www.graphviz.org/License.php] viewer). I
|
|
8
|
+
# use the {Mac OS X version}[http://www.pixelglow.com/graphviz/], but there's
|
|
9
|
+
# a
|
|
10
|
+
# {Darwinports}[http://darwinports.opendarwin.org/darwinports/dports/graphics/graphviz/Portfile]
|
|
11
|
+
# (aka, Macports) version, too. Or get the
|
|
12
|
+
# source[http://www.graphviz.org/Download.php] and build it yourself. You can
|
|
13
|
+
# also import a DOT file with OmniGraffle, but it doesn't support all the edge
|
|
14
|
+
# decorations that I'm using.
|
|
15
|
+
#
|
|
16
|
+
# DOT format:: http://www.graphviz.org/doc/info/lang.html
|
|
17
|
+
# Graphviz license:: http://www.graphviz.org/License.php
|
|
18
|
+
# Mac OS X Graphviz:: http://www.pixelglow.com/graphviz/
|
|
19
|
+
# Darwinports Graphviz:: http://darwinports.opendarwin.org/darwinports/dports/graphics/graphviz/Portfile
|
|
20
|
+
# Graphviz source code:: http://www.graphviz.org/Download.php
|
|
21
|
+
#
|
|
22
|
+
# This is *certainly* a work-in-progress.
|
|
23
|
+
#
|
|
24
|
+
# === Usage:
|
|
25
|
+
#
|
|
26
|
+
# model_graph [options]
|
|
27
|
+
#
|
|
28
|
+
# then open tmp/model_graph.dot with a viewer. Using 'model_graph --debug'
|
|
29
|
+
# will write a bunch of the raw information obtained from reflecting on the
|
|
30
|
+
# ActiveRecord model classes into the output as comments (including some
|
|
31
|
+
# things that don't actually affect the final graph).
|
|
32
|
+
#
|
|
33
|
+
# See the documentation for ModelGraph#do_graph for some additional options.
|
|
34
|
+
#
|
|
35
|
+
# === Bugs:
|
|
36
|
+
#
|
|
37
|
+
# The ordering within DOT is based on the tail-to-head relationship of edges,
|
|
38
|
+
# but these are somewhat arbitrarily determined by the current reflection on
|
|
39
|
+
# ActiveRecord associations. The use of the
|
|
40
|
+
# <tt>--edges=<var>[list]</var></tt> and <tt>--nodes=<var>[list]</var></tt>
|
|
41
|
+
# options is only a partial fix.
|
|
42
|
+
#
|
|
43
|
+
# === TODO:
|
|
44
|
+
# * reverse some edges so nodes stay mostly balanced (close to same number of
|
|
45
|
+
# in and out edges)
|
|
46
|
+
# * handle indirect descendants of ActiveRecord::Base? (at least make it
|
|
47
|
+
# clearer how they're filtered out of the graph)
|
|
48
|
+
# * models that have no (outbound) associations are depicted in red, but
|
|
49
|
+
# sometimes these are just confused by an overridden class (even with :as)
|
|
50
|
+
#
|
|
51
|
+
# ==== Credits:
|
|
52
|
+
# Inspired by: Matt Biddulph at August 2, 2006 02:57 PM
|
|
53
|
+
# URL: http://www.hackdiary.com/archives/000093.html
|
|
54
|
+
#
|
|
55
|
+
# ----
|
|
56
|
+
#
|
|
57
|
+
# This is released under the MIT License. Please send comments or
|
|
58
|
+
# enhanncement ideas to Rob[at]AgileConsultingLLC[dot]com or
|
|
59
|
+
# Rob_Biedenharn[at]alum[dot]mit[dot]edu
|
|
60
|
+
#
|
|
61
|
+
# Copyright (c) 2006 Rob Biedenharn
|
|
62
|
+
#
|
|
63
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
64
|
+
# of this software and associated documentation files (the "Software"), to
|
|
65
|
+
# deal in the Software without restriction, including without limitation the
|
|
66
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
67
|
+
# sell copies of the Software, and to permit persons to whom the Software is
|
|
68
|
+
# furnished to do so, subject to the following conditions:
|
|
69
|
+
#
|
|
70
|
+
# The above copyright notice and this permission notice shall be included in
|
|
71
|
+
# all copies or substantial portions of the Software.
|
|
72
|
+
#
|
|
73
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
74
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
75
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
76
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
77
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
78
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
79
|
+
# IN THE SOFTWARE.
|
|
80
|
+
|
|
81
|
+
require 'rubygems'
|
|
82
|
+
|
|
83
|
+
require 'config/boot'
|
|
84
|
+
require 'config/environment'
|
|
85
|
+
|
|
86
|
+
require 'optparse'
|
|
87
|
+
require 'ostruct'
|
|
88
|
+
require File.expand_path(File.dirname(__FILE__)+'/../lib/model_graph.rb')
|
|
89
|
+
|
|
90
|
+
class Hash # :nodoc:
|
|
91
|
+
def inspect(options={})
|
|
92
|
+
out = ''
|
|
93
|
+
sep = '['
|
|
94
|
+
self.each { |k,v| unless ! options[:label] && k =~ /(?:head|tail)label/
|
|
95
|
+
out << sep << "#{k}=#{v}"; sep=', '
|
|
96
|
+
end }
|
|
97
|
+
out << ']' unless sep == '['
|
|
98
|
+
out
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class OpenStruct # :nodoc:
|
|
103
|
+
def to_h
|
|
104
|
+
@table
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
module ModelGraph
|
|
109
|
+
|
|
110
|
+
# Should :belongs_to differ when paired with :has_one versus :has_many?
|
|
111
|
+
#
|
|
112
|
+
# Should :has_one be 'teetee' if required? (i.e., not null)
|
|
113
|
+
|
|
114
|
+
ARROW_FOR = {
|
|
115
|
+
:belongs_to => 'tee',
|
|
116
|
+
:has_many => 'crowodot',
|
|
117
|
+
:has_one => 'odottee',
|
|
118
|
+
:has_and_belongs_to_many => 'crowodot'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
RC_FILE = '.model_graph_rc'
|
|
122
|
+
|
|
123
|
+
# classes that should not be graphed, but are subclasses of
|
|
124
|
+
# ActiveRecord::Base
|
|
125
|
+
def self.posers
|
|
126
|
+
[CGI::Session::ActiveRecordStore::Session]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# I'm suppressing the labels for now, but this might be useful (or something
|
|
130
|
+
# like it) if labels are included.
|
|
131
|
+
# edge [labeldistance=2.5, labelangle=15]
|
|
132
|
+
|
|
133
|
+
# Examines the models and constructs a DOT formatted graph description based
|
|
134
|
+
# on the ActiveRecord associations that are discovered.
|
|
135
|
+
#
|
|
136
|
+
# If called with:
|
|
137
|
+
#
|
|
138
|
+
# model_graph --edges=Author-Book
|
|
139
|
+
#
|
|
140
|
+
# will cause an edge between, for example, Author and Book which can alter
|
|
141
|
+
# the relative hierarchical rank of the two nodes (placing the first above
|
|
142
|
+
# the second). This can often make a dramatic improvement in the overall
|
|
143
|
+
# layout of the graph. Unless overridden with a normally discovered edge,
|
|
144
|
+
# the plain arrow will be used to connect the two nodes (so a misspelt
|
|
145
|
+
# node is more easily detected). Additional edges can be separated by '/'
|
|
146
|
+
# as in <tt>--edges=Author-Book/Book-Chapter</tt>
|
|
147
|
+
#
|
|
148
|
+
# If called with:
|
|
149
|
+
#
|
|
150
|
+
# model_graph --nodes=Author
|
|
151
|
+
#
|
|
152
|
+
# will cause a node to be placed into the output early. This tends to make
|
|
153
|
+
# a node appear further to the left in the resulting graph and can be used
|
|
154
|
+
# to improve the overall layout. Typically, nodes are not specified, but
|
|
155
|
+
# are left to be positioned based on their edges with other nodes.
|
|
156
|
+
# Additional nodes can be separated by '/' as in
|
|
157
|
+
# <tt>--nodes=Author/Book</tt>. Highly connected nodes are less likely to
|
|
158
|
+
# be influenced by this option.
|
|
159
|
+
#
|
|
160
|
+
# ===== Options
|
|
161
|
+
#
|
|
162
|
+
# <tt>--name=<em>name</em></tt>:: Change the name of the file into which the
|
|
163
|
+
# graph is written and the internal name that is assigned.
|
|
164
|
+
# <tt>--debug</tt>:: When set to _any_ value, causes comments describing the
|
|
165
|
+
# ActiveRecord models to be included in the DOT output.
|
|
166
|
+
# <tt>--edges=<em>edges</em></tt>:: With a value of <tt>N1-N2</tt>
|
|
167
|
+
# [<em>/N3-N4</em>...] adds a relationship between <tt>N1</tt>
|
|
168
|
+
# and <tt>N2</tt> (and <tt>N3</tt> and <tt>N4</tt>, etc.) as
|
|
169
|
+
# described above. When separated by a '+' as in <tt>N1+N2</tt>,
|
|
170
|
+
# the relative placement of the nodes will not be constrained
|
|
171
|
+
# (this is sometimes useful for allowing nodes to share a 'rank'
|
|
172
|
+
# and be rendered horizontally if there are no other
|
|
173
|
+
# relationships).
|
|
174
|
+
# <tt>--nodes=<em>nodes</em></tt>:: Adds extra +nodes+ early in the DOT
|
|
175
|
+
# output to influence placement.
|
|
176
|
+
# <tt>--test</tt>:: Graphs an internal set of model classes rather than
|
|
177
|
+
# what's in <tt>app/models/*.rb</tt>
|
|
178
|
+
# <tt>--shape==<em>type</em></tt>:: Changes the default shape of the nodes
|
|
179
|
+
# in the graph from +plaintext+ to any
|
|
180
|
+
# {valid DOT value}[http://www.graphviz.org/doc/info/shapes.html] is
|
|
181
|
+
# acceptable (try +rectangle+ or +ellipse+)
|
|
182
|
+
# <tt>--label</tt>:: Show edge labels
|
|
183
|
+
# <tt>--constraints-first</tt>:: (or '--cf') Output constrained edges first
|
|
184
|
+
# (normally last). This may improve the overall layout when
|
|
185
|
+
# there are has_and_belongs_to_many relationships.
|
|
186
|
+
#
|
|
187
|
+
# ===== Persistent Options
|
|
188
|
+
#
|
|
189
|
+
# Any of the options can be specified in a file named .model_graph_rc in the
|
|
190
|
+
# RAILS_ROOT directory which will be used to initialize the options prior to
|
|
191
|
+
# processing the command-line.
|
|
192
|
+
#
|
|
193
|
+
# Note that options on the command line supercede the contents of the
|
|
194
|
+
# .model_graph_rc file rather than add to it. For 'edges=...' and
|
|
195
|
+
# 'nodes=...', this might be considered unfortunate when trying to influence
|
|
196
|
+
# the resulting layout after your models change.
|
|
197
|
+
#
|
|
198
|
+
# Sure it's a hack, but this special rc file can set or override the default
|
|
199
|
+
# options. I use this for long "edges=..." lines mostly. If model_graph
|
|
200
|
+
# was a bit smarter about the implicit layout, then perhaps this would be
|
|
201
|
+
# unnecessary. However, the DOT documentation also mentions that this
|
|
202
|
+
# technique of influencing the layout by tweaking the order of nodes and
|
|
203
|
+
# edges is fragile anyway and may (should!) change in the future.
|
|
204
|
+
def self.do_graph(options)
|
|
205
|
+
output = ""
|
|
206
|
+
graph = ::Graph.new(options.name)
|
|
207
|
+
|
|
208
|
+
if options.edges
|
|
209
|
+
options.edges.scan(%r{(\w+)([-+])(\w+)/?}) do |f,kind,t|
|
|
210
|
+
eopts = { 'style' => 'solid' }
|
|
211
|
+
eopts.merge!('constraint' => 'false') if kind == '+'
|
|
212
|
+
graph.add_edge(f, t, eopts)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if options.nodes
|
|
217
|
+
options.nodes.scan(%r{(\w+)/?}) do |n|
|
|
218
|
+
graph.add_node(n)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
version = ActiveRecord::Migrator.current_version
|
|
223
|
+
if version > 0
|
|
224
|
+
output << "// Schema version: #{version}\n"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# except that I'm spitting out the debugging, this could certainly go right
|
|
228
|
+
# before the Graph.edges part:
|
|
229
|
+
output << "digraph #{graph.name} {\n"
|
|
230
|
+
output << " graph [overlap=scale, nodesep=0.5, ranksep=0.5, separation=0.25]\n"
|
|
231
|
+
output << " node [shape=#{options.shape.downcase}]\n"
|
|
232
|
+
|
|
233
|
+
nodes = Hash.new { |h,k| h[k] = Hash.new }
|
|
234
|
+
|
|
235
|
+
for klass in ActiveRecord::Base.send(:subclasses)
|
|
236
|
+
next if posers.include?(klass)
|
|
237
|
+
|
|
238
|
+
# node
|
|
239
|
+
if options.debug
|
|
240
|
+
output << "// #{klass.name}"
|
|
241
|
+
output << " (#{klass.class_name})" unless klass.name == klass.class_name
|
|
242
|
+
output << "\n"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
standalone = true
|
|
246
|
+
for a in klass.reflect_on_all_associations
|
|
247
|
+
# edge
|
|
248
|
+
if options.debug
|
|
249
|
+
output << " //"
|
|
250
|
+
output << " through #{a.through_reflection.class_name}" if a.through_reflection
|
|
251
|
+
output << " #{a.macro} #{a.class_name}"
|
|
252
|
+
output << " as #{a.options[:as].to_s.camelize.singularize}" if a.options[:as]
|
|
253
|
+
output << " polymorphic" if a.options[:polymorphic]
|
|
254
|
+
output << "\n"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Why was I skipping these?
|
|
258
|
+
# next unless a.class_name == a.name.to_s.camelize.singularize
|
|
259
|
+
|
|
260
|
+
next if a.options[:polymorphic]
|
|
261
|
+
|
|
262
|
+
opts = { 'label' => a.macro.to_s, 'arrow' => ARROW_FOR[a.macro] }
|
|
263
|
+
opts.merge!('style' => 'dotted', 'constraint' => 'false') if a.through_reflection
|
|
264
|
+
opts.merge!('color' => 'blue', 'midlabel' => a.options[:as].to_s.camelize.singularize) if a.options[:as]
|
|
265
|
+
opts.merge!('style' => 'dashed', 'color' => 'green', 'fontsize' => '8',
|
|
266
|
+
'midlabel' => a.options[:foreign_key] || "#{a.name.to_s.singularize.underscore}_id"
|
|
267
|
+
) unless a.class_name == a.name.to_s.camelize.singularize
|
|
268
|
+
|
|
269
|
+
opts.merge!('color' => 'red') if a.options[:finder_sql]
|
|
270
|
+
|
|
271
|
+
fromnodename = klass.name
|
|
272
|
+
#tonodename = a.name.to_s.camelize.singularize
|
|
273
|
+
tonodename = a.class_name
|
|
274
|
+
|
|
275
|
+
if a.macro == :has_and_belongs_to_many
|
|
276
|
+
tonodename = [fromnodename, tonodename].sort.join('_')
|
|
277
|
+
myopts = opts.merge('arrow' => ARROW_FOR[:belongs_to])
|
|
278
|
+
myopts.merge!('constraint' => 'false') if tonodename > fromnodename
|
|
279
|
+
graph.add_node(tonodename, %{[shape=diamond, label="", height=0.2, width=0.3]})
|
|
280
|
+
graph.add_edge(tonodename, fromnodename, myopts)
|
|
281
|
+
standalone = false
|
|
282
|
+
end
|
|
283
|
+
# Was this part of the polymorphic thing?
|
|
284
|
+
#if klass.name == klass.class_name
|
|
285
|
+
graph.add_edge(fromnodename, tonodename, opts)
|
|
286
|
+
if a.options[:as]
|
|
287
|
+
graph.add_edge(tonodename, fromnodename,
|
|
288
|
+
'arrow' => ARROW_FOR[:belongs_to], 'fontcolor' => 'blue')
|
|
289
|
+
end
|
|
290
|
+
standalone = false
|
|
291
|
+
# elsif options.debug
|
|
292
|
+
# output << " // !! skipping edge #{fromnodename} -> #{tonodename} #{opts.inspect}\n"
|
|
293
|
+
# end
|
|
294
|
+
end
|
|
295
|
+
graph.add_node(klass.name, %{[color=red, fontcolor=red]}) if standalone
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
graph.nodes { |n| output << n << "\n" }
|
|
299
|
+
|
|
300
|
+
graph.edges(options.to_h) { |e| output << e << "\n" }
|
|
301
|
+
|
|
302
|
+
output << "}\n"
|
|
303
|
+
|
|
304
|
+
begin
|
|
305
|
+
File.open(File.join('tmp', graph.name+'.dot'), 'w') do |f|
|
|
306
|
+
f.puts output
|
|
307
|
+
end
|
|
308
|
+
rescue
|
|
309
|
+
File.open(graph.name+'.dot', 'w') do |f|
|
|
310
|
+
f.puts output
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
options = OpenStruct.new(:name => 'model',
|
|
317
|
+
:shape => 'plaintext',
|
|
318
|
+
:debug => false,
|
|
319
|
+
:test => false,
|
|
320
|
+
:label => false,
|
|
321
|
+
:constraints_first => false)
|
|
322
|
+
|
|
323
|
+
# Sure it's a hack, but a special rc file can set or override the
|
|
324
|
+
# default options. I use this for long "edges=..." lines mostly. If
|
|
325
|
+
# model_graph was a bit smarter about the implicit layout, then perhaps this
|
|
326
|
+
# would be unnecessary. However, the DOT documentation also mentions that
|
|
327
|
+
# this technique of influencing the layout by tweaking the order of nodes
|
|
328
|
+
# and edges is fragile anyway and may (should!) change in the future.
|
|
329
|
+
File.open(RC_FILE, 'r') do |rc|
|
|
330
|
+
for line in rc
|
|
331
|
+
next if line =~ /\A\s*#/
|
|
332
|
+
var, value = line.split(/=/, 2)
|
|
333
|
+
options.send("#{var}=", value)
|
|
334
|
+
end
|
|
335
|
+
end if File.exists?(RC_FILE)
|
|
336
|
+
|
|
337
|
+
OptionParser.new do |opts|
|
|
338
|
+
opts.on("-t[=FILE]", "--test[=FILE]", String) do |val|
|
|
339
|
+
puts "test: #{val}" if options.debug
|
|
340
|
+
if val && File.exists?(val)
|
|
341
|
+
puts "getting #{val}..." if options.debug
|
|
342
|
+
require val
|
|
343
|
+
options.test = val
|
|
344
|
+
else
|
|
345
|
+
options.test = true
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
opts.on("--sample=WHICH", String) do |val|
|
|
350
|
+
puts "sample: #{val}" if options.debug
|
|
351
|
+
puts "__FILE__ = #{__FILE__}" if options.debug
|
|
352
|
+
|
|
353
|
+
sample = File.join(File.dirname(__FILE__), '..', 'examples',
|
|
354
|
+
File.basename(val, ".rb") + '.rb')
|
|
355
|
+
if File.exists?(sample)
|
|
356
|
+
puts "getting #{sample} ..." if options.debug
|
|
357
|
+
require sample
|
|
358
|
+
options.test = sample
|
|
359
|
+
options.name = File.basename(val, ".rb") if options.name == 'model'
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
opts.on("-d", '--debug',
|
|
364
|
+
"Include debugging comments in the graph") do |val|
|
|
365
|
+
puts "debug: #{val}"
|
|
366
|
+
options.debug = true
|
|
367
|
+
end
|
|
368
|
+
opts.on("--nodes=NODELIST",
|
|
369
|
+
"Add named nodes to graph") do |val|
|
|
370
|
+
puts "nodes: #{val}" if options.debug
|
|
371
|
+
options.nodes = val
|
|
372
|
+
end
|
|
373
|
+
opts.on("--edges=EDGELIST",
|
|
374
|
+
"Add edges to graph (to affect node rank)") do |val|
|
|
375
|
+
puts "edges: #{val}" if options.debug
|
|
376
|
+
options.edges = val
|
|
377
|
+
end
|
|
378
|
+
opts.on("--name=NAME",
|
|
379
|
+
"Give the graph an internal name and use tmp/NAME.dot for the output") do |val|
|
|
380
|
+
puts "name: #{val}" if options.debug
|
|
381
|
+
options.name = val
|
|
382
|
+
end
|
|
383
|
+
opts.on("--shape=KIND", "override the shape of a node with a valid DOT shape") do |val|
|
|
384
|
+
puts "shape: #{val}" if options.debug
|
|
385
|
+
options.shape = val
|
|
386
|
+
end
|
|
387
|
+
opts.on("--label", "-l", "show edge labels") { |val| options.label = true }
|
|
388
|
+
opts.on("--constraints-first", "--cf", "output constrained edges first (normally last)") do |val|
|
|
389
|
+
options.constraints_first = true
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
end.parse!
|
|
393
|
+
|
|
394
|
+
puts options.to_s if options.debug
|
|
395
|
+
|
|
396
|
+
unless options.test
|
|
397
|
+
for f in Dir.glob(File.join(RAILS_ROOT || '.', "app/models", "*.rb"))
|
|
398
|
+
puts "getting #{f}..." if options.debug
|
|
399
|
+
require f
|
|
400
|
+
end
|
|
401
|
+
else
|
|
402
|
+
# Testing
|
|
403
|
+
if TrueClass === options.test
|
|
404
|
+
eval <<-"SAMPLE"
|
|
405
|
+
class A < ActiveRecord::Base # :nodoc:
|
|
406
|
+
has_many :bs
|
|
407
|
+
has_one :c
|
|
408
|
+
end
|
|
409
|
+
class B < ActiveRecord::Base # :nodoc:
|
|
410
|
+
belongs_to :a
|
|
411
|
+
end
|
|
412
|
+
class C < ActiveRecord::Base # :nodoc:
|
|
413
|
+
belongs_to :a
|
|
414
|
+
end
|
|
415
|
+
class One < ActiveRecord::Base # :nodoc:
|
|
416
|
+
has_and_belongs_to_many :twos
|
|
417
|
+
end
|
|
418
|
+
class Two < ActiveRecord::Base # :nodoc:
|
|
419
|
+
has_and_belongs_to_many :ones
|
|
420
|
+
end
|
|
421
|
+
class Alpha < ActiveRecord::Base # :nodoc:
|
|
422
|
+
has_many :betas
|
|
423
|
+
has_many :gammas, :through => :betas
|
|
424
|
+
end
|
|
425
|
+
class Beta < ActiveRecord::Base # :nodoc:
|
|
426
|
+
belongs_to :alpha
|
|
427
|
+
belongs_to :gamma
|
|
428
|
+
end
|
|
429
|
+
class Gamma < ActiveRecord::Base # :nodoc:
|
|
430
|
+
has_many :betas
|
|
431
|
+
has_many :alphas, :through => :betas
|
|
432
|
+
end
|
|
433
|
+
class Selfish < ActiveRecord::Base # :nodoc:
|
|
434
|
+
has_many :selfishes, :foreign_key => :solo_id
|
|
435
|
+
end
|
|
436
|
+
SAMPLE
|
|
437
|
+
puts 'doing the SAMPLE' if options.debug
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
do_graph(options)
|
|
442
|
+
|
|
443
|
+
end
|
|
444
|
+
__END__
|