model_graph 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|