tarikjn-railroad 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # RailRoad - RoR diagrams generator
4
+ # http://railroad.rubyforge.org
5
+ #
6
+ # RailRoad generates models and controllers diagrams in DOT language
7
+ # for a Rails application.
8
+ #
9
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
10
+ #
11
+ # This program is free software; you can redistribute it and/or modify
12
+ # it under the terms of the GNU General Public License as published by
13
+ # the Free Software Foundation; either version 2 of the License, or
14
+ # (at your option) any later version.
15
+ #
16
+
17
+ APP_NAME = "railroad"
18
+ APP_HUMAN_NAME = "RailRoad"
19
+ APP_VERSION = [0,5,0]
20
+ COPYRIGHT = "Copyright (C) 2007-2008 Javier Smaldone"
21
+
22
+ $: << "."
23
+ require 'railroad/options_struct'
24
+ require 'railroad/models_diagram'
25
+ require 'railroad/controllers_diagram'
26
+ require 'railroad/aasm_diagram'
27
+
28
+ options = OptionsStruct.new
29
+
30
+ options.parse ARGV
31
+
32
+ old_dir = Dir.pwd
33
+
34
+ Dir.chdir(options.root) if options.root != ''
35
+
36
+ if options.command == 'models'
37
+ diagram = ModelsDiagram.new options
38
+ elsif options.command == 'controllers'
39
+ diagram = ControllersDiagram.new options
40
+ elsif options.command == 'aasm'
41
+ diagram = AasmDiagram.new options
42
+ else
43
+ STDERR.print "Error: You must supply a command\n" +
44
+ " (try #{APP_NAME} -h)\n\n"
45
+ exit 1
46
+ end
47
+
48
+ diagram.generate
49
+
50
+ Dir.chdir(old_dir)
51
+
52
+ diagram.print
53
+
@@ -0,0 +1,81 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+ # AASM code provided by Ana Nelson (http://ananelson.com/)
8
+
9
+ require 'railroad/app_diagram'
10
+
11
+ # Diagram for Acts As State Machine
12
+ class AasmDiagram < AppDiagram
13
+
14
+ def initialize(options)
15
+ #options.exclude.map! {|e| e = "app/models/" + e}
16
+ super options
17
+ @graph.diagram_type = 'Models'
18
+ # Processed habtm associations
19
+ @habtm = []
20
+ end
21
+
22
+ # Process model files
23
+ def generate
24
+ STDERR.print "Generating AASM diagram\n" if @options.verbose
25
+ files = Dir.glob("app/models/**/*.rb")
26
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
27
+ files -= @options.exclude
28
+ files.each do |f|
29
+ process_class extract_class_name(f).constantize
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Load model classes
36
+ def load_classes
37
+ begin
38
+ disable_stdout
39
+ files = Dir.glob("app/models/**/*.rb")
40
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
41
+ files -= @options.exclude
42
+ files.each {|m| require m }
43
+ enable_stdout
44
+ rescue LoadError
45
+ enable_stdout
46
+ print_error "model classes"
47
+ raise
48
+ end
49
+ end # load_classes
50
+
51
+ # Process a model class
52
+ def process_class(current_class)
53
+
54
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
55
+
56
+ # Only interested in acts_as_state_machine models.
57
+ return unless current_class.respond_to?'states'
58
+
59
+ node_attribs = []
60
+ node_type = 'aasm'
61
+
62
+ current_class.states.each do |state_name|
63
+ state = current_class.read_inheritable_attribute(:states)[state_name]
64
+ node_shape = (current_class.initial_state === state_name) ? ", peripheries = 2" : ""
65
+ node_attribs << "#{current_class.name.downcase}_#{state_name} [label=#{state_name} #{node_shape}];"
66
+ end
67
+ @graph.add_node [node_type, current_class.name, node_attribs]
68
+
69
+ current_class.read_inheritable_attribute(:transition_table).each do |event_name, event|
70
+ event.each do |transition|
71
+ @graph.add_edge [
72
+ 'event',
73
+ current_class.name.downcase + "_" + transition.from.to_s,
74
+ current_class.name.downcase + "_" + transition.to.to_s,
75
+ event_name.to_s
76
+ ]
77
+ end
78
+ end
79
+ end # process_class
80
+
81
+ end # class AasmDiagram
@@ -0,0 +1,89 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+ require 'railroad/diagram_graph'
8
+
9
+ # Root class for RailRoad diagrams
10
+ class AppDiagram
11
+
12
+ def initialize(options)
13
+ @options = options
14
+ @graph = DiagramGraph.new
15
+ @graph.show_label = @options.label
16
+
17
+ STDERR.print "Loading application environment\n" if @options.verbose
18
+ load_environment
19
+
20
+ STDERR.print "Loading application classes\n" if @options.verbose
21
+ load_classes
22
+ end
23
+
24
+ # Print diagram
25
+ def print
26
+ if @options.output
27
+ old_stdout = STDOUT.dup
28
+ begin
29
+ STDOUT.reopen(@options.output)
30
+ rescue
31
+ STDERR.print "Error: Cannot write diagram to #{@options.output}\n\n"
32
+ exit 2
33
+ end
34
+ end
35
+
36
+ if @options.xmi
37
+ STDERR.print "Generating XMI diagram\n" if @options.verbose
38
+ STDOUT.print @graph.to_xmi
39
+ else
40
+ STDERR.print "Generating DOT graph\n" if @options.verbose
41
+ STDOUT.print @graph.to_dot
42
+ end
43
+
44
+ if @options.output
45
+ STDOUT.reopen(old_stdout)
46
+ end
47
+ end # print
48
+
49
+ private
50
+
51
+ # Prevents Rails application from writing to STDOUT
52
+ def disable_stdout
53
+ @old_stdout = STDOUT.dup
54
+ STDOUT.reopen(RUBY_PLATFORM =~ /mswin/ ? "NUL" : "/dev/null")
55
+ end
56
+
57
+ # Restore STDOUT
58
+ def enable_stdout
59
+ STDOUT.reopen(@old_stdout)
60
+ end
61
+
62
+
63
+ # Print error when loading Rails application
64
+ def print_error(type)
65
+ STDERR.print "Error loading #{type}.\n (Are you running " +
66
+ "#{APP_NAME} on the aplication's root directory?)\n\n"
67
+ end
68
+
69
+ # Load Rails application's environment
70
+ def load_environment
71
+ begin
72
+ disable_stdout
73
+ require "config/environment"
74
+ enable_stdout
75
+ rescue LoadError
76
+ enable_stdout
77
+ print_error "application environment"
78
+ raise
79
+ end
80
+ end
81
+
82
+ # Extract class name from filename
83
+ def extract_class_name(filename)
84
+ #filename.split('/')[2..-1].join('/').split('.').first.camelize
85
+ # Fixed by patch from ticket #12742
86
+ File.basename(filename).chomp(".rb").camelize
87
+ end
88
+
89
+ end # class AppDiagram
@@ -0,0 +1,83 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+ require 'railroad/app_diagram'
8
+
9
+ # RailRoad controllers diagram
10
+ class ControllersDiagram < AppDiagram
11
+
12
+ def initialize(options)
13
+ #options.exclude.map! {|e| "app/controllers/" + e}
14
+ super options
15
+ @graph.diagram_type = 'Controllers'
16
+ end
17
+
18
+ # Process controller files
19
+ def generate
20
+ STDERR.print "Generating controllers diagram\n" if @options.verbose
21
+
22
+ files = Dir.glob("app/controllers/**/*_controller.rb") - @options.exclude
23
+ files << 'app/controllers/application.rb'
24
+ files.each do |f|
25
+ class_name = extract_class_name(f)
26
+ # ApplicationController's file is 'application.rb'
27
+ class_name += 'Controller' if class_name == 'Application'
28
+ process_class class_name.constantize
29
+ end
30
+ end # generate
31
+
32
+ private
33
+
34
+ # Load controller classes
35
+ def load_classes
36
+ begin
37
+ disable_stdout
38
+ # ApplicationController must be loaded first
39
+ require "app/controllers/application.rb"
40
+ files = Dir.glob("app/controllers/**/*_controller.rb") - @options.exclude
41
+ files.each {|c| require c }
42
+ enable_stdout
43
+ rescue LoadError
44
+ enable_stdout
45
+ print_error "controller classes"
46
+ raise
47
+ end
48
+ end # load_classes
49
+
50
+ # Proccess a controller class
51
+ def process_class(current_class)
52
+
53
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
54
+
55
+ if @options.brief
56
+ @graph.add_node ['controller-brief', current_class.name]
57
+ elsif current_class.is_a? Class
58
+ # Collect controller's methods
59
+ node_attribs = {:public => [],
60
+ :protected => [],
61
+ :private => []}
62
+ current_class.public_instance_methods(false).sort.each { |m|
63
+ node_attribs[:public] << m
64
+ } unless @options.hide_public
65
+ current_class.protected_instance_methods(false).sort.each { |m|
66
+ node_attribs[:protected] << m
67
+ } unless @options.hide_protected
68
+ current_class.private_instance_methods(false).sort.each { |m|
69
+ node_attribs[:private] << m
70
+ } unless @options.hide_private
71
+ @graph.add_node ['controller', current_class.name, node_attribs]
72
+ elsif @options.modules && current_class.is_a?(Module)
73
+ @graph.add_node ['module', current_class.name]
74
+ end
75
+
76
+ # Generate the inheritance edge (only for ApplicationControllers)
77
+ if @options.inheritance &&
78
+ (ApplicationController.subclasses.include? current_class.name)
79
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
80
+ end
81
+ end # process_class
82
+
83
+ end # class ControllersDiagram
@@ -0,0 +1,133 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+
8
+ # RailRoad diagram structure
9
+ class DiagramGraph
10
+
11
+ def initialize
12
+ @diagram_type = ''
13
+ @show_label = false
14
+ @nodes = []
15
+ @edges = []
16
+ end
17
+
18
+ def add_node(node)
19
+ @nodes << node
20
+ end
21
+
22
+ def add_edge(edge)
23
+ @edges << edge
24
+ end
25
+
26
+ def diagram_type= (type)
27
+ @diagram_type = type
28
+ end
29
+
30
+ def show_label= (value)
31
+ @show_label = value
32
+ end
33
+
34
+
35
+ # Generate DOT graph
36
+ def to_dot
37
+ return dot_header +
38
+ @nodes.map{|n| dot_node n[0], n[1], n[2]}.join +
39
+ @edges.map{|e| dot_edge e[0], e[1], e[2], e[3]}.join +
40
+ dot_footer
41
+ end
42
+
43
+ # Generate XMI diagram (not yet implemented)
44
+ def to_xmi
45
+ STDERR.print "Sorry. XMI output not yet implemented.\n\n"
46
+ return ""
47
+ end
48
+
49
+ private
50
+
51
+ # Build DOT diagram header
52
+ def dot_header
53
+ result = "digraph #{@diagram_type.downcase}_diagram {\n" +
54
+ "\tgraph[overlap=false, splines=true]\n"
55
+ result += dot_label if @show_label
56
+ return result
57
+ end
58
+
59
+ # Build DOT diagram footer
60
+ def dot_footer
61
+ return "}\n"
62
+ end
63
+
64
+ # Build diagram label
65
+ def dot_label
66
+ return "\t_diagram_info [shape=\"plaintext\", " +
67
+ "label=\"#{@diagram_type} diagram\\l" +
68
+ "Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l" +
69
+ "Migration version: " +
70
+ "#{ActiveRecord::Migrator.current_version}\\l" +
71
+ "Generated by #{APP_HUMAN_NAME} #{APP_VERSION.join('.')}"+
72
+ "\\l\", fontsize=14]\n"
73
+ end
74
+
75
+ # Build a DOT graph node
76
+ def dot_node(type, name, attributes=nil)
77
+ case type
78
+ when 'model'
79
+ options = 'shape=Mrecord, label="{' + name + '|'
80
+ options += attributes.join('\l')
81
+ options += '\l}"'
82
+ when 'model-brief'
83
+ options = ''
84
+ when 'class'
85
+ options = 'shape=record, label="{' + name + '|}"'
86
+ when 'class-brief'
87
+ options = 'shape=box'
88
+ when 'controller'
89
+ options = 'shape=Mrecord, label="{' + name + '|'
90
+ public_methods = attributes[:public].join('\l')
91
+ protected_methods = attributes[:protected].join('\l')
92
+ private_methods = attributes[:private].join('\l')
93
+ options += public_methods + '\l|' + protected_methods + '\l|' +
94
+ private_methods + '\l'
95
+ options += '}"'
96
+ when 'controller-brief'
97
+ options = ''
98
+ when 'module'
99
+ options = 'shape=box, style=dotted, label="' + name + '"'
100
+ when 'aasm'
101
+ # Return subgraph format
102
+ return "subgraph cluster_#{name.downcase} {\n\tlabel = #{quote(name)}\n\t#{attributes.join("\n ")}}"
103
+ end # case
104
+ return "\t#{quote(name)} [#{options}]\n"
105
+ end # dot_node
106
+
107
+ # Build a DOT graph edge
108
+ def dot_edge(type, from, to, name = '')
109
+ options = name != '' ? "label=\"#{name}\", " : ''
110
+ case type
111
+ when 'one-one'
112
+ #options += 'taillabel="1"'
113
+ options += 'arrowtail=odot, arrowhead=dot, dir=both'
114
+ when 'one-many'
115
+ #options += 'taillabel="n"'
116
+ options += 'arrowtail=crow, arrowhead=dot, dir=both'
117
+ when 'many-many'
118
+ #options += 'taillabel="n", headlabel="n", arrowtail="normal"'
119
+ options += 'arrowtail=crow, arrowhead=crow, dir=both'
120
+ when 'is-a'
121
+ options += 'arrowhead="none", arrowtail="onormal"'
122
+ when 'event'
123
+ options += "fontsize=10"
124
+ end
125
+ return "\t#{quote(from)} -> #{quote(to)} [#{options}]\n"
126
+ end # dot_edge
127
+
128
+ # Quotes a class name
129
+ def quote(name)
130
+ '"' + name.to_s + '"'
131
+ end
132
+
133
+ end # class DiagramGraph
@@ -0,0 +1,155 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+ require 'railroad/app_diagram'
8
+
9
+ # RailRoad models diagram
10
+ class ModelsDiagram < AppDiagram
11
+
12
+ def initialize(options)
13
+ #options.exclude.map! {|e| "app/models/" + e}
14
+ super options
15
+ @graph.diagram_type = 'Models'
16
+ # Processed habtm associations
17
+ @habtm = []
18
+ end
19
+
20
+ # Process model files
21
+ def generate
22
+ STDERR.print "Generating models diagram\n" if @options.verbose
23
+ files = Dir.glob("app/models/**/*.rb")
24
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
25
+ files -= @options.exclude
26
+ files.each do |f|
27
+ process_class extract_class_name(f).constantize
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Load model classes
34
+ def load_classes
35
+ begin
36
+ disable_stdout
37
+ files = Dir.glob("app/models/**/*.rb")
38
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
39
+ files -= @options.exclude
40
+ files.each {|m| require m }
41
+ enable_stdout
42
+ rescue LoadError
43
+ enable_stdout
44
+ print_error "model classes"
45
+ raise
46
+ end
47
+ end # load_classes
48
+
49
+ # Process a model class
50
+ def process_class(current_class)
51
+
52
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
53
+
54
+ generated = false
55
+
56
+ # Is current_clas derived from ActiveRecord::Base?
57
+ if current_class.respond_to?'reflect_on_all_associations'
58
+
59
+
60
+ node_attribs = []
61
+ if @options.brief || current_class.abstract_class?
62
+ node_type = 'model-brief'
63
+ else
64
+ node_type = 'model'
65
+
66
+ # Collect model's content columns
67
+
68
+ content_columns = current_class.content_columns
69
+
70
+ if @options.hide_magic
71
+ # From patch #13351
72
+ # http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
73
+ magic_fields = [
74
+ "created_at", "created_on", "updated_at", "updated_on",
75
+ "lock_version", "type", "id", "position", "parent_id", "lft",
76
+ "rgt", "quote", "template"
77
+ ]
78
+ magic_fields << current_class.table_name + "_count" if current_class.respond_to? 'table_name'
79
+ content_columns = current_class.content_columns.select {|c| ! magic_fields.include? c.name}
80
+ else
81
+ content_columns = current_class.content_columns
82
+ end
83
+
84
+ content_columns.each do |a|
85
+ content_column = a.name
86
+ content_column += ' :' + a.type.to_s unless @options.hide_types
87
+ node_attribs << content_column
88
+ end
89
+ end
90
+ @graph.add_node [node_type, current_class.name, node_attribs]
91
+ generated = true
92
+ # Process class associations
93
+ associations = current_class.reflect_on_all_associations
94
+ if @options.inheritance && ! @options.transitive
95
+ superclass_associations = current_class.superclass.reflect_on_all_associations
96
+
97
+ associations = associations.select{|a| ! superclass_associations.include? a}
98
+ # This doesn't works!
99
+ # associations -= current_class.superclass.reflect_on_all_associations
100
+ end
101
+ associations.each do |a|
102
+ process_association current_class.name, a
103
+ end
104
+ elsif @options.all && (current_class.is_a? Class)
105
+ # Not ActiveRecord::Base model
106
+ node_type = @options.brief ? 'class-brief' : 'class'
107
+ @graph.add_node [node_type, current_class.name]
108
+ generated = true
109
+ elsif @options.modules && (current_class.is_a? Module)
110
+ @graph.add_node ['module', current_class.name]
111
+ end
112
+
113
+ # Only consider meaningful inheritance relations for generated classes
114
+ if @options.inheritance && generated &&
115
+ (current_class.superclass != ActiveRecord::Base) &&
116
+ (current_class.superclass != Object)
117
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
118
+ end
119
+
120
+ end # process_class
121
+
122
+ # Process a model association
123
+ def process_association(class_name, assoc)
124
+
125
+ STDERR.print "\t\tProcessing model association #{assoc.name.to_s}\n" if @options.verbose
126
+
127
+ # Skip "belongs_to" associations
128
+ return if assoc.macro.to_s == 'belongs_to'
129
+
130
+ # Only non standard association names needs a label
131
+
132
+ # from patch #12384
133
+ # if assoc.class_name == assoc.name.to_s.singularize.camelize
134
+ assoc_class_name = (assoc.class_name.respond_to? 'underscore') ? assoc.class_name.underscore.singularize.camelize : assoc.class_name
135
+ if assoc_class_name == assoc.name.to_s.singularize.camelize
136
+ assoc_name = ''
137
+ else
138
+ assoc_name = assoc.name.to_s
139
+ end
140
+
141
+ if assoc.macro.to_s == 'has_one'
142
+ assoc_type = 'one-one'
143
+ elsif assoc.macro.to_s == 'has_many' && (! assoc.options[:through])
144
+ assoc_type = 'one-many'
145
+ else # habtm or has_many, :through
146
+ return if @habtm.include? [assoc.class_name, class_name, assoc_name]
147
+ assoc_type = 'many-many'
148
+ @habtm << [class_name, assoc.class_name, assoc_name]
149
+ end
150
+ # from patch #12384
151
+ # @graph.add_edge [assoc_type, class_name, assoc.class_name, assoc_name]
152
+ @graph.add_edge [assoc_type, class_name, assoc_class_name, assoc_name]
153
+ end # process_association
154
+
155
+ end # class ModelsDiagram