bryanlarsen-railroad 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
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
+ # RailRoad controllers diagram
8
+ class ControllersDiagram < AppDiagram
9
+
10
+ def initialize(options)
11
+ #options.exclude.map! {|e| "app/controllers/" + e}
12
+ super options
13
+ @graph.diagram_type = 'Controllers'
14
+ end
15
+
16
+ # Process controller files
17
+ def generate
18
+ STDERR.print "Generating controllers diagram\n" if @options.verbose
19
+
20
+ files = Dir.glob("app/controllers/**/*_controller.rb") - @options.exclude
21
+ files.each do |f|
22
+ class_name = extract_class_name('app/controllers/', f)
23
+ process_class class_name.constantize
24
+ end
25
+ end # generate
26
+
27
+ private
28
+
29
+ # Load controller classes
30
+ def load_classes
31
+ begin
32
+ disable_stdout
33
+ files = Dir.glob("app/controllers/**/*_controller.rb") - @options.exclude
34
+ files.each {|file| get_controller_class(file) }
35
+ enable_stdout
36
+ rescue LoadError
37
+ enable_stdout
38
+ print_error "controller classes"
39
+ raise
40
+ end
41
+ end # load_classes
42
+
43
+ # This method is taken from the annotate models gem
44
+ # http://github.com/ctran/annotate_models/tree/master
45
+ #
46
+ # Retrieve the classes belonging to the controller names we're asked to process
47
+ # Check for namespaced controllers in subdirectories as well as controllers
48
+ # in subdirectories without namespacing.
49
+ def get_controller_class(file)
50
+ model = file.sub(/^.*app\/controllers\//, '').sub(/\.rb$/, '').camelize
51
+ parts = model.split('::')
52
+ begin
53
+ parts.inject(Object) {|klass, part| klass.const_get(part) }
54
+ rescue LoadError
55
+ Object.const_get(parts.last)
56
+ end
57
+ end
58
+
59
+ # Proccess a controller class
60
+ def process_class(current_class)
61
+
62
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
63
+
64
+ if @options.brief
65
+ @graph.add_node ['controller-brief', current_class.name]
66
+ elsif current_class.is_a? Class
67
+ # Collect controller's methods
68
+ node_attribs = {:public => [],
69
+ :protected => [],
70
+ :private => []}
71
+ current_class.public_instance_methods(false).sort.each { |m|
72
+ process_method(node_attribs[:public], m)
73
+ } unless @options.hide_public
74
+ current_class.protected_instance_methods(false).sort.each { |m|
75
+ process_method(node_attribs[:protected], m)
76
+ } unless @options.hide_protected
77
+ current_class.private_instance_methods(false).sort.each { |m|
78
+ process_method(node_attribs[:private], m)
79
+ } unless @options.hide_private
80
+ @graph.add_node ['controller', current_class.name, node_attribs]
81
+ elsif @options.modules && current_class.is_a?(Module)
82
+ @graph.add_node ['module', current_class.name]
83
+ end
84
+
85
+ # Generate the inheritance edge (only for ApplicationControllers)
86
+ if @options.inheritance &&
87
+ (ApplicationController.subclasses.include? current_class.name)
88
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
89
+ end
90
+ end # process_class
91
+
92
+ # Process a method
93
+ def process_method(attribs, method)
94
+ return if @options.hide_underscore && method[0..0] == '_'
95
+ attribs << method
96
+ end
97
+
98
+ 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,203 @@
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
+ # RailRoad models diagram
8
+ class ModelsDiagram < AppDiagram
9
+
10
+ def initialize(options)
11
+ #options.exclude.map! {|e| "app/models/" + e}
12
+ super options
13
+ @graph.diagram_type = 'Models'
14
+ # Processed habtm associations
15
+ @habtm = []
16
+ end
17
+
18
+ # Process model files
19
+ def generate
20
+ STDERR.print "Generating models diagram\n" if @options.verbose
21
+ base = "(app/models/|lib/)"
22
+ files = Dir.glob("app/models/**/*.rb")
23
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
24
+ files += Dir.glob("lib/**/*.rb") if @options.libraries
25
+
26
+ files -= @options.exclude
27
+ (files + ['filter_condition_type']).each do |file|
28
+ model_name =
29
+ @options.classes_by_files[file] ||
30
+ (model_path = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\2')).camelize
31
+ STDERR.print "Processing #{file} ...\n" if @options.verbose
32
+ # Hack to skip all xxx_related.rb files
33
+ next if /_related/i =~ model_name
34
+
35
+ klass = begin
36
+ model_name.constantize
37
+ rescue LoadError
38
+ STDERR.print "\t#{model_name} raised LoadError.\n" if @options.verbose
39
+ oldlen = model_path.length
40
+ model_path.gsub!(/.*[\/\\]/, '')
41
+ model_name = model_path.camelize
42
+ if oldlen > model_path.length
43
+ retry
44
+ end
45
+ STDERR.print "\tDone trying to remove slashes, skipping this model.\n" if @options.verbose
46
+ next
47
+ rescue NameError
48
+ STDERR.print "\t#{model_name} raised NameError, skipping this model.\n" if @options.verbose
49
+ next
50
+ end
51
+
52
+ process_class klass
53
+ STDERR.print "Done #{file}\n" if @options.verbose
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Load model classes
60
+ def load_classes
61
+ begin
62
+ disable_stdout
63
+ files = Dir.glob("app/models/**/*.rb")
64
+ files += Dir.glob("vendor/plugins/**/app/models/*.rb") if @options.plugins_models
65
+ files -= @options.exclude
66
+ files.each {|file| get_model_class(file) }
67
+ enable_stdout
68
+ rescue LoadError
69
+ enable_stdout
70
+ print_error "model classes"
71
+ raise
72
+ end
73
+ end # load_classes
74
+
75
+ # This method is taken from the annotate models gem
76
+ # http://github.com/ctran/annotate_models/tree/master
77
+ #
78
+ # Retrieve the classes belonging to the model names we're asked to process
79
+ # Check for namespaced models in subdirectories as well as models
80
+ # in subdirectories without namespacing.
81
+ def get_model_class(file)
82
+ model = file.sub(/^.*app\/models\//, '').sub(/\.rb$/, '').camelize
83
+ parts = model.split('::')
84
+ begin
85
+ parts.inject(Object) {|klass, part| klass.const_get(part) }
86
+ rescue LoadError
87
+ Object.const_get(parts.last)
88
+ end
89
+ end
90
+
91
+ # Process a model class
92
+ def process_class(current_class)
93
+
94
+ STDERR.print "\tProcessing #{current_class} ...\n" if @options.verbose
95
+
96
+ generated = false
97
+
98
+ # Is current_clas derived from ActiveRecord::Base?
99
+ if current_class.respond_to?'reflect_on_all_associations'
100
+
101
+
102
+ node_attribs = []
103
+ if @options.brief || current_class.abstract_class? || current_class.superclass != ActiveRecord::Base
104
+ node_type = 'model-brief'
105
+ else
106
+ node_type = 'model'
107
+
108
+ # Collect model's content columns
109
+
110
+ content_columns = current_class.content_columns
111
+
112
+ if @options.hide_magic
113
+ magic_fields = [
114
+ # Restful Authentication
115
+ "login", "crypted_password", "salt", "remember_token", "remember_token_expires_at", "activation_code", "activated_at",
116
+ # From patch #13351
117
+ # http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
118
+ "created_at", "created_on", "updated_at", "updated_on",
119
+ "lock_version", "type", "id", "position", "parent_id", "lft",
120
+ "rgt", "quote", "template"
121
+ ]
122
+ magic_fields << current_class.table_name + "_count" if current_class.respond_to? 'table_name'
123
+ content_columns = current_class.content_columns.select {|c| ! magic_fields.include? c.name}
124
+ else
125
+ content_columns = current_class.content_columns
126
+ end
127
+
128
+ content_columns.each do |a|
129
+ content_column = a.name
130
+ content_column += ' :' + a.type.to_s unless @options.hide_types
131
+ node_attribs << content_column
132
+ end
133
+ end
134
+ @graph.add_node [node_type, current_class.name, node_attribs]
135
+ generated = true
136
+ # Process class associations
137
+ associations = current_class.reflect_on_all_associations
138
+ if @options.inheritance && ! @options.transitive
139
+ superclass_associations = current_class.superclass.reflect_on_all_associations
140
+
141
+ associations = associations.select{|a| ! superclass_associations.include? a}
142
+ # This doesn't works!
143
+ # associations -= current_class.superclass.reflect_on_all_associations
144
+ end
145
+ associations.each do |a|
146
+ process_association current_class.name, a
147
+ end
148
+ elsif @options.all && (current_class.is_a? Class)
149
+ # Not ActiveRecord::Base model
150
+ node_type = @options.brief ? 'class-brief' : 'class'
151
+ @graph.add_node [node_type, current_class.name]
152
+ generated = true
153
+ elsif @options.modules && (current_class.is_a? Module)
154
+ @graph.add_node ['module', current_class.name]
155
+ end
156
+
157
+ # Only consider meaningful inheritance relations for generated classes
158
+ if @options.inheritance && generated &&
159
+ (current_class.superclass != ActiveRecord::Base) &&
160
+ (current_class.superclass != Object)
161
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
162
+ end
163
+
164
+ STDERR.print "\tDone #{current_class}\n" if @options.verbose
165
+ end # process_class
166
+
167
+ # Process a model association
168
+ def process_association(class_name, assoc)
169
+ begin
170
+ STDERR.print "\t\tProcessing model association #{assoc.name.to_s} ..." if @options.verbose
171
+
172
+ # Skip "belongs_to" associations
173
+ return if assoc.macro.to_s == 'belongs_to'
174
+
175
+ # Only non standard association names needs a label
176
+
177
+ # from patch #12384
178
+ # if assoc.class_name == assoc.name.to_s.singularize.camelize
179
+ assoc_class_name = (assoc.class_name.respond_to? 'underscore') ? assoc.class_name.underscore.singularize.camelize : assoc.class_name
180
+ if assoc_class_name == assoc.name.to_s.singularize.camelize
181
+ assoc_name = ''
182
+ else
183
+ assoc_name = assoc.name.to_s
184
+ end
185
+ # STDERR.print "#{assoc_name}\n"
186
+ if assoc.macro.to_s == 'has_one'
187
+ assoc_type = 'one-one'
188
+ elsif assoc.macro.to_s == 'has_many' && (! assoc.options[:through])
189
+ assoc_type = 'one-many'
190
+ else # habtm or has_many, :through
191
+ return if @habtm.include? [assoc.class_name, class_name, assoc_name]
192
+ assoc_type = 'many-many'
193
+ @habtm << [class_name, assoc.class_name, assoc_name]
194
+ end
195
+ # from patch #12384
196
+ # @graph.add_edge [assoc_type, class_name, assoc.class_name, assoc_name]
197
+ @graph.add_edge [assoc_type, class_name, assoc_class_name, assoc_name]
198
+ ensure
199
+ STDERR.print " done.\n" if @options.verbose
200
+ end
201
+ end # process_association
202
+
203
+ end # class ModelsDiagram
@@ -0,0 +1,181 @@
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 'ostruct'
8
+
9
+ # RailRoad command line options parser
10
+ class OptionsStruct < OpenStruct
11
+
12
+ require 'optparse'
13
+
14
+ def initialize
15
+ init_options = { :all => false,
16
+ :brief => false,
17
+ :exclude => [],
18
+ :classes_by_files => {},
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
+ :hide_underscore => false,
29
+ :plugins_models => false,
30
+ :libraries => false,
31
+ :root => '',
32
+ :transitive => false,
33
+ :verbose => false,
34
+ :xmi => false,
35
+ :command => '' }
36
+ super(init_options)
37
+ end # initialize
38
+
39
+ def parse(args)
40
+ @opt_parser = OptionParser.new do |opts|
41
+ opts.banner = "Usage: #{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("-e", "--exclude=file1[,fileN]", Array, "Exclude given files") do |list|
49
+ self.exclude = list
50
+ end
51
+ opts.on("-c", "--class-map=file1,MyClass1[,fileN,MyClassN]", Array, "Map files to classes they contain") do |list|
52
+ self.classes_by_files = Hash[*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("--hide-magic", "Hide magic field names") do |h|
82
+ self.hide_magic = h
83
+ end
84
+ opts.on("--hide-types", "Hide attributes type") do |h|
85
+ self.hide_types = h
86
+ end
87
+ opts.on("-j", "--join", "Concentrate edges") do |j|
88
+ self.join = j
89
+ end
90
+ opts.on("-m", "--modules", "Include modules") do |m|
91
+ self.modules = m
92
+ end
93
+ opts.on("-p", "--plugins-models", "Include plugins models") do |p|
94
+ self.plugins_models = p
95
+ end
96
+ opts.on("-y", "--libraries", "Include application library") do |y|
97
+ self.libraries = y
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.on("--hide-underscore", "Hide methods that begin with an underscore") do |h|
115
+ self.hide_underscore = h
116
+ end
117
+ opts.separator ""
118
+ opts.separator "Other options:"
119
+ opts.on("-h", "--help", "Show this message") do
120
+ STDOUT.print "#{opts}\n"
121
+ exit
122
+ end
123
+ opts.on("--version", "Show version and copyright") do
124
+ STDOUT.print "#{APP_HUMAN_NAME} version #{APP_VERSION.join('.')}\n\n" +
125
+ "#{COPYRIGHT}\nThis is free software; see the source " +
126
+ "for copying conditions.\n\n"
127
+ exit
128
+ end
129
+ opts.separator ""
130
+ opts.separator "Commands (you must supply one of these):"
131
+ opts.on("-M", "--models", "Generate models diagram") do |c|
132
+ if self.command != ''
133
+ STDERR.print "Error: Can only generate one diagram type\n\n"
134
+ exit 1
135
+ else
136
+ self.command = 'models'
137
+ end
138
+ end
139
+ opts.on("-C", "--controllers", "Generate controllers diagram") do |c|
140
+ if self.command != ''
141
+ STDERR.print "Error: Can only generate one diagram type\n\n"
142
+ exit 1
143
+ else
144
+ self.command = 'controllers'
145
+ end
146
+ end
147
+ # From Ana Nelson's patch
148
+ opts.on("-A", "--aasm", "Generate \"acts as state machine\" diagram") do |c|
149
+ if self.command == 'controllers'
150
+ STDERR.print "Error: Can only generate one diagram type\n\n"
151
+ exit 1
152
+ else
153
+ self.command = 'aasm'
154
+ end
155
+ end
156
+ opts.separator ""
157
+ opts.separator "For bug reporting and additional information, please see:"
158
+ opts.separator "http://railroad.rubyforge.org/"
159
+ end # do
160
+
161
+ begin
162
+ @opt_parser.parse!(args)
163
+ rescue OptionParser::AmbiguousOption
164
+ option_error "Ambiguous option"
165
+ rescue OptionParser::InvalidOption
166
+ option_error "Invalid option"
167
+ rescue OptionParser::InvalidArgument
168
+ option_error "Invalid argument"
169
+ rescue OptionParser::MissingArgument
170
+ option_error "Missing argument"
171
+ end
172
+ end # parse
173
+
174
+ private
175
+
176
+ def option_error(msg)
177
+ STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
178
+ exit 1
179
+ end
180
+
181
+ end # class OptionsStruct