Class: ObjectGraph

Inherits:
Object
  • Object
show all
Defined in:
lib/ograph.rb

Overview

ObjectGraph creates a dot file that represents the connections between your Objects. ObjectGraph knows about Hash, Enumerable, instance variables and ActiveRecord::Base to create the prettiest graphs possible.

Here’s an example that graphs the fixtures of a Rails application. Run ‘rake db:test:prepare` beforehand.

ENV['RAILS_ENV'] = 'test'
require 'config/environment'
require 'ograph'

ar_descendents = []
ObjectSpace.each_object Class do |klass|
  ar_descendents << klass if klass < ActiveRecord::Base
end

File.open 'world.dot', 'w' do |fp|
  graph = ObjectGraph.new

  graph.preprocess_callback do |object|
    next unless ActiveRecord::Base === object

    object.class.reflect_on_all_associations.each do |a|
      object.send a.name, true
    end
  end

  ar_descendents.each do |klass|
    next if klass.abstract_class?
    begin
      klass.find(:all).each do |obj|
        graph.add obj
      end
    rescue => e
      puts "#{klass} has problems:\n\t#{e}"
    end
  end

  fp.write graph
end

Defined Under Namespace

Classes: Node, Pointer

Constant Summary collapse

VERSION =

The version of ObjectGraph you are currently using.

'0.2.0'
ESCAPE =

Characters we need to escape

/([<>"\\])/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ ObjectGraph

Creates a new ObjectGraph instance. Options may include:

:class_filter

Regular expression that matches classes to include.

:show_ivars

Show instance variables (in the record, edges still get drawn.)

:show_nil

Draw nil (and edges to nil.)



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/ograph.rb', line 75

def initialize(opts = {})
  @opts = { :class_filter => //,
            :show_ivars   => true,
            :show_nil     => true,
            :ascendants   => false,
            :descendants  => true,
  }.merge opts

  @nodes         = {}
  @preprocess_callback = nil
end

Instance Attribute Details

#nodesObject

Returns the value of attribute nodes.



44
45
46
# File 'lib/ograph.rb', line 44

def nodes
  @nodes
end

Class Method Details

.graph(target, opts = {}) ⇒ Object

Handy-dandy super-friendly graph generator shortcut.

Just hand it and object and let it rip! Returns a dot graph.



61
62
63
64
65
# File 'lib/ograph.rb', line 61

def self.graph(target, opts = {})
  graph = new opts
  graph.graph target
  graph.to_s
end

Instance Method Details

#add_ascendants(target) ⇒ Object

Adds target plus ascendants to graph



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/ograph.rb', line 135

def add_ascendants(target)
  ascendants = []
  stack = [target]

  while stack.length > 0
    o_target = stack.pop
    ascendants << o_target

    GC.start
    ObjectSpace.each_object do |object|
      next if @nodes.key? object.object_id
      next if object.equal? o_target
      next if object.equal? ascendants
      next if object.equal? stack
      next if ascendants.any? { |x| x.equal? object }

      case object
      when IO, String, ARGF then
        # Do nothing
      when Hash then
        object.each do |k,v|
          if k.equal?(o_target) || v.equal?(o_target)
            stack << object
            break
          end
        end
      when Enumerable then
        object.each do |v|
          if v.equal?(o_target)
            stack << object
            break
          end
        end
      end

      if object.instance_variables.length > 0
        stack << object if object.instance_variables.any? do |iv_sym|
          object.instance_variable_get(iv_sym).equal?(o_target)
        end
      end
    end
  end

  add_object_to_graph(target, { :style => 'dashed' })
  (ascendants - [target]).each { |x| add_object_to_graph(x) }
  ascendants.clear
  stack.clear
end

#add_descendants(target) ⇒ Object

Adds target to the graph, descending in to the target



187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/ograph.rb', line 187

def add_descendants(target)
  stack = add_object_to_graph(target, { :style => 'dashed' })

  while stack.length > 0
    object = stack.pop
    next if @nodes.key? object.object_id
    @preprocess_callback.call object if @preprocess_callback

    stack += add_object_to_graph(object, {:style => 'filled'})

  end
end

#add_ivars(record, object) ⇒ Object

Adds ivars for object to the graph.



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/ograph.rb', line 266

def add_ivars(record, object)
  return unless object.instance_variables # HACK WTF Rails?
  stack = []

  ivars = object.instance_variables.sort

  if defined? ActiveRecord::Base && ActiveRecord::Base === object then
    ivars.delete '@attributes'
    attrs = object.instance_variable_get :@attributes

    unless attrs.nil? then
      attrs.each_with_index do |(k,v),i|
        next if v.nil? && ! @opts[:show_nil]
        next unless v.class.to_s =~ @opts[:class_filter]

        stack += add_to_record(record, k, v)
      end
    end
  end

  ivars.each do |iv_sym|
    iv = object.instance_variable_get iv_sym
    next if iv.nil? && ! @opts[:show_nil]
    if iv.class.to_s =~ @opts[:class_filter] ||
      (object.is_a?(Enumerable) && !object.is_a?(String))

      obj_name = object_name(iv)
      record.pointers["#{iv_sym}"] << Pointer.new(obj_name, iv.object_id)
      record.values.push("#{iv_sym}")
      stack.push(iv)
    end
  end

  stack
end

#add_object_to_graph(object, opts = {}) ⇒ Object

Adds object to tree



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/ograph.rb', line 203

def add_object_to_graph(object, opts = {})
  record = Node.new(object.object_id, object_name(object))
  record.options = opts

  stack = []
  case object
  when String then
    str = object[0..15].gsub(ESCAPE, '\\\\\1').gsub(/[\r\n]/,' ')
    str << '...' if object.length > 15
    record.values << "'#{str}'"
  when Numeric then
    record.values << object
  when IO then
  when Hash then
    if object.respond_to? :empty? and object.empty? then
      record.values << 'Empty!'
    else
      object.each_with_index do |(k,v),i|
        next if v.nil? && ! @opts[:show_nil]
        next unless v.class.to_s =~ @opts[:class_filter]

        stack += add_to_record(record, k, v)
      end
    end
  when Enumerable then
    if object.respond_to? :empty? and object.empty? then
      record.values.push 'Empty!'
    else
      object.each_with_index do |v, i|
        next if v.nil? && ! @opts[:show_nil]
        if v.class.to_s =~ @opts[:class_filter] or
           (v.is_a? Enumerable and not v.is_a? String) then
          stack += add_to_record(record, i, v)
        end
      end
    end
  end

  # This is a HACK around bug 1345 (I think)
  if object.class and object.class.ancestors.include? Array and
     not Enumerable === object then
    if object.respond_to? :empty? and object.empty? then
      record.values.push 'Empty!'
    else
      object.each_with_index do |v, i|
        next if v.nil? && ! @opts[:show_nil]
        if v.class.to_s =~ @opts[:class_filter] or
           (v.is_a? Enumerable and not v.is_a? String) then
          stack += add_to_record(record, i, v)
        end
      end
    end
  end

  stack += add_ivars(record, object)

  @nodes[record.node_id] = record
  stack
end

#add_to_record(record, key, value) ⇒ Object

Adds key and value to record



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/ograph.rb', line 305

def add_to_record(record, key, value)
  stack = []
  record.values.push(
    [key, value].map { |val|
      case val
      when NilClass
        'nil'
      when Fixnum
        "#{val}"
      when String, Symbol
        val = ":#{val}" if val.is_a? Symbol
        string_val = val[0..12].gsub(ESCAPE, '\\\\\1').gsub(/[\r\n]/, ' ')
        string_val << '...' if val.length > 12
        string_val
      else
        string_val = object_name val
        record.pointers[string_val] << Pointer.new(string_val, val.object_id)
        stack.push val
        string_val
      end
    }.join(' is '))
  stack
end

#diff(target) ⇒ Object

Combines target with the current graph and highlights differences



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/ograph.rb', line 103

def diff(target)
  old_nodes = @nodes
  @nodes = {}
  graph(target)
  new_nodes   = @nodes.keys - old_nodes.keys
  lost_nodes  = old_nodes.keys - @nodes.keys

  (@nodes.keys & old_nodes.keys).each do |object_id|
    old = old_nodes[object_id].pointers.values
    new = @nodes[object_id].pointers.values

    (old - new).flatten.each do |lost_link|
      lost_link.options = { :color => 'red' }
      @nodes[object_id].lost_links << lost_link
    end
  end

  # Color the lost nodes and add them to the graph
  lost_nodes.each do |object_id|
    old_nodes[object_id].options['color'] = 'hotpink2'
    @nodes[object_id] = old_nodes[object_id]
  end

  # Color the new nodes
  new_nodes.each do |object_id|
    @nodes[object_id].options['color'] = 'yellowgreen'
  end
end

#graph(target) ⇒ Object

Adds target to the graph



89
90
91
92
93
94
95
96
97
98
# File 'lib/ograph.rb', line 89

def graph(target)
  GC.start
  add_ascendants(target)  if @opts[:ascendants]
  add_descendants(target) if @opts[:descendants]
  if block_given?
    yield target
    GC.start
    diff(target)
  end
end

#object_name(object) ⇒ Object

The name of an Object. Understands ActiveRecord::Base objects.



332
333
334
335
336
337
338
339
# File 'lib/ograph.rb', line 332

def object_name(object)
  id = if defined? ActiveRecord::Base and ActiveRecord::Base === object then
         object.id
       else
         object.object_id
       end
  "#{id}-#{object.class}"
end

#preprocess_callback(&block) ⇒ Object

Stores a callback for #add to call on each object it processes before adding to the graph.

To add all the associations of an ActiveRecord::Base object:

graph.preprocess_callback do |object|
  next unless ActiveRecord::Base === object

  object.class.reflect_on_all_associations.each do |a|
    object.send a.name, true
  end
end


355
356
357
# File 'lib/ograph.rb', line 355

def preprocess_callback(&block)
  @preprocess_callback = block
end

#to_sObject

Returns dot for the objects added to the graph. Use GraphViz to turn the graph into a PNG (or whatever).



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/ograph.rb', line 363

def to_s
  s = <<END
digraph g {
\tgraph [ rankdir = "LR" ];
\tnode [ fontsize = "8"
\t\tshape = "ellipse"
\t];
\tedge [ ];

END

  @nodes.values.sort.each do |node|
    s << "\"#{node.name}\" [\n\tlabel="
    list = []
    list << "<f0>#{node.name}"
    node.values.each_with_index do |value,i|
      list << "<f#{i + 1}>#{value}"
    end
    s << "\"#{list.join('|')}\"\n"
    node.options.each do |k,v|
      s << "#{k}=#{v}\n"
    end
    s << "\tshape = \"record\"\n]\n\n"
  end

  @nodes.values.each do |node|
    node.pointers.each_with_index do |(from,pointer),i|
      pointer.each do |p|
        to = @nodes[p.object_id]
        next unless to
        s<< "\"#{node.name}\":f#{i + 1} -> \"#{p.name}\":f0 [ id = #{i}\n"
        p.options.each do |k,v|
          s << "#{k}=#{v}\n"
        end
        s << "]\n"
      end
    end
    node.lost_links.each do |p|
      s<< "\"#{node.name}\":f0 -> \"#{p.name}\":f0 [\n"
      p.options.each do |k,v|
        s << "#{k}=#{v}\n"
      end
      s << "]\n"
    end
  end

  s << "}\n"

  s
end