model_graph 0.1.2 → 0.1.3
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/bin/model_graph +85 -102
- data/lib/graph.rb +71 -0
- data/lib/model_graph.rb +1 -1
- data/lib/model_graph/version.rb +1 -1
- metadata +5 -3
data/bin/model_graph
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/local/bin/ruby
|
2
|
+
# -*- ruby -*-
|
2
3
|
|
3
4
|
# When run from the trunk of a Rails project, produces
|
4
5
|
# {DOT}[http://www.graphviz.org/doc/info/lang.html] output which can be
|
@@ -22,12 +23,12 @@
|
|
22
23
|
#
|
23
24
|
# === Usage:
|
24
25
|
#
|
25
|
-
# model_graph
|
26
|
+
# model_graph [options]
|
26
27
|
#
|
27
|
-
# then open tmp/model_graph.dot with a viewer. Using 'model_graph
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
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).
|
31
32
|
#
|
32
33
|
# See the documentation for ModelGraph#do_graph for some additional options.
|
33
34
|
#
|
@@ -81,12 +82,15 @@ require 'config/environment'
|
|
81
82
|
|
82
83
|
require 'optparse'
|
83
84
|
require 'ostruct'
|
85
|
+
require File.expand_path(File.dirname(__FILE__)+'/../lib/model_graph.rb')
|
84
86
|
|
85
87
|
class Hash # :nodoc:
|
86
88
|
def inspect(options={})
|
87
89
|
out = ''
|
88
90
|
sep = '['
|
89
|
-
self.each { |k,v| unless ! options[:label] && k =~ /(?:head|tail)label
|
91
|
+
self.each { |k,v| unless ! options[:label] && k =~ /(?:head|tail)label/
|
92
|
+
out << sep << "#{k}=#{v}"; sep=', '
|
93
|
+
end }
|
90
94
|
out << ']' unless sep == '['
|
91
95
|
out
|
92
96
|
end
|
@@ -111,77 +115,7 @@ module ModelGraph
|
|
111
115
|
:has_and_belongs_to_many => 'crowodot'
|
112
116
|
}
|
113
117
|
|
114
|
-
|
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
|
-
# A hm B :as => Y gives edge A->B and implies B bt A
|
130
|
-
# C hm B :as => Y gives edge C->B and implies B bt A
|
131
|
-
# B bt Y :polymorphic => true no new information
|
132
|
-
end
|
133
|
-
|
134
|
-
# Create an unattached node in this graph.
|
135
|
-
def add_node(nodename, options="")
|
136
|
-
@nodes[nodename] = options
|
137
|
-
end
|
138
|
-
|
139
|
-
# Iterates over all the nodes previously added to this graph.
|
140
|
-
def nodes # :yields: nodestring
|
141
|
-
@nodes.each do |name,options|
|
142
|
-
yield "#{name} #{options}"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# Create a directed edge from one node to another. If an edge between
|
147
|
-
# nodes already exists in the opposite direction, the arrow will be
|
148
|
-
# attached to the other end of the existing edge.
|
149
|
-
def add_edge(fromnode, tonode, options={})
|
150
|
-
unless @edges[tonode].has_key? fromnode
|
151
|
-
options.each do |k,v|
|
152
|
-
@edges[fromnode][tonode][case k.to_s
|
153
|
-
when 'label' : 'taillabel'
|
154
|
-
when 'midlabel' : 'label'
|
155
|
-
when /^arrow(?:head|tail)?$/ : 'arrowhead'
|
156
|
-
else k
|
157
|
-
end] = v
|
158
|
-
end
|
159
|
-
else
|
160
|
-
# reverse sense and overload existing edge
|
161
|
-
options.each do |k,v|
|
162
|
-
@edges[tonode][fromnode][case k.to_s
|
163
|
-
when 'label' : 'headlabel'
|
164
|
-
when 'midlabel' : 'label'
|
165
|
-
when /^arrow(?:head|tail)?$/ : 'arrowtail'
|
166
|
-
else k
|
167
|
-
end] = v
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Iterates over all the DOT formatted edges with nodes having the most
|
173
|
-
# edges first and the edges without a constraint attribute before those
|
174
|
-
# that do.
|
175
|
-
def edges(options={}) # :yields: edgestring
|
176
|
-
@edges.sort { |a,b| b[1].length <=> a[1].length }.each do |(fromnode,nh)|
|
177
|
-
nh.sort_by { |(t,a)| (a.has_key?('constraint') ^ options[:constraints_first]) ? 1 : 0 }.each do |tonode,eh|
|
178
|
-
e = "#{fromnode} -> #{tonode} "
|
179
|
-
e << eh.inspect(options) unless eh.nil?
|
180
|
-
yield e
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
118
|
+
RC_FILE = '.model_graph_rc'
|
185
119
|
|
186
120
|
# classes that should not be graphed, but are subclasses of
|
187
121
|
# ActiveRecord::Base
|
@@ -198,7 +132,7 @@ module ModelGraph
|
|
198
132
|
#
|
199
133
|
# If called with:
|
200
134
|
#
|
201
|
-
# model_graph
|
135
|
+
# model_graph --edges=Author-Book
|
202
136
|
#
|
203
137
|
# will cause an edge between, for example, Author and Book which can alter
|
204
138
|
# the relative hierarchical rank of the two nodes (placing the first above
|
@@ -210,7 +144,7 @@ module ModelGraph
|
|
210
144
|
#
|
211
145
|
# If called with:
|
212
146
|
#
|
213
|
-
# model_graph
|
147
|
+
# model_graph --nodes=Author
|
214
148
|
#
|
215
149
|
# will cause a node to be placed into the output early. This tends to make
|
216
150
|
# a node appear further to the left in the resulting graph and can be used
|
@@ -222,14 +156,18 @@ module ModelGraph
|
|
222
156
|
#
|
223
157
|
# ===== Options
|
224
158
|
#
|
225
|
-
# <tt>--name=<em>name</em>:: Change the name of the file into which the
|
159
|
+
# <tt>--name=<em>name</em></tt>:: Change the name of the file into which the
|
226
160
|
# graph is written and the internal name that is assigned.
|
227
161
|
# <tt>--debug</tt>:: When set to _any_ value, causes comments describing the
|
228
162
|
# ActiveRecord models to be included in the DOT output.
|
229
163
|
# <tt>--edges=<em>edges</em></tt>:: With a value of <tt>N1-N2</tt>
|
230
164
|
# [<em>/N3-N4</em>...] adds a relationship between <tt>N1</tt>
|
231
165
|
# and <tt>N2</tt> (and <tt>N3</tt> and <tt>N4</tt>, etc.) as
|
232
|
-
# described above.
|
166
|
+
# described above. When separated by a '+' as in <tt>N1+N2</tt>,
|
167
|
+
# the relative placement of the nodes will not be constrained
|
168
|
+
# (this is sometimes useful for allowing nodes to share a 'rank'
|
169
|
+
# and be rendered horizontally if there are no other
|
170
|
+
# relationships).
|
233
171
|
# <tt>--nodes=<em>nodes</em></tt>:: Adds extra +nodes+ early in the DOT
|
234
172
|
# output to influence placement.
|
235
173
|
# <tt>--test</tt>:: Graphs an internal set of model classes rather than
|
@@ -238,14 +176,37 @@ module ModelGraph
|
|
238
176
|
# in the graph from +plaintext+ to any
|
239
177
|
# {valid DOT value}[http://www.graphviz.org/doc/info/shapes.html] is
|
240
178
|
# acceptable (try +rectangle+ or +ellipse+)
|
179
|
+
# <tt>--label</tt>:: Show edge labels
|
180
|
+
# <tt>--constraints-first</tt>:: (or '--cf') Output constrained edges first
|
181
|
+
# (normally last). This may improve the overall layout when
|
182
|
+
# there are has_and_belongs_to_many relationships.
|
183
|
+
#
|
184
|
+
# ===== Persistent Options
|
241
185
|
#
|
186
|
+
# Any of the options can be specified in a file named .model_graph_rc in the
|
187
|
+
# RAILS_ROOT directory which will be used to initialize the options prior to
|
188
|
+
# processing the command-line.
|
189
|
+
#
|
190
|
+
# Note that options on the command line supercede the contents of the
|
191
|
+
# .model_graph_rc file rather than add to it. For 'edges=...' and
|
192
|
+
# 'nodes=...', this might be considered unfortunate when trying to influence
|
193
|
+
# the resulting layout after your models change.
|
194
|
+
#
|
195
|
+
# Sure it's a hack, but this special rc file can set or override the default
|
196
|
+
# options. I use this for long "edges=..." lines mostly. If model_graph
|
197
|
+
# was a bit smarter about the implicit layout, then perhaps this would be
|
198
|
+
# unnecessary. However, the DOT documentation also mentions that this
|
199
|
+
# technique of influencing the layout by tweaking the order of nodes and
|
200
|
+
# edges is fragile anyway and may (should!) change in the future.
|
242
201
|
def self.do_graph(options)
|
243
202
|
output = ""
|
244
|
-
graph = Graph.new(options.name)
|
203
|
+
graph = ::Graph.new(options.name)
|
245
204
|
|
246
205
|
if options.edges
|
247
|
-
options.edges.scan(%r{(\w+)
|
248
|
-
|
206
|
+
options.edges.scan(%r{(\w+)([-+])(\w+)/?}) do |f,kind,t|
|
207
|
+
eopts = { 'style' => 'solid' }
|
208
|
+
eopts.merge!('constraint' => 'false') if kind == '+'
|
209
|
+
graph.add_edge(f, t, eopts)
|
249
210
|
end
|
250
211
|
end
|
251
212
|
|
@@ -290,17 +251,23 @@ module ModelGraph
|
|
290
251
|
output << "\n"
|
291
252
|
end
|
292
253
|
|
293
|
-
|
254
|
+
# Why was I skipping these?
|
255
|
+
# next unless a.class_name == a.name.to_s.camelize.singularize
|
256
|
+
|
294
257
|
next if a.options[:polymorphic]
|
295
258
|
|
296
259
|
opts = { 'label' => a.macro.to_s, 'arrow' => ARROW_FOR[a.macro] }
|
297
260
|
opts.merge!('style' => 'dotted', 'constraint' => 'false') if a.through_reflection
|
298
261
|
opts.merge!('color' => 'blue', 'midlabel' => a.options[:as].to_s.camelize.singularize) if a.options[:as]
|
262
|
+
opts.merge!('style' => 'dashed', 'color' => 'green', 'fontsize' => '8',
|
263
|
+
'midlabel' => a.options[:foreign_key] || "#{a.name.to_s.singularize.underscore}_id"
|
264
|
+
) unless a.class_name == a.name.to_s.camelize.singularize
|
299
265
|
|
300
266
|
opts.merge!('color' => 'red') if a.options[:finder_sql]
|
301
267
|
|
302
268
|
fromnodename = klass.name
|
303
|
-
tonodename = a.name.to_s.camelize.singularize
|
269
|
+
#tonodename = a.name.to_s.camelize.singularize
|
270
|
+
tonodename = a.class_name
|
304
271
|
|
305
272
|
if a.macro == :has_and_belongs_to_many
|
306
273
|
tonodename = [fromnodename, tonodename].sort.join('_')
|
@@ -310,16 +277,17 @@ module ModelGraph
|
|
310
277
|
graph.add_edge(tonodename, fromnodename, myopts)
|
311
278
|
standalone = false
|
312
279
|
end
|
313
|
-
|
280
|
+
# Was this part of the polymorphic thing?
|
281
|
+
#if klass.name == klass.class_name
|
314
282
|
graph.add_edge(fromnodename, tonodename, opts)
|
315
283
|
if a.options[:as]
|
316
284
|
graph.add_edge(tonodename, fromnodename,
|
317
285
|
'arrow' => ARROW_FOR[:belongs_to], 'fontcolor' => 'blue')
|
318
286
|
end
|
319
287
|
standalone = false
|
320
|
-
|
321
|
-
|
322
|
-
|
288
|
+
# elsif options.debug
|
289
|
+
# output << " // !! skipping edge #{fromnodename} -> #{tonodename} #{opts.inspect}\n"
|
290
|
+
# end
|
323
291
|
end
|
324
292
|
graph.add_node(klass.name, %{[color=red, fontcolor=red]}) if standalone
|
325
293
|
end
|
@@ -349,11 +317,25 @@ module ModelGraph
|
|
349
317
|
:label => false,
|
350
318
|
:constraints_first => false)
|
351
319
|
|
320
|
+
# Sure it's a hack, but a special rc file can set or override the
|
321
|
+
# default options. I use this for long "edges=..." lines mostly. If
|
322
|
+
# model_graph was a bit smarter about the implicit layout, then perhaps this
|
323
|
+
# would be unnecessary. However, the DOT documentation also mentions that
|
324
|
+
# this technique of influencing the layout by tweaking the order of nodes
|
325
|
+
# and edges is fragile anyway and may (should!) change in the future.
|
326
|
+
File.open(RC_FILE, 'r') do |rc|
|
327
|
+
for line in rc
|
328
|
+
next if line =~ /\A\s*#/
|
329
|
+
var, value = line.split(/=/, 2)
|
330
|
+
options.send("#{var}=", value)
|
331
|
+
end
|
332
|
+
end if File.exists?(RC_FILE)
|
333
|
+
|
352
334
|
OptionParser.new do |opts|
|
353
335
|
opts.on("-t[=FILE]", "--test[=FILE]", String) do |val|
|
354
|
-
puts "test: #{val}"
|
336
|
+
puts "test: #{val}" if options.debug
|
355
337
|
if val && File.exists?(val)
|
356
|
-
puts "getting #{val}..."
|
338
|
+
puts "getting #{val}..." if options.debug
|
357
339
|
require val
|
358
340
|
options.test = val
|
359
341
|
else
|
@@ -362,13 +344,13 @@ module ModelGraph
|
|
362
344
|
end
|
363
345
|
|
364
346
|
opts.on("--sample=WHICH", String) do |val|
|
365
|
-
puts "sample: #{val}"
|
366
|
-
puts "__FILE__ = #{__FILE__}"
|
347
|
+
puts "sample: #{val}" if options.debug
|
348
|
+
puts "__FILE__ = #{__FILE__}" if options.debug
|
367
349
|
|
368
350
|
sample = File.join(File.dirname(__FILE__), '..', 'examples',
|
369
351
|
File.basename(val, ".rb") + '.rb')
|
370
352
|
if File.exists?(sample)
|
371
|
-
puts "getting #{sample} ..."
|
353
|
+
puts "getting #{sample} ..." if options.debug
|
372
354
|
require sample
|
373
355
|
options.test = sample
|
374
356
|
options.name = File.basename(val, ".rb") if options.name == 'model'
|
@@ -382,21 +364,21 @@ module ModelGraph
|
|
382
364
|
end
|
383
365
|
opts.on("--nodes=NODELIST",
|
384
366
|
"Add named nodes to graph") do |val|
|
385
|
-
puts "nodes: #{val}"
|
367
|
+
puts "nodes: #{val}" if options.debug
|
386
368
|
options.nodes = val
|
387
369
|
end
|
388
370
|
opts.on("--edges=EDGELIST",
|
389
371
|
"Add edges to graph (to affect node rank)") do |val|
|
390
|
-
puts "edges: #{val}"
|
372
|
+
puts "edges: #{val}" if options.debug
|
391
373
|
options.edges = val
|
392
374
|
end
|
393
375
|
opts.on("--name=NAME",
|
394
376
|
"Give the graph an internal name and use tmp/NAME.dot for the output") do |val|
|
395
|
-
puts "name: #{val}"
|
377
|
+
puts "name: #{val}" if options.debug
|
396
378
|
options.name = val
|
397
379
|
end
|
398
380
|
opts.on("--shape=KIND", "override the shape of a node with a valid DOT shape") do |val|
|
399
|
-
puts "shape: #{val}"
|
381
|
+
puts "shape: #{val}" if options.debug
|
400
382
|
options.shape = val
|
401
383
|
end
|
402
384
|
opts.on("--label", "-l", "show edge labels") { |val| options.label = true }
|
@@ -406,11 +388,11 @@ module ModelGraph
|
|
406
388
|
|
407
389
|
end.parse!
|
408
390
|
|
409
|
-
puts options.to_s
|
391
|
+
puts options.to_s if options.debug
|
410
392
|
|
411
393
|
unless options.test
|
412
394
|
for f in Dir.glob(File.join(RAILS_ROOT || '.', "app/models", "*.rb"))
|
413
|
-
puts "getting #{f}..."
|
395
|
+
puts "getting #{f}..." if options.debug
|
414
396
|
require f
|
415
397
|
end
|
416
398
|
else
|
@@ -449,10 +431,11 @@ module ModelGraph
|
|
449
431
|
has_many :selfishes, :foreign_key => :solo_id
|
450
432
|
end
|
451
433
|
SAMPLE
|
452
|
-
puts 'doing the SAMPLE'
|
434
|
+
puts 'doing the SAMPLE' if options.debug
|
453
435
|
end
|
454
436
|
end
|
455
437
|
|
456
438
|
do_graph(options)
|
457
439
|
|
458
440
|
end
|
441
|
+
__END__
|
data/lib/graph.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# An internal class to collect abstract nodes and edges and deliver them
|
2
|
+
# back when needed.
|
3
|
+
class Graph
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
# Holds information about nodes and edges that should be depicted on the
|
7
|
+
# UML-ish graph of the ActiveRecord model classes. The +name+ is optional
|
8
|
+
# and only serves to give the graph an internal name. If you had an
|
9
|
+
# application to combine model graphs from multiple applications, this
|
10
|
+
# might be useful.
|
11
|
+
def initialize(name="model_graph")
|
12
|
+
@name = name
|
13
|
+
@nodes = Hash.new # holds simple strings
|
14
|
+
@edges = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = Hash.new } }
|
15
|
+
|
16
|
+
# A hm B :as => Y gives edge A->B and implies B bt A
|
17
|
+
# C hm B :as => Y gives edge C->B and implies B bt A
|
18
|
+
# B bt Y :polymorphic => true no new information
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create an unattached node in this graph.
|
22
|
+
def add_node(nodename, options="")
|
23
|
+
@nodes[nodename] = options
|
24
|
+
end
|
25
|
+
|
26
|
+
# Iterates over all the nodes previously added to this graph.
|
27
|
+
def nodes # :yields: nodestring
|
28
|
+
@nodes.each do |name,options|
|
29
|
+
yield "#{name} #{options}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a directed edge from one node to another. If an edge between
|
34
|
+
# nodes already exists in the opposite direction, the arrow will be
|
35
|
+
# attached to the other end of the existing edge.
|
36
|
+
def add_edge(fromnode, tonode, options={})
|
37
|
+
unless @edges[tonode].has_key? fromnode
|
38
|
+
options.each do |k,v|
|
39
|
+
@edges[fromnode][tonode][case k.to_s
|
40
|
+
when 'label' : 'taillabel'
|
41
|
+
when 'midlabel' : 'label'
|
42
|
+
when /^arrow(?:head|tail)?$/ : 'arrowhead'
|
43
|
+
else k
|
44
|
+
end] = v
|
45
|
+
end
|
46
|
+
else
|
47
|
+
# reverse sense and overload existing edge
|
48
|
+
options.each do |k,v|
|
49
|
+
@edges[tonode][fromnode][case k.to_s
|
50
|
+
when 'label' : 'headlabel'
|
51
|
+
when 'midlabel' : 'label'
|
52
|
+
when /^arrow(?:head|tail)?$/ : 'arrowtail'
|
53
|
+
else k
|
54
|
+
end] = v
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Iterates over all the DOT formatted edges with nodes having the most
|
60
|
+
# edges first and the edges without a constraint attribute before those
|
61
|
+
# that do.
|
62
|
+
def edges(options={}) # :yields: edgestring
|
63
|
+
@edges.sort { |a,b| b[1].length <=> a[1].length }.each do |(fromnode,nh)|
|
64
|
+
nh.sort_by { |(t,a)| (a.has_key?('constraint') ^ options[:constraints_first]) ? 1 : 0 }.each do |tonode,eh|
|
65
|
+
e = "#{fromnode} -> #{tonode} "
|
66
|
+
e << eh.inspect(options) unless eh.nil?
|
67
|
+
yield e
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/model_graph.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
Dir[File.join(File.dirname(__FILE__), '
|
1
|
+
Dir[File.join(File.dirname(__FILE__), '**/*.rb')].sort.each { |lib| require lib }
|
data/lib/model_graph/version.rb
CHANGED
metadata
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.
|
2
|
+
rubygems_version: 0.9.3
|
3
3
|
specification_version: 1
|
4
4
|
name: model_graph
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.1.
|
7
|
-
date:
|
6
|
+
version: 0.1.3
|
7
|
+
date: 2007-05-23 00:00:00 -04:00
|
8
8
|
summary: "When run from the trunk of a Rails project, produces # {DOT}[http://www.graphviz.org/doc/info/lang.html] output which can be # rendered into a graph by programs such as dot and neato and viewed with # Graphviz (an {Open Source}[http://www.graphviz.org/License.php] viewer)."
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -25,6 +25,7 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
25
25
|
platform: ruby
|
26
26
|
signing_key:
|
27
27
|
cert_chain:
|
28
|
+
post_install_message:
|
28
29
|
authors:
|
29
30
|
- Rob Biedenharn
|
30
31
|
files:
|
@@ -61,6 +62,7 @@ files:
|
|
61
62
|
- doc/files/model_graph_rb.html
|
62
63
|
- test/model_graph_test.rb
|
63
64
|
- test/test_helper.rb
|
65
|
+
- lib/graph.rb
|
64
66
|
- lib/model_graph
|
65
67
|
- lib/model_graph.rb
|
66
68
|
- lib/model_graph/version.rb
|