railroady 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,95 @@
1
+ # RailRoady - 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 'railroady/app_diagram'
8
+
9
+ # RailRoady controllers diagram
10
+ class ControllersDiagram < AppDiagram
11
+
12
+ # as of Rails 2.3 the file is no longer application.rb but instead
13
+ # application_controller.rb
14
+ APP_CONTROLLER = File.exist?('app/controllers/application.rb') ? 'app/controllers/application.rb' : 'app/controllers/application_controller.rb'
15
+
16
+ def initialize(options = OptionsStruct.new)
17
+ #options.exclude.map! {|e| "app/controllers/" + e}
18
+ super options
19
+ @graph.diagram_type = 'Controllers'
20
+ end
21
+
22
+ # Process controller files
23
+ def generate
24
+ STDERR.print "Generating controllers diagram\n" if @options.verbose
25
+ files = get_files
26
+ # only add APP_CONTROLLER if it isn't already included from the glob above
27
+ files << APP_CONTROLLER unless files.include? APP_CONTROLLER
28
+ files.each do |f|
29
+ class_name = extract_class_name(f)
30
+ # ApplicationController's file is 'application.rb' in Rails < 2.3
31
+ class_name += 'Controller' if class_name == 'Application'
32
+ begin
33
+ process_class class_name.constantize
34
+ rescue Exception
35
+ STDERR.print "Warning: exception #{$!} raised while trying to load controller class #{f}"
36
+ end
37
+ end
38
+ end # generate
39
+
40
+ def get_files(prefix ='')
41
+ files = !@options.specify.empty? ? Dir.glob(@options.specify) : Dir.glob(prefix << "app/controllers/**/*_controller.rb")
42
+ files -= Dir.glob(@options.exclude)
43
+ files
44
+ end
45
+
46
+ private
47
+ # Load controller classes
48
+ def load_classes
49
+ begin
50
+ disable_stdout
51
+ # ApplicationController must be loaded first
52
+ require APP_CONTROLLER
53
+ get_files.each {|c| require "./#{c}" }
54
+ enable_stdout
55
+ rescue LoadError
56
+ enable_stdout
57
+ print_error "controller classes"
58
+ raise
59
+ end
60
+ end # load_classes
61
+
62
+ # Proccess a controller class
63
+ def process_class(current_class)
64
+
65
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
66
+
67
+ if @options.brief
68
+ @graph.add_node ['controller-brief', current_class.name]
69
+ elsif current_class.is_a? Class
70
+ # Collect controller's methods
71
+ node_attribs = {:public => [],
72
+ :protected => [],
73
+ :private => []}
74
+ current_class.public_instance_methods(false).sort.each { |m|
75
+ node_attribs[:public] << m
76
+ } unless @options.hide_public
77
+ current_class.protected_instance_methods(false).sort.each { |m|
78
+ node_attribs[:protected] << m
79
+ } unless @options.hide_protected
80
+ current_class.private_instance_methods(false).sort.each { |m|
81
+ node_attribs[:private] << m
82
+ } unless @options.hide_private
83
+ @graph.add_node ['controller', current_class.name, node_attribs]
84
+ elsif @options.modules && current_class.is_a?(Module)
85
+ @graph.add_node ['module', current_class.name]
86
+ end
87
+
88
+ # Generate the inheritance edge (only for ApplicationControllers)
89
+ if @options.inheritance &&
90
+ (ApplicationController.subclasses.include? current_class.name)
91
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
92
+ end
93
+ end # process_class
94
+
95
+ end # class ControllersDiagram
@@ -0,0 +1,130 @@
1
+ # RailRoady - 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
+ # RailRoady 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}"+
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 += 'arrowtail=odot, arrowhead=dot, dir=both'
113
+ when 'one-many'
114
+ options += 'arrowtail=odot, arrowhead=crow, dir=both'
115
+ when 'many-many'
116
+ options += 'arrowtail=crow, arrowhead=crow, dir=both'
117
+ when 'is-a'
118
+ options += 'arrowhead="none", arrowtail="onormal"'
119
+ when 'event'
120
+ options += "fontsize=10"
121
+ end
122
+ return "\t#{quote(from)} -> #{quote(to)} [#{options}]\n"
123
+ end # dot_edge
124
+
125
+ # Quotes a class name
126
+ def quote(name)
127
+ '"' + name.to_s + '"'
128
+ end
129
+
130
+ end # class DiagramGraph
@@ -0,0 +1,146 @@
1
+ # RailRoady - 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 'railroady/app_diagram'
8
+
9
+ # RailRoady models diagram
10
+ class ModelsDiagram < AppDiagram
11
+
12
+ def initialize(options = OptionsStruct.new)
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
+ get_files.each do |f|
24
+ begin
25
+ process_class extract_class_name(f).constantize
26
+ rescue Exception
27
+ STDERR.print "Warning: exception #{$!} raised while trying to load model class #{f}\n"
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ def get_files(prefix ='')
34
+ files = !@options.specify.empty? ? Dir.glob(@options.specify) : Dir.glob(prefix << "app/models/**/*.rb")
35
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
36
+ files -= Dir.glob(@options.exclude)
37
+ files
38
+ end
39
+
40
+ # Process a model class
41
+ def process_class(current_class)
42
+
43
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
44
+
45
+ generated = false
46
+
47
+ # Is current_clas derived from ActiveRecord::Base?
48
+ if current_class.respond_to?'reflect_on_all_associations'
49
+
50
+
51
+ node_attribs = []
52
+ if @options.brief || current_class.abstract_class?
53
+ node_type = 'model-brief'
54
+ else
55
+ node_type = 'model'
56
+
57
+ # Collect model's content columns
58
+
59
+ content_columns = current_class.content_columns
60
+
61
+ if @options.hide_magic
62
+ # From patch #13351
63
+ # http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
64
+ magic_fields = [
65
+ "created_at", "created_on", "updated_at", "updated_on",
66
+ "lock_version", "type", "id", "position", "parent_id", "lft",
67
+ "rgt", "quote", "template"
68
+ ]
69
+ magic_fields << current_class.table_name + "_count" if current_class.respond_to? 'table_name'
70
+ content_columns = current_class.content_columns.select {|c| ! magic_fields.include? c.name}
71
+ else
72
+ content_columns = current_class.content_columns
73
+ end
74
+
75
+ content_columns.each do |a|
76
+ content_column = a.name
77
+ content_column += ' :' + a.type.to_s unless @options.hide_types
78
+ node_attribs << content_column
79
+ end
80
+ end
81
+ @graph.add_node [node_type, current_class.name, node_attribs]
82
+ generated = true
83
+ # Process class associations
84
+ associations = current_class.reflect_on_all_associations
85
+ if @options.inheritance && ! @options.transitive
86
+ superclass_associations = current_class.superclass.reflect_on_all_associations
87
+
88
+ associations = associations.select{|a| ! superclass_associations.include? a}
89
+ # This doesn't works!
90
+ # associations -= current_class.superclass.reflect_on_all_associations
91
+ end
92
+ associations.each do |a|
93
+ process_association current_class.name, a
94
+ end
95
+ elsif @options.all && (current_class.is_a? Class)
96
+ # Not ActiveRecord::Base model
97
+ node_type = @options.brief ? 'class-brief' : 'class'
98
+ @graph.add_node [node_type, current_class.name]
99
+ generated = true
100
+ elsif @options.modules && (current_class.is_a? Module)
101
+ @graph.add_node ['module', current_class.name]
102
+ end
103
+
104
+ # Only consider meaningful inheritance relations for generated classes
105
+ if @options.inheritance && generated &&
106
+ (current_class.superclass != ActiveRecord::Base) &&
107
+ (current_class.superclass != Object)
108
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
109
+ end
110
+
111
+ end # process_class
112
+
113
+ # Process a model association
114
+ def process_association(class_name, assoc)
115
+
116
+ STDERR.print "\t\tProcessing model association #{assoc.name.to_s}\n" if @options.verbose
117
+
118
+ # Skip "belongs_to" associations
119
+ return if assoc.macro.to_s == 'belongs_to' && !@options.show_belongs_to
120
+
121
+ # Only non standard association names needs a label
122
+
123
+ # from patch #12384
124
+ # if assoc.class_name == assoc.name.to_s.singularize.camelize
125
+ assoc_class_name = (assoc.class_name.respond_to? 'underscore') ? assoc.class_name.underscore.singularize.camelize : assoc.class_name
126
+ if assoc_class_name == assoc.name.to_s.singularize.camelize
127
+ assoc_name = ''
128
+ else
129
+ assoc_name = assoc.name.to_s
130
+ end
131
+
132
+ if ['has_one', 'belongs_to'].include? assoc.macro.to_s
133
+ assoc_type = 'one-one'
134
+ elsif assoc.macro.to_s == 'has_many' && (! assoc.options[:through])
135
+ assoc_type = 'one-many'
136
+ else # habtm or has_many, :through
137
+ return if @habtm.include? [assoc.class_name, class_name, assoc_name]
138
+ assoc_type = 'many-many'
139
+ @habtm << [class_name, assoc.class_name, assoc_name]
140
+ end
141
+ # from patch #12384
142
+ # @graph.add_edge [assoc_type, class_name, assoc.class_name, assoc_name]
143
+ @graph.add_edge [assoc_type, class_name, assoc_class_name, assoc_name]
144
+ end # process_association
145
+
146
+ end # class ModelsDiagram
@@ -0,0 +1,178 @@
1
+ # RailRoady - 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 'ostruct'
8
+
9
+ # RailRoady command line options parser
10
+ class OptionsStruct < OpenStruct
11
+
12
+ require 'optparse'
13
+
14
+ def initialize(args={})
15
+ init_options = { :all => false,
16
+ :brief => false,
17
+ :specify => [],
18
+ :exclude => [],
19
+ :inheritance => false,
20
+ :join => false,
21
+ :label => false,
22
+ :modules => false,
23
+ :hide_magic => false,
24
+ :hide_types => false,
25
+ :hide_public => false,
26
+ :hide_protected => false,
27
+ :hide_private => false,
28
+ :plugins_models => false,
29
+ :root => '',
30
+ :show_belongs_to => false,
31
+ :transitive => false,
32
+ :verbose => false,
33
+ :xmi => false,
34
+ :command => '',
35
+ :app_name => 'railroady', :app_human_name => 'Railroady', :app_version =>'', :copyright =>'' }
36
+ super(init_options.merge(args))
37
+ end # initialize
38
+
39
+ def parse(args)
40
+ @opt_parser = OptionParser.new do |opts|
41
+ opts.banner = "Usage: #{self.app_name} [options] command"
42
+ opts.separator ""
43
+ opts.separator "Common options:"
44
+ opts.on("-b", "--brief", "Generate compact diagram",
45
+ " (no attributes nor methods)") do |b|
46
+ self.brief = b
47
+ end
48
+ opts.on("-s", "--specify file1[,fileN]", Array, "Specify only given files") do |list|
49
+ self.specify = list
50
+ end
51
+ opts.on("-e", "--exclude file1[,fileN]", Array, "Exclude given files") do |list|
52
+ self.exclude = list
53
+ end
54
+ opts.on("-i", "--inheritance", "Include inheritance relations") do |i|
55
+ self.inheritance = i
56
+ end
57
+ opts.on("-l", "--label", "Add a label with diagram information",
58
+ " (type, date, migration, version)") do |l|
59
+ self.label = l
60
+ end
61
+ opts.on("-o", "--output FILE", "Write diagram to file FILE") do |f|
62
+ self.output = f
63
+ end
64
+ opts.on("-r", "--root PATH", "Set PATH as the application root") do |r|
65
+ self.root = r
66
+ end
67
+ opts.on("-v", "--verbose", "Enable verbose output",
68
+ " (produce messages to STDOUT)") do |v|
69
+ self.verbose = v
70
+ end
71
+ opts.on("-x", "--xmi", "Produce XMI instead of DOT",
72
+ " (for UML tools)") do |x|
73
+ self.xmi = x
74
+ end
75
+ opts.separator ""
76
+ opts.separator "Models diagram options:"
77
+ opts.on("-a", "--all", "Include all models",
78
+ " (not only ActiveRecord::Base derived)") do |a|
79
+ self.all = a
80
+ end
81
+ opts.on("--show-belongs_to", "Show belongs_to associations") do |s|
82
+ self.show_belongs_to = s
83
+ end
84
+ opts.on("--hide-magic", "Hide magic field names") do |h|
85
+ self.hide_magic = h
86
+ end
87
+ opts.on("--hide-types", "Hide attributes type") do |h|
88
+ self.hide_types = h
89
+ end
90
+ opts.on("-j", "--join", "Concentrate edges") do |j|
91
+ self.join = j
92
+ end
93
+ opts.on("-m", "--modules", "Include modules") do |m|
94
+ self.modules = m
95
+ end
96
+ opts.on("-p", "--plugins-models", "Include plugins models") do |p|
97
+ self.plugins_models = p
98
+ end
99
+ opts.on("-t", "--transitive", "Include transitive associations",
100
+ "(through inheritance)") do |t|
101
+ self.transitive = t
102
+ end
103
+ opts.separator ""
104
+ opts.separator "Controllers diagram options:"
105
+ opts.on("--hide-public", "Hide public methods") do |h|
106
+ self.hide_public = h
107
+ end
108
+ opts.on("--hide-protected", "Hide protected methods") do |h|
109
+ self.hide_protected = h
110
+ end
111
+ opts.on("--hide-private", "Hide private methods") do |h|
112
+ self.hide_private = h
113
+ end
114
+ opts.separator ""
115
+ opts.separator "Other options:"
116
+ opts.on("-h", "--help", "Show this message") do
117
+ STDOUT.print "#{opts}\n"
118
+ exit
119
+ end
120
+ opts.on("--version", "Show version and copyright") do
121
+ STDOUT.print"#{self.app_human_name} version #{self.app_version}\n\n" +
122
+ "#{self.copyright}\nThis is free software; see the source " +
123
+ "for copying conditions.\n\n"
124
+ exit
125
+ end
126
+ opts.separator ""
127
+ opts.separator "Commands (you must supply one of these):"
128
+ opts.on("-M", "--models", "Generate models diagram") do |c|
129
+ if self.command != ''
130
+ STDERR.print "Error: Can only generate one diagram type\n\n"
131
+ exit 1
132
+ else
133
+ self.command = 'models'
134
+ end
135
+ end
136
+ opts.on("-C", "--controllers", "Generate controllers diagram") do |c|
137
+ if self.command != ''
138
+ STDERR.print "Error: Can only generate one diagram type\n\n"
139
+ exit 1
140
+ else
141
+ self.command = 'controllers'
142
+ end
143
+ end
144
+ # From Ana Nelson's patch
145
+ opts.on("-A", "--aasm", "Generate \"acts as state machine\" diagram") do |c|
146
+ if self.command == 'controllers'
147
+ STDERR.print "Error: Can only generate one diagram type\n\n"
148
+ exit 1
149
+ else
150
+ self.command = 'aasm'
151
+ end
152
+ end
153
+ opts.separator ""
154
+ opts.separator "For bug reporting and additional information, please see:"
155
+ opts.separator "http://railroad.rubyforge.org/"
156
+ end # do
157
+
158
+ begin
159
+ @opt_parser.parse!(args)
160
+ rescue OptionParser::AmbiguousOption
161
+ option_error "Ambiguous option"
162
+ rescue OptionParser::InvalidOption
163
+ option_error "Invalid option"
164
+ rescue OptionParser::InvalidArgument
165
+ option_error "Invalid argument"
166
+ rescue OptionParser::MissingArgument
167
+ option_error "Missing argument"
168
+ end
169
+ end # parse
170
+
171
+ private
172
+
173
+ def option_error(msg)
174
+ STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
175
+ exit 1
176
+ end
177
+
178
+ end # class OptionsStruct