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 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