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