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.
@@ -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.rb [options]
26
+ # model_graph [options]
26
27
  #
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).
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/; out << sep << "#{k}=#{v}"; sep=', '; end }
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
- # 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
- # 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.rb --edges=Author-Book
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.rb --nodes=Author
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+)-(\w+)/?}) do |f,t|
248
- graph.add_edge(f, t, 'style' => 'solid')
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
- next unless a.class_name == a.name.to_s.camelize.singularize
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
- if klass.name == klass.class_name
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
- elsif options.debug
321
- output << " // !! skipping edge #{fromnodename} -> #{tonodename} #{opts.inspect}\n"
322
- end
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__
@@ -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
@@ -1 +1 @@
1
- Dir[File.join(File.dirname(__FILE__), 'model_graph/**/*.rb')].sort.each { |lib| require lib }
1
+ Dir[File.join(File.dirname(__FILE__), '**/*.rb')].sort.each { |lib| require lib }
@@ -2,7 +2,7 @@ module ModelGraph
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- TINY = 2
5
+ TINY = 3
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.11
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.2
7
- date: 2006-11-22 00:00:00 -05:00
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