mini_gauge 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +6 -0
- data/README.rdoc +40 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/lib/mini_gauge.rb +370 -0
- data/lib/tasks/mini_gauge.rake +123 -0
- data/test/helper.rb +10 -0
- data/test/test_mini_gauge.rb +7 -0
- metadata +91 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 ASEE
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
This gem includes rake tasks designed to be run from within a rails application. To enable them, create or find an existing
|
2
|
+
rakefile from your project and add the following line:
|
3
|
+
|
4
|
+
Dir["#{Gem.searcher.find('mini_gauge').full_gem_path}/lib/tasks/*.rake"].each { |ext| load ext }
|
5
|
+
|
6
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
= mini_gauge
|
2
|
+
|
3
|
+
Mini Gauge is a project based off of railroad to provide nice Graphviz documentation for a Ruby on Rails project. It was created to solve a few internal concerns with railroad, namely:
|
4
|
+
|
5
|
+
* It loads within the project environment, so any customizations or models available in the environment are available for documentation
|
6
|
+
* It comes with support for defining which models and relations appear, instead of everything on one graph
|
7
|
+
* It supports instances of objects becoming graphviz objects, with data included
|
8
|
+
|
9
|
+
When documenting a large project with 134 models and counting, railroad was useful but too large. Additionally, we needed to show both the relations between the classes and what specific instances looked like before and after an operation.
|
10
|
+
|
11
|
+
MiniGauge is loaded within the application and extends ActiveRecord to build a Graphviz dot format document, and to append an instance or class and relations to the document. It comes with a sample rakefile to show how to build documentation on a per-class basis, but can be expanded to document extended relations or instances.
|
12
|
+
|
13
|
+
This gem includes rake tasks designed to be run from within a rails application. To enable them, create or find an existing
|
14
|
+
rakefile from your project and add the following line:
|
15
|
+
|
16
|
+
Dir["#{Gem.searcher.find('mini_gauge').full_gem_path}/lib/tasks/*.rake"].each { |ext| load ext }
|
17
|
+
|
18
|
+
Then from the console run
|
19
|
+
|
20
|
+
doc:mini_gauge:graph
|
21
|
+
|
22
|
+
to produce a set of dot source files and graphs in doc/graphs
|
23
|
+
|
24
|
+
Mini gauge is currently in use internally as a library module, this gem is currently under development as a port of the internal library.
|
25
|
+
|
26
|
+
See more about Railroad at http://railroad.rubyforge.org/
|
27
|
+
|
28
|
+
== Note on Patches/Pull Requests
|
29
|
+
|
30
|
+
* Fork the project.
|
31
|
+
* Make your feature addition or bug fix.
|
32
|
+
* Add tests for it. This is important so I don't break it in a
|
33
|
+
future version unintentionally.
|
34
|
+
* Commit, do not mess with rakefile, version, or history.
|
35
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
36
|
+
* Send me a pull request. Bonus points for topic branches.
|
37
|
+
|
38
|
+
== Copyright
|
39
|
+
|
40
|
+
Copyright (c) 2010 ASEE. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "mini_gauge"
|
8
|
+
gem.summary = %Q{s one-line summary of your gem}
|
9
|
+
gem.description = %Q{ longer description of your gem}
|
10
|
+
gem.email = "james@mabonus.net"
|
11
|
+
gem.homepage = "http://github.com/asee/mini_gauge"
|
12
|
+
gem.authors = ["James Prior"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
gem.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*', 'lib/tasks/*.rake'].to_a
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
24
|
+
test.libs << 'lib' << 'test'
|
25
|
+
test.pattern = 'test/**/test_*.rb'
|
26
|
+
test.verbose = true
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'rcov/rcovtask'
|
31
|
+
Rcov::RcovTask.new do |test|
|
32
|
+
test.libs << 'test'
|
33
|
+
test.pattern = 'test/**/test_*.rb'
|
34
|
+
test.verbose = true
|
35
|
+
end
|
36
|
+
rescue LoadError
|
37
|
+
task :rcov do
|
38
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :test => :check_dependencies
|
43
|
+
|
44
|
+
task :default => :test
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "mini_gauge #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.0
|
data/lib/mini_gauge.rb
ADDED
@@ -0,0 +1,370 @@
|
|
1
|
+
# === About this module ===
|
2
|
+
#
|
3
|
+
# This module is designed to make it easy to visualize complex data. It does this by generating Graphviz files using the dot syntax.
|
4
|
+
#
|
5
|
+
# It is not designed for everyday use however, so to use it you must first call:
|
6
|
+
# MiniGauge.enable!
|
7
|
+
#
|
8
|
+
# It adds a few methods to instance objects of ActiveRecord, the most useful is to_dot_notation, which will accept a list of
|
9
|
+
# includes similar to the parameters for a find() operation.
|
10
|
+
#
|
11
|
+
# Eg:
|
12
|
+
# MemberProduct.last.to_dot_notation(:include => [:product, {:invoice_item => :invoice}])
|
13
|
+
#
|
14
|
+
# will return something like:
|
15
|
+
#
|
16
|
+
# digraph MemberProduct_131299 {
|
17
|
+
# graph[overlap=false, splines=true]
|
18
|
+
# "MemberProduct_131299" [shape=Mrecord, label="{MemberProduct_131299|id: 131299\lmembership_id: 46857\lproduct_id: 3\lproduct_type: Product \l}" ]
|
19
|
+
# "Donation_3" [shape=Mrecord, label="{Donation_3|id: 3\lname: Donation\lposition: 3\lprice_in_cents: 0\lrevenue_account_id: 3\ltype: Donation \l}" ]
|
20
|
+
# "InvoiceItem_567108" [shape=Mrecord, label="{InvoiceItem_567108|discount_in_cents: 0\lgross_price_in_cents: 0\lid: 567108\linvoice_id: 261641\lnet_price_in_cents: 0\lprice_in_cents: 0\lproduct_id: 131299\lproduct_type: MemberProduct\lquantity: 1 }" ]
|
21
|
+
# "Invoice_261641" [shape=Mrecord, label="{Invoice_261641|id: 261641\linvoiceable_id: 405\linvoiceable_type: Person\lnumber: 261641\lproforma: true }" ]
|
22
|
+
#
|
23
|
+
#
|
24
|
+
# "Donation_3" -> "MemberProduct_131299"
|
25
|
+
# "Invoice_261641" -> "InvoiceItem_567108"
|
26
|
+
# "InvoiceItem_567108" -> "MemberProduct_131299"
|
27
|
+
# }
|
28
|
+
#
|
29
|
+
# which can then turned into a graph.
|
30
|
+
#
|
31
|
+
# Or, to get a complete set of memberships and member products
|
32
|
+
|
33
|
+
# doc = Organization.find(400).to_dot_notation(:include => {
|
34
|
+
# :memberships => [
|
35
|
+
# {:invoice_item => :invoice},
|
36
|
+
# {:member_products => [
|
37
|
+
# :product,
|
38
|
+
# {:invoice_item => :invoice}
|
39
|
+
# ]
|
40
|
+
# }
|
41
|
+
# ]
|
42
|
+
# })
|
43
|
+
#
|
44
|
+
# File.open("big_org.dot", 'w') {|f| f.write(doc) }
|
45
|
+
#
|
46
|
+
# to_dot_notation also accepts a block and passes the graph object to it, in case there are extra details to add.
|
47
|
+
# For example, to only do some of the member products try this:
|
48
|
+
# @org_membership = Membership::Organizational::Base.find(400)
|
49
|
+
# doc = @org_membership.to_dot_notation(:include => {:member => :people}) do |graph|
|
50
|
+
# @org_membership.member_products.contact_reps.each do |cr_mpr|
|
51
|
+
# graph.add(:source => @org_membership, :destination => cr_mpr, :label => "member_products.contact_reps")
|
52
|
+
# cr_mpr.fill_dot_graph(graph, :include => [{:product => :member}, {:invoice_item => :invoice}])
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Classes can also be exported to dot notation, where the attributes and active record relations
|
57
|
+
# will be displayed
|
58
|
+
#
|
59
|
+
# They work the same way, at the class level:
|
60
|
+
# doc = Membership::Organizational::Base.to_dot_notation
|
61
|
+
#
|
62
|
+
# By default only one level of relations is fetched, but if more are desired to_dot_notation will also accept a block:
|
63
|
+
#
|
64
|
+
# doc = Membership::Organizational::Base.to_dot_notation do |graph|
|
65
|
+
# Invoice.fill_with_relations(graph)
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
#
|
69
|
+
module MiniGauge
|
70
|
+
|
71
|
+
HIDDEN_FIELDS = [ "created_at", "created_on", "updated_at", "updated_on",
|
72
|
+
"lock_version", "type", "id", "position", "parent_id", "lft",
|
73
|
+
"rgt", "quote", "template", "salt", "persistence_token", "crypted_password", "current_login_at"]
|
74
|
+
|
75
|
+
# Include this module in AR Base
|
76
|
+
def self.enable!
|
77
|
+
ActiveRecord::Base.send(:include, MiniGauge)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.included(base)
|
81
|
+
base.send :include, InstanceMethods
|
82
|
+
base.extend ClassMethods
|
83
|
+
end
|
84
|
+
|
85
|
+
# From Railroad, an object to hold our nodes and edges,
|
86
|
+
# also to export them on demand
|
87
|
+
class Graph
|
88
|
+
attr_accessor :graph_type, :show_label, :nodes, :edges, :title, :description
|
89
|
+
|
90
|
+
def initialize(opts = {})
|
91
|
+
@graph_type = opts[:graph_type] || 'Model'
|
92
|
+
@show_label = opts[:show_label] || true
|
93
|
+
@title = opts[:title] || "#{@graph_type} diagram"
|
94
|
+
@description = opts[:description] || ''
|
95
|
+
@nodes = []
|
96
|
+
@edges = []
|
97
|
+
|
98
|
+
# Designed to work with the other class and instance methods on AR objects
|
99
|
+
# to make adding nodes and edges easier. Allows you to do something like
|
100
|
+
# @graph.nodes << Invoice.first
|
101
|
+
@nodes.instance_eval(%Q(
|
102
|
+
def <<(item)
|
103
|
+
if item.respond_to?(:dot_node_definition)
|
104
|
+
super(item.dot_node_definition)
|
105
|
+
else
|
106
|
+
super(item)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
))
|
110
|
+
|
111
|
+
# Makes building graphs easier, allows you to do something like
|
112
|
+
# @graph.edges << {:source => Person.first, :destination => Person.first.organization}
|
113
|
+
@edges.instance_eval(%Q(
|
114
|
+
def <<(item)
|
115
|
+
if item.is_a? Hash
|
116
|
+
if item[:source] && item[:source].respond_to?(:dot_node_name)
|
117
|
+
item[:source] = item[:source].dot_node_name
|
118
|
+
end
|
119
|
+
|
120
|
+
if item[:destination] && item[:destination].respond_to?(:dot_node_name)
|
121
|
+
item[:destination] = item[:destination].dot_node_name
|
122
|
+
end
|
123
|
+
end
|
124
|
+
super(item)
|
125
|
+
end
|
126
|
+
))
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
# Add an item to the graph, expects a source node and destination node or a label if there is no destination
|
131
|
+
def add(opts)
|
132
|
+
raise ArgumentError.new("Must supply :source and :destination") unless opts.is_a?(Hash) && opts.keys.include?(:source) && opts.keys.include?(:destination)
|
133
|
+
raise ArgumentError.new("Cannot supply a nil :destination without a label") if opts[:destination].nil? && opts[:label].nil?
|
134
|
+
|
135
|
+
if opts[:destination].nil?
|
136
|
+
node_name = "#{opts[:label].underscore}_#{opts.object_id}"
|
137
|
+
@nodes << self.nil_node_definition(:label => opts[:label], :name => node_name)
|
138
|
+
@edges << {:destination => node_name, :source => opts[:source], :empty_rec => true}
|
139
|
+
else
|
140
|
+
@nodes<<opts[:source] unless @nodes.any?{|x| opts[:source].dot_node_name == x[:name] }
|
141
|
+
@nodes<<opts[:destination] unless @nodes.any?{|x| opts[:destination].dot_node_name == x[:name] }
|
142
|
+
@edges<<opts
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns a node definition for an empty node
|
147
|
+
def nil_node_definition(opts)
|
148
|
+
raise ArgumentError.new("Must supply at least :name or :label") unless opts.is_a?(Hash) && (opts.keys.include?(:name) || opts.keys.include?(:label))
|
149
|
+
|
150
|
+
{:name => (opts[:name] || opts[:label].underscore), :label => (opts[:label] || opts[:name].humanize.titleize), :attributes => ["none"], :options => {:color => "gray61"}}
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns a string representing the DOT graph
|
154
|
+
def to_dot_notation
|
155
|
+
header = "digraph #{@graph_type.downcase}_diagram {\n" +
|
156
|
+
"\tgraph[overlap=false, splines=true]\n"
|
157
|
+
header += dot_label if @show_label
|
158
|
+
|
159
|
+
uniq_nodes = @nodes.inject([]) { |result,h| result << h unless result.include?(h); result }
|
160
|
+
uniq_edges = @edges.inject([]) { |result,h| result << h unless result.include?(h); result }
|
161
|
+
|
162
|
+
return header +
|
163
|
+
uniq_nodes.map{|node| format_node(node)}.to_s + "\n" +
|
164
|
+
uniq_edges.map{|edge| format_edge(edge)}.to_s +
|
165
|
+
"}\n"
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
# Build diagram label
|
170
|
+
def dot_label
|
171
|
+
return "\t_diagram_info [shape=\"plaintext\", " +
|
172
|
+
"label=\"#{@title} \\l" +
|
173
|
+
"Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l" +
|
174
|
+
"Migration version: " +
|
175
|
+
"#{ActiveRecord::Migrator.current_version}\\l" +
|
176
|
+
"Description: #{@description}\\l" +
|
177
|
+
"\\l\", fontsize=14]\n"
|
178
|
+
end
|
179
|
+
|
180
|
+
# Take a node (hash) and format it into dot notation
|
181
|
+
def format_node(node)
|
182
|
+
node[:options] ||= {}
|
183
|
+
node[:options][:shape] = "Mrecord"
|
184
|
+
node[:options][:label] = %Q({#{(node[:label] || node[:name])} | #{node[:attributes].join('\l')} \\l} )
|
185
|
+
opts = node[:options].collect{|k,v| %Q(#{k}="#{v}") }.join(", ")
|
186
|
+
return %Q(\t"#{node[:name]}" [#{opts}]\n)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Take an edge (hash) and format it into dot notation
|
190
|
+
def format_edge(edge)
|
191
|
+
edge[:options] ||= {}
|
192
|
+
edge[:options][:label] = edge[:label] unless edge[:label].blank?
|
193
|
+
edge[:options].merge!({:style => "dotted", :color => "gray61"}) if edge[:empty_rec]
|
194
|
+
case edge[:type]
|
195
|
+
when 'one-one'
|
196
|
+
edge[:options].merge!({:arrowtail => "odot", :arrowhead => "dot", :dir => "both"})
|
197
|
+
when 'one-many'
|
198
|
+
edge[:options].merge!({:arrowtail => "crow", :arrowhead => "dot", :dir => "both"})
|
199
|
+
when 'many-many'
|
200
|
+
edge[:options].merge!({:arrowtail => "crow", :arrowhead => "crow", :dir => "both"})
|
201
|
+
when 'is-a'
|
202
|
+
edge[:options].merge!({:arrowtail => "onormal", :arrowhead => "none"})
|
203
|
+
end
|
204
|
+
opts = edge[:options].collect{|k,v| %Q(#{k}="#{v}") }.join(", ")
|
205
|
+
return %Q( "#{edge[:source]}" -> "#{edge[:destination]}" [#{opts}]\n )
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
module ClassMethods
|
214
|
+
|
215
|
+
# Returns the name of this for graph nodes
|
216
|
+
def dot_node_name
|
217
|
+
self.name
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns the attributes of this node for dot graphs
|
221
|
+
def dot_node_attributes
|
222
|
+
if self.table_exists?
|
223
|
+
hidden_fields = MiniGauge::HIDDEN_FIELDS << "#{self.table_name}_count"
|
224
|
+
|
225
|
+
return self.content_columns.reject{|x| hidden_fields.include?(x.name)}.collect{ |col|
|
226
|
+
"#{col.name} :#{col.type.to_s}"
|
227
|
+
}
|
228
|
+
else
|
229
|
+
return ["Table doesn't exist"]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Accepts a Graph object and fills it with the relations for this object
|
234
|
+
def fill_with_relations(dot_graph)
|
235
|
+
self.reflect_on_all_associations.each do |assoc|
|
236
|
+
|
237
|
+
if assoc.class_name == assoc.name.to_s.singularize.camelize
|
238
|
+
assoc_name = ''
|
239
|
+
else
|
240
|
+
assoc_name = assoc.name.to_s
|
241
|
+
end
|
242
|
+
|
243
|
+
if assoc.macro.to_s == 'has_one' || assoc.macro.to_s == 'belongs_to'
|
244
|
+
assoc_type = 'one-one'
|
245
|
+
elsif assoc.macro.to_s == 'has_many' && (! assoc.options[:through])
|
246
|
+
assoc_type = 'one-many'
|
247
|
+
else # habtm or has_many, :through
|
248
|
+
assoc_type = 'many-many'
|
249
|
+
end
|
250
|
+
|
251
|
+
if assoc.options[:polymorphic]
|
252
|
+
dot_graph.nodes << {:name => assoc.class_name, :attributes => ["polymorphic record"], :options => {:color => "gray61"}}
|
253
|
+
dot_graph.edges << {:empty_rec => true, :source => self.dot_node_name, :destination => assoc.class_name, :type => assoc_type, :label => assoc_name }
|
254
|
+
else
|
255
|
+
dot_graph.nodes << {:name => assoc.klass.dot_node_name, :attributes => assoc.klass.dot_node_attributes}
|
256
|
+
dot_graph.edges << {:source => self.dot_node_name, :destination => assoc.klass.dot_node_name, :type => assoc_type, :label => assoc_name }
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Creats a dot node graph and fills it with the relations for this object. Returns a string representing the graph.
|
263
|
+
# Will accept a block which passes the Graph object in case there are other relations to add.
|
264
|
+
def to_dot_notation(opts = {})
|
265
|
+
@graph = MiniGauge::Graph.new(opts)
|
266
|
+
|
267
|
+
self.add_to_graph(@graph)
|
268
|
+
|
269
|
+
yield(@graph) if block_given?
|
270
|
+
|
271
|
+
return @graph.to_dot_notation
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
def add_to_graph(graph)
|
276
|
+
graph.nodes << {:name => self.dot_node_name, :attributes => self.dot_node_attributes}
|
277
|
+
self.fill_with_relations(graph)
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
module InstanceMethods
|
283
|
+
|
284
|
+
# Returns a node name for the current object, used when creating relations and in definitions
|
285
|
+
def dot_node_name
|
286
|
+
"#{self.class.name.to_s}_#{self.id || self.object_id}"
|
287
|
+
end
|
288
|
+
|
289
|
+
# Returns a string used as the node description in the graph by inspecting the instanece attributes
|
290
|
+
def dot_node_attributes
|
291
|
+
|
292
|
+
hidden_fields = MiniGauge::HIDDEN_FIELDS << "#{self.class.table_name}_count"
|
293
|
+
|
294
|
+
return self.attributes.reject{|k,v| hidden_fields.include?(k) || v.nil? }.collect{ |k,v|
|
295
|
+
"#{k}: #{v.to_s.gsub(/['"]/,'')}"
|
296
|
+
}
|
297
|
+
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns a node definition for the current object. For example:
|
301
|
+
def dot_node_definition
|
302
|
+
{:name => self.dot_node_name, :label => self.dot_node_name, :attributes => self.dot_node_attributes }
|
303
|
+
end
|
304
|
+
|
305
|
+
# Optionally pass a block to manually fill in details.
|
306
|
+
#
|
307
|
+
# @org_membership.to_dot_notation(:include => {:member => :people}, :title => "An organization with a transferred Contact Representative", :description => "Organizations can transfer Contact Representative memberships to other people. This graph shows what the data looks like after a transfer" ) do |graph|
|
308
|
+
# @org_membership.member_products.contact_reps.each do |cr_mpr|
|
309
|
+
# graph.add(:source => @org_membership, :destination => cr_mpr, :label => "member_products.contact_reps")
|
310
|
+
# cr_mpr.fill_dot_graph(graph, :include => [{:product => :member}, {:invoice_item => :invoice}])
|
311
|
+
# end
|
312
|
+
# end
|
313
|
+
def to_dot_notation(opts = {})
|
314
|
+
@graph = MiniGauge::Graph.new(opts)
|
315
|
+
|
316
|
+
self.fill_dot_graph(@graph, opts)
|
317
|
+
|
318
|
+
yield(@graph) if block_given?
|
319
|
+
|
320
|
+
return @graph.to_dot_notation
|
321
|
+
|
322
|
+
end
|
323
|
+
|
324
|
+
# Takes a graph and fills in the graph given the options by inspecting the relations
|
325
|
+
def fill_dot_graph(dot_graph, opts={})
|
326
|
+
|
327
|
+
dot_graph.nodes << self.dot_node_definition
|
328
|
+
|
329
|
+
# Set up to allow something like this:
|
330
|
+
# :include => [:product, {:invoice_item => :invoice}]
|
331
|
+
# ie, about the same as ar associations
|
332
|
+
opts[:include] = Array(opts[:include]) unless opts[:include].respond_to?(:each)
|
333
|
+
opts[:include].each do |relation, relation_opts|
|
334
|
+
if relation.is_a?(Hash) || relation.is_a?(Array)
|
335
|
+
relation.each do |name, sub_opts|
|
336
|
+
# We do the funny bits with data here because calling Array() on it makes AR complain
|
337
|
+
data = self.send(name.to_sym)
|
338
|
+
data = [data] unless data.respond_to?(:each)
|
339
|
+
data = [nil] if data.empty? #make sure we have something to show for this leg of the graph
|
340
|
+
data.each do |obj|
|
341
|
+
if obj.nil? # It was in the options, but there is nothing found for it, eg self.product returns nil
|
342
|
+
dot_graph.add(:destination => obj, :source => self, :label => name.to_s)
|
343
|
+
else
|
344
|
+
# Here is where we recurse, rolling everything into our list of nodes and edges.
|
345
|
+
obj.fill_dot_graph(dot_graph, :include => sub_opts)
|
346
|
+
dot_graph.edges << {:destination => obj.dot_node_name, :source => dot_node_name, :label => name.to_s.humanize}
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
else
|
351
|
+
data = self.send(relation.to_sym)
|
352
|
+
data = [data] unless data.respond_to?(:each)
|
353
|
+
data = [nil] if data.empty? #make sure we have something to show for this leg of the graph
|
354
|
+
data.each do |obj|
|
355
|
+
if relation_opts #more recursion here
|
356
|
+
obj.fill_dot_graph(dot_graph, :include => relation_opts)
|
357
|
+
dot_graph.edges << {:destination => obj.dot_node_name, :source => dot_node_name, :label => relation.to_s.humanize}
|
358
|
+
else
|
359
|
+
dot_graph.add(:destination => obj, :source => self, :label => relation.to_s.humanize)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
end
|
369
|
+
|
370
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# Make this file available in your environment! Copy it or add the following to a rakefile:
|
2
|
+
#
|
3
|
+
# Dir["#{Gem.searcher.find('mini_gauge').full_gem_path}/lib/tasks/*.rake"].each { |ext| load ext }
|
4
|
+
#
|
5
|
+
|
6
|
+
namespace :doc do
|
7
|
+
|
8
|
+
namespace :mini_gauge do
|
9
|
+
|
10
|
+
desc "Create graphs of instances of classes and the example relations"
|
11
|
+
task :graph do
|
12
|
+
%w(doc:mini_gauge:environment_for_graph db:test:load doc:mini_gauge:clobber
|
13
|
+
doc:mini_gauge:generate_class_sources doc:mini_gauge:generate_complete_graph
|
14
|
+
doc:mini_gauge:build_pdfs).collect do |task|
|
15
|
+
|
16
|
+
Rake::Task[task].invoke
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
#The locations of where we place our files
|
22
|
+
CLASS_ROOT = File.join(RAILS_ROOT, "doc", "graphs", "dot_sources", "classes") #Where the output plaintext goes for classes
|
23
|
+
MODEL_ROOT = File.join(RAILS_ROOT, "doc", "graphs", "dot_sources", "models") #Where the output plaintext goes for models (ie, the ones with data)
|
24
|
+
GRAPH_OUTPUT_ROOT = File.join(RAILS_ROOT, "doc", "graphs") # Where the png/pdf/whatever graphs go
|
25
|
+
|
26
|
+
# A helper to save a model. Models will include several instances of classes with data and demonstrate some relation
|
27
|
+
def saving_model(name, subfolders = [])
|
28
|
+
output_dir = File.join(MODEL_ROOT, *subfolders)
|
29
|
+
FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
|
30
|
+
|
31
|
+
location = File.join(output_dir, name)
|
32
|
+
File.open(location, "w") {|f| f.write( yield ) }
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
#Classes just inspect the class itself and it's relation, no data is included
|
37
|
+
def saving_class(name, subfolders = [])
|
38
|
+
output_dir = File.join(CLASS_ROOT, *subfolders)
|
39
|
+
FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
|
40
|
+
|
41
|
+
location = File.join(output_dir, name)
|
42
|
+
File.open(location, "w") {|f| f.write( yield ) }
|
43
|
+
end
|
44
|
+
|
45
|
+
task :environment_for_graph do
|
46
|
+
if Rails && Rails.initialized? && RAILS_ENV != "test"
|
47
|
+
raise "Rails environment already set to #{ENV["RAILS_ENV"] || RAILS_ENV}, graphs expect to be generated in test environment"
|
48
|
+
end
|
49
|
+
|
50
|
+
ENV["RAILS_ENV"] = "test"
|
51
|
+
RAILS_ENV = "test"
|
52
|
+
|
53
|
+
Rake::Task['environment'].invoke
|
54
|
+
|
55
|
+
MiniGauge.enable!
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Remove the graph sources destination folders"
|
59
|
+
task :clobber_sources do
|
60
|
+
FileUtils.rm(Dir.glob(File.join(CLASS_ROOT,"**","*.dot")))
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Remove the graph sources destination folders"
|
64
|
+
task :clobber_output do
|
65
|
+
FileUtils.rm(Dir.glob(File.join(GRAPH_OUTPUT_ROOT,"**","*.pdf")))
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "Remove all graphs and sources"
|
69
|
+
task :clobber do
|
70
|
+
Rake::Task["doc:mini_gauge:clobber_sources"].invoke
|
71
|
+
Rake::Task["doc:mini_gauge:clobber_output"].invoke
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
task :build_pdfs do
|
76
|
+
raise "Cannot create graphs because the 'dot' command is not found" unless system("which dot")
|
77
|
+
|
78
|
+
Dir.glob(File.join(MODEL_ROOT,"**","*.dot")).each do |filename|
|
79
|
+
destination = File.join(GRAPH_OUTPUT_ROOT, "models", filename.gsub(MODEL_ROOT,"").gsub(/\.dot$/,".pdf"))
|
80
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
81
|
+
system("dot -Tpdf -o'#{destination}' '#{filename}'")
|
82
|
+
end
|
83
|
+
|
84
|
+
Dir.glob(File.join(CLASS_ROOT,"**","*.dot")).each do |filename|
|
85
|
+
destination = File.join(GRAPH_OUTPUT_ROOT, "classes", filename.gsub(CLASS_ROOT,"").gsub(/\.dot$/,".pdf"))
|
86
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
87
|
+
system("dot -Kcirco -Tpdf -o'#{destination}' '#{filename}'")
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
task :generate_class_sources => :environment_for_graph do
|
93
|
+
ActiveRecord::Base.send(:subclasses).each do |klass|
|
94
|
+
namespace_parts = klass.name.split("::")
|
95
|
+
saving_class("#{namespace_parts.pop.underscore}.dot", namespace_parts) do
|
96
|
+
klass.to_dot_notation(:title => "#{klass.name} model associations")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
task :generate_complete_graph => :environment_for_graph do
|
102
|
+
saving_class("all_classes_in_app.dot") do
|
103
|
+
@graph = MiniGauge::Graph.new(:title => "All classes in app", :description => "The complete set of classes and their relations")
|
104
|
+
|
105
|
+
# No lazy loading here!
|
106
|
+
Dir['app/models/**/*.rb'].each{|f| require f}
|
107
|
+
|
108
|
+
# Run through all the reflections first in an attempt to load everything before getting all the subclasses.
|
109
|
+
ActiveRecord::Base.send(:subclasses).each do |klass|
|
110
|
+
begin klass.reflect_on_all_associations.each(&:klass) rescue nil end
|
111
|
+
end
|
112
|
+
|
113
|
+
ActiveRecord::Base.send(:subclasses).each do |klass|
|
114
|
+
klass.add_to_graph(@graph)
|
115
|
+
end
|
116
|
+
|
117
|
+
@graph.to_dot_notation
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mini_gauge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 11
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 5
|
9
|
+
- 0
|
10
|
+
version: 0.5.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- James Prior
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-24 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: thoughtbot-shoulda
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
description: " longer description of your gem"
|
36
|
+
email: james@mabonus.net
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README
|
44
|
+
- README.rdoc
|
45
|
+
files:
|
46
|
+
- LICENSE
|
47
|
+
- README
|
48
|
+
- README.rdoc
|
49
|
+
- Rakefile
|
50
|
+
- VERSION
|
51
|
+
- lib/mini_gauge.rb
|
52
|
+
- lib/tasks/mini_gauge.rake
|
53
|
+
- test/helper.rb
|
54
|
+
- test/test_mini_gauge.rb
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/asee/mini_gauge
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options:
|
61
|
+
- --charset=UTF-8
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 3
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.5.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: s one-line summary of your gem
|
89
|
+
test_files:
|
90
|
+
- test/helper.rb
|
91
|
+
- test/test_mini_gauge.rb
|