mini_gauge 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
+
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'mini_gauge'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestMiniGauge < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
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