railroady 0.9.0

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,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