railroad 0.3.3 → 0.3.4

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.
data/ChangeLog CHANGED
@@ -1,6 +1,17 @@
1
+ Version 0.3.4 (apr 12 2007)
2
+ - Add support for model abstract classes.
3
+ (don't try to get content columns, bug #10033)
4
+ - Add verbose mode
5
+ - More code cleanup.
6
+ - Using an internal representation and then
7
+ generating the DOT output. This will allow to
8
+ add more output formats in the future.
9
+
10
+
1
11
  Version 0.3.3 (apr 10 2007)
2
12
  - Code cleanup
3
13
 
14
+
4
15
  Version 0.3.2 (apr 9 2007)
5
16
  - Disable STDOUT when loading applications classes, avoiding
6
17
  messing up the DOT output.
data/README CHANGED
@@ -22,6 +22,8 @@ Common options:
22
22
  -l, --label Add a label with diagram information
23
23
  (type, date, migration, version)
24
24
  -o, --output FILE Write diagram to file FILE
25
+ -v, --verbose Enable verbose output
26
+ (produce messages to STDOUT)
25
27
 
26
28
  Models diagram options:
27
29
  -a, --all Include all models
@@ -98,7 +100,7 @@ from Graphviz.
98
100
 
99
101
  = Website and Project Home
100
102
 
101
- http://railroad.rubyforge.org/
103
+ http://railroad.rubyforge.org
102
104
 
103
105
  = License
104
106
 
@@ -0,0 +1,41 @@
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 - 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,3,4]
20
+ COPYRIGHT = "Copyright (C) 2007 Javier Smaldone"
21
+
22
+ require 'railroad/options_struct'
23
+ require 'railroad/models_diagram'
24
+ require 'railroad/controllers_diagram'
25
+
26
+ options = OptionsStruct.new
27
+
28
+ options.parse ARGV
29
+
30
+ if options.command == 'models'
31
+ diagram = ModelsDiagram.new options
32
+ elsif options.command == 'controllers'
33
+ diagram = ControllersDiagram.new options
34
+ else
35
+ STDERR.print "Error: You must supply a command\n" +
36
+ " (try #{APP_NAME} -h)\n\n"
37
+ exit 1
38
+ end
39
+
40
+ diagram.generate
41
+ diagram.print
@@ -0,0 +1,83 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007 - 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
+ STDERR.print "Generating DOT graph\n" if @options.verbose
37
+
38
+ STDOUT.print @graph.to_dot
39
+
40
+ if @options.output
41
+ STDOUT.reopen(old_stdout)
42
+ end
43
+ end # print
44
+
45
+ private
46
+
47
+ # Prevents Rails application from writing to STDOUT
48
+ def disable_stdout
49
+ @old_stdout = STDOUT.dup
50
+ STDOUT.reopen(PLATFORM =~ /mswin/ ? "NUL" : "/dev/null")
51
+ end
52
+
53
+ # Restore STDOUT
54
+ def enable_stdout
55
+ STDOUT.reopen(@old_stdout)
56
+ end
57
+
58
+
59
+ # Print error when loading Rails application
60
+ def print_error(type)
61
+ STDERR.print "Error loading #{type}.\n (Are you running " +
62
+ "#{APP_NAME} on the aplication's root directory?)\n\n"
63
+ end
64
+
65
+ # Load Rails application's environment
66
+ def load_environment
67
+ begin
68
+ disable_stdout
69
+ require "config/environment"
70
+ enable_stdout
71
+ rescue LoadError
72
+ enable_stdout
73
+ print_error "application environment"
74
+ raise
75
+ end
76
+ end
77
+
78
+ # Extract class name from filename
79
+ def extract_class_name(filename)
80
+ filename.split('/')[2..-1].join('/').split('.').first.camelize
81
+ end
82
+
83
+ end # class AppDiagram
@@ -0,0 +1,81 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007 - 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
+ super options
14
+ @graph.diagram_type = 'Controllers'
15
+ end
16
+
17
+ # Process controller files
18
+ def generate
19
+ STDERR.print "Generating controllers diagram\n" if @options.verbose
20
+
21
+ files = Dir.glob("app/controllers/**/*_controller.rb")
22
+ files << 'app/controllers/application.rb'
23
+ files.each do |f|
24
+ class_name = extract_class_name(f)
25
+ # ApplicationController's file is 'application.rb'
26
+ class_name += 'Controller' if class_name == 'Application'
27
+ process_class class_name.constantize
28
+ end
29
+ end # generate
30
+
31
+ private
32
+
33
+ # Load controller classes
34
+ def load_classes
35
+ begin
36
+ disable_stdout
37
+ # ApplicationController must be loaded first
38
+ require "app/controllers/application.rb"
39
+ Dir.glob("app/controllers/**/*_controller.rb") {|c| require c }
40
+ enable_stdout
41
+ rescue LoadError
42
+ enable_stdout
43
+ print_error "controller classes"
44
+ raise
45
+ end
46
+ end # load_classes
47
+
48
+ # Proccess a controller class
49
+ def process_class(current_class)
50
+
51
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
52
+
53
+ if @options.brief
54
+ @graph.add_node ['controller-brief', current_class.name]
55
+ elsif current_class.is_a? Class
56
+ # Collect controller's methods
57
+ node_attribs = {:public => [],
58
+ :protected => [],
59
+ :private => []}
60
+ current_class.public_instance_methods(false).sort.each { |m|
61
+ node_attribs[:public] << m
62
+ } unless @options.hide_public
63
+ current_class.protected_instance_methods(false).sort.each { |m|
64
+ node_attribs[:protected] << m
65
+ } unless @options.hide_protected
66
+ current_class.private_instance_methods(false).sort.each { |m|
67
+ node_attribs[:private] << m
68
+ } unless @options.hide_private
69
+ @graph.add_node ['controller', current_class.name, node_attribs]
70
+ elsif @options.modules && current_class.is_a?(Module)
71
+ @graph.add_node ['module', current_class.name]
72
+ end
73
+
74
+ # Generate the inheritance edge (only for ApplicationControllers)
75
+ if @options.inheritance &&
76
+ (ApplicationController.subclasses.include? current_class.name)
77
+ @graph.add_edge ['is-a', current_class.name, current_class.superclass.name]
78
+ end
79
+ end # process_class
80
+
81
+ end # class ControllersDiagram
@@ -0,0 +1,119 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007 - 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
+ private
44
+
45
+ # Build DOT diagram header
46
+ def dot_header
47
+ result = "digraph #{@diagram_type.downcase}_diagram {\n" +
48
+ "\tgraph[overlap=false, splines=true]\n"
49
+ result += dot_label if @show_label
50
+ return result
51
+ end
52
+
53
+ # Build DOT diagram footer
54
+ def dot_footer
55
+ return "}\n"
56
+ end
57
+
58
+ # Build diagram label
59
+ def dot_label
60
+ return "\t_diagram_info [shape=\"plaintext\", " +
61
+ "label=\"Diagram: #{@diagram_type}\\l" +
62
+ "Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l" +
63
+ "Migration version: " +
64
+ "#{ActiveRecord::Migrator.current_version}\\l" +
65
+ "Generated by #{APP_HUMAN_NAME} #{APP_VERSION.join('.')}"+
66
+ "\\l\", fontsize=14]\n"
67
+ end
68
+
69
+ # Build a DOT graph node
70
+ def dot_node(type, name, attributes=nil)
71
+ case type
72
+ when 'model'
73
+ options = 'shape=Mrecord, label="{' + name + '|'
74
+ options += attributes.join('\l')
75
+ options += '\l}"'
76
+ when 'model-brief'
77
+ options = ''
78
+ when 'class'
79
+ options = 'shape=record, label="{' + name + '|}"'
80
+ when 'class-brief'
81
+ options = 'shape=box'
82
+ when 'controller'
83
+ options = 'shape=Mrecord, label="{' + name + '|'
84
+ public_methods = attributes[:public].join('\l')
85
+ protected_methods = attributes[:protected].join('\l')
86
+ private_methods = attributes[:private].join('\l')
87
+ options += public_methods + '\l|' + protected_methods + '\l|' +
88
+ private_methods + '\l'
89
+ options += '}"'
90
+ when 'controller-brief'
91
+ options = ''
92
+ when 'module'
93
+ options = 'shape=box, style=dotted, label="' + name + '"'
94
+ end # case
95
+ return "\t#{quote(name)} [#{options}]\n"
96
+ end # dot_node
97
+
98
+ # Build a DOT graph edge
99
+ def dot_edge(type, from, to, name = '')
100
+ options = name != '' ? "label=\"#{name}\", " : ''
101
+ case type
102
+ when 'one-one'
103
+ options += 'taillabel="1"'
104
+ when 'one-many'
105
+ options += 'taillabel="n"'
106
+ when 'many-many'
107
+ options += 'taillabel="n", headlabel="n", arrowtail="normal"'
108
+ when 'is-a'
109
+ options += 'arrowhead="onormal"'
110
+ end
111
+ return "\t#{quote(from)} -> #{quote(to)} [#{options}]\n"
112
+ end # dot_edge
113
+
114
+ # Quotes a class name
115
+ def quote(name)
116
+ '"' + name + '"'
117
+ end
118
+
119
+ end # class DiagramGraph
@@ -0,0 +1,115 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007 - 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
+ super options
14
+ @graph.diagram_type = 'Models'
15
+ # Processed habtm associations
16
+ @habtm = []
17
+ end
18
+
19
+ # Process model files
20
+ def generate
21
+ STDERR.print "Generating models diagram\n" if @options.verbose
22
+ files = Dir.glob("app/models/**/*.rb")
23
+ files.each do |f|
24
+ process_class extract_class_name(f).constantize
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Load model classes
31
+ def load_classes
32
+ begin
33
+ disable_stdout
34
+ Dir.glob("app/models/**/*.rb") {|m| require m }
35
+ enable_stdout
36
+ rescue LoadError
37
+ enable_stdout
38
+ print_error "model classes"
39
+ raise
40
+ end
41
+ end # load_classes
42
+
43
+ # Process a model class
44
+ def process_class(current_class)
45
+
46
+ STDERR.print "\tProcessing #{current_class}\n" if @options.verbose
47
+
48
+ generated = false
49
+
50
+ # Is current_clas derived from ActiveRecord::Base?
51
+ if current_class.respond_to?'reflect_on_all_associations'
52
+ node_attribs = []
53
+ if @options.brief || current_class.abstract_class?
54
+ node_type = 'model-brief'
55
+ else
56
+ node_type = 'model'
57
+ # Collect model's content columns
58
+ current_class.content_columns.each do |a|
59
+ content_column = a.name
60
+ content_column += ' :' + a.type.to_s unless @options.hide_types
61
+ node_attribs << content_column
62
+ end
63
+ end
64
+ @graph.add_node [node_type, current_class.name, node_attribs]
65
+ generated = true
66
+ # Process class associations
67
+ current_class.reflect_on_all_associations.each do |a|
68
+ process_association current_class.name, a
69
+ end
70
+ elsif @options.all && (current_class.is_a? Class)
71
+ # Not ActiveRecord::Base model
72
+ node_type = @options.brief ? 'class-brief' : 'class'
73
+ @graph.add_node [node_type, current_class.name]
74
+ generated = true
75
+ elsif @options.modules && (current_class.is_a? Module)
76
+ @graph.add_node ['module', current_class.name]
77
+ end
78
+
79
+ # Only consider meaningful inheritance relations for generated classes
80
+ if @options.inheritance && generated &&
81
+ (current_class.superclass != ActiveRecord::Base) &&
82
+ (current_class.superclass != Object)
83
+ @graph.add_edge ['is-a', current_class.name, current_class.superclass.name]
84
+ end
85
+
86
+ end # process_class
87
+
88
+ # Process a model association
89
+ def process_association(class_name, assoc)
90
+
91
+ STDERR.print "\t\tProcessing model association #{assoc.name.to_s}\n" if @options.verbose
92
+
93
+ # Skip "belongs_to" associations
94
+ return if assoc.macro.to_s == 'belongs_to'
95
+
96
+ # Only non standard association names needs a label
97
+ if assoc.class_name == assoc.name.to_s.singularize.camelize
98
+ assoc_name = ''
99
+ else
100
+ assoc_name = assoc.name.to_s
101
+ end
102
+
103
+ if assoc.macro.to_s == 'has_one'
104
+ assoc_type = 'one-one'
105
+ elsif assoc.macro.to_s == 'has_many' && (! assoc.options[:through])
106
+ assoc_type = 'one-many'
107
+ else # habtm or has_many, :through
108
+ return if @habtm.include? [assoc.class_name, class_name, assoc_name]
109
+ assoc_type = 'many-many'
110
+ @habtm << [class_name, assoc.class_name, assoc_name]
111
+ end
112
+ @graph.add_edge [assoc_type, class_name, assoc.class_name, assoc_name]
113
+ end # process_association
114
+
115
+ end # class ModelsDiagram
@@ -0,0 +1,138 @@
1
+ # RailRoad - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007 - 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
+ :inheritance => false,
18
+ :join => false,
19
+ :label => false,
20
+ :modules => false,
21
+ :hide_types => false,
22
+ :hide_public => false,
23
+ :hide_protected => false,
24
+ :hide_private => false,
25
+ :verbose => false,
26
+ :command => '' }
27
+ super(init_options)
28
+ end # initialize
29
+
30
+ def parse(args)
31
+ @opt_parser = OptionParser.new do |opts|
32
+ opts.banner = "Usage: #{APP_NAME} [options] command"
33
+ opts.separator ""
34
+ opts.separator "Common options:"
35
+ opts.on("-b", "--brief", "Generate compact diagram",
36
+ " (no attributes nor methods)") do |b|
37
+ self.brief = b
38
+ end
39
+ opts.on("-i", "--inheritance", "Include inheritance relations") do |i|
40
+ self.inheritance = i
41
+ end
42
+ opts.on("-l", "--label", "Add a label with diagram information",
43
+ " (type, date, migration, version)") do |l|
44
+ self.label = l
45
+ end
46
+ opts.on("-o", "--output FILE", "Write diagram to file FILE") do |f|
47
+ self.output = f
48
+ end
49
+ opts.on("-v", "--verbose", "Enable verbose output",
50
+ " (produce messages to STDOUT)") do |v|
51
+ self.verbose = v
52
+ end
53
+ opts.separator ""
54
+ opts.separator "Models diagram options:"
55
+ opts.on("-a", "--all", "Include all models",
56
+ " (not only ActiveRecord::Base derived)") do |a|
57
+ self.all = a
58
+ end
59
+ opts.on("--hide-types", "Hide attributes type") do |h|
60
+ self.hide_types = h
61
+ end
62
+ opts.on("-j", "--join", "Concentrate edges") do |j|
63
+ self.join = j
64
+ end
65
+ opts.on("-m", "--modules", "Include modules") do |m|
66
+ self.modules = m
67
+ end
68
+ opts.separator ""
69
+ opts.separator "Controllers diagram options:"
70
+ opts.on("--hide-public", "Hide public methods") do |h|
71
+ self.hide_public = h
72
+ end
73
+ opts.on("--hide-protected", "Hide protected methods") do |h|
74
+ self.hide_protected = h
75
+ end
76
+ opts.on("--hide-private", "Hide private methods") do |h|
77
+ self.hide_private = h
78
+ end
79
+ opts.separator ""
80
+ opts.separator "Other options:"
81
+ opts.on("-h", "--help", "Show this message") do
82
+ STDOUT.print "#{opts}\n"
83
+ exit
84
+ end
85
+ opts.on("--version", "Show version and copyright") do
86
+ STDOUT.print "#{APP_HUMAN_NAME} version #{APP_VERSION.join('.')}\n\n" +
87
+ "#{COPYRIGHT}\nThis is free software; see the source " +
88
+ "for copying conditions.\n\n"
89
+ exit
90
+ end
91
+ opts.separator ""
92
+ opts.separator "Commands (you must supply one of these):"
93
+ opts.on("-M", "--models", "Generate models diagram") do |c|
94
+ if self.command == 'controllers'
95
+ STDERR.print "Error: Can't generate models AND " +
96
+ "controllers diagram\n\n"
97
+ exit 1
98
+ else
99
+ self.command = 'models'
100
+ end
101
+ end
102
+ opts.on("-C", "--controllers", "Generate controllers diagram") do |c|
103
+ if self.command == 'models'
104
+ STDERR.print "Error: Can't generate models AND " +
105
+ "controllers diagram\n\n"
106
+ exit 1
107
+ else
108
+ self.command = 'controllers'
109
+ end
110
+ end
111
+ opts.separator ""
112
+ opts.separator "For bug reporting and additional information, please see:"
113
+ opts.separator "http://railroad.rubyforge.org"
114
+ end # do
115
+
116
+ begin
117
+ @opt_parser.parse!(args)
118
+ rescue OptionParser::AmbiguousOption
119
+ option_error "Ambiguous option"
120
+ rescue OptionParser::InvalidOption
121
+ option_error "Invalid option"
122
+ rescue OptionParser::InvalidArgument
123
+ option_error "Invalid argument"
124
+ rescue OptionParser::MissingArgument
125
+ option_error "Missing argument"
126
+ rescue
127
+ option_error "Unknown error"
128
+ end
129
+ end # parse
130
+
131
+ private
132
+
133
+ def option_error(msg)
134
+ STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
135
+ exit 1
136
+ end
137
+
138
+ end # class OptionsStruct
@@ -1,16 +1,17 @@
1
1
  require 'rubygems'
2
2
  SPEC = Gem::Specification.new do |s|
3
3
  s.name = "railroad"
4
- s.version = "0.3.3"
4
+ s.version = "0.3.4"
5
5
  s.author = "Javier Smaldone"
6
6
  s.email = "javier@smaldone.com.ar"
7
7
  s.homepage = "http://railroad.rubyforge.org"
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.summary = "A DOT diagram generator for Ruby on Rail applications"
10
- s.files = ["lib/railroad", "ChangeLog", "COPYING", "rake.gemspec"]
11
- s.bindir = "lib"
10
+ s.files = Dir.glob("lib/railroad/*.rb") +
11
+ ["ChangeLog", "COPYING", "rake.gemspec"]
12
+ s.bindir = "bin"
12
13
  s.executables = ["railroad"]
13
14
  s.default_executable = "railroad"
14
15
  s.has_rdoc = true
15
- s.extra_rdoc_files = ["README", "lib/railroad"]
16
+ s.extra_rdoc_files = ["README", "COPYING"]
16
17
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: railroad
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.3
7
- date: 2007-04-10 00:00:00 -03:00
6
+ version: 0.3.4
7
+ date: 2007-04-12 00:00:00 -03:00
8
8
  summary: A DOT diagram generator for Ruby on Rail applications
9
9
  require_paths:
10
10
  - lib
@@ -14,7 +14,7 @@ rubyforge_project:
14
14
  description:
15
15
  autorequire:
16
16
  default_executable: railroad
17
- bindir: lib
17
+ bindir: bin
18
18
  has_rdoc: true
19
19
  required_ruby_version: !ruby/object:Gem::Version::Requirement
20
20
  requirements:
@@ -29,7 +29,11 @@ post_install_message:
29
29
  authors:
30
30
  - Javier Smaldone
31
31
  files:
32
- - lib/railroad
32
+ - lib/railroad/models_diagram.rb
33
+ - lib/railroad/app_diagram.rb
34
+ - lib/railroad/controllers_diagram.rb
35
+ - lib/railroad/diagram_graph.rb
36
+ - lib/railroad/options_struct.rb
33
37
  - ChangeLog
34
38
  - COPYING
35
39
  - rake.gemspec
@@ -40,7 +44,7 @@ rdoc_options: []
40
44
 
41
45
  extra_rdoc_files:
42
46
  - README
43
- - lib/railroad
47
+ - COPYING
44
48
  executables:
45
49
  - railroad
46
50
  extensions: []
@@ -1,459 +0,0 @@
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 - 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,3,3]
20
- COPYRIGHT = "Copyright (C) 2007 Javier Smaldone"
21
-
22
- require 'ostruct'
23
-
24
- # Command line options structure
25
- class OptionsStruct < OpenStruct
26
-
27
- require 'optparse'
28
-
29
- def initialize
30
- init_options = { :all => false,
31
- :brief => false,
32
- :inheritance => false,
33
- :join => false,
34
- :label => false,
35
- :modules => false,
36
- :hide_types => false,
37
- :hide_public => false,
38
- :hide_protected => false,
39
- :hide_private => false,
40
- :command => '' }
41
- super(init_options)
42
- end # initialize
43
-
44
- def parse(args)
45
- @opt_parser = OptionParser.new do |opts|
46
- opts.banner = "Usage: #{APP_NAME} [options] command"
47
- opts.separator ""
48
- opts.separator "Common options:"
49
- opts.on("-b", "--brief", "Generate compact diagram",
50
- " (no attributes nor methods)") do |b|
51
- self.brief = b
52
- end
53
- opts.on("-i", "--inheritance", "Include inheritance relations") do |i|
54
- self.inheritance = i
55
- end
56
- opts.on("-l", "--label", "Add a label with diagram information",
57
- " (type, date, migration, version)") do |l|
58
- self.label = l
59
- end
60
- opts.on("-o", "--output FILE", "Write diagram to file FILE") do |f|
61
- self.output = f
62
- end
63
- opts.separator ""
64
- opts.separator "Models diagram options:"
65
- opts.on("-a", "--all", "Include all models",
66
- " (not only ActiveRecord::Base derived)") do |a|
67
- self.all = a
68
- end
69
- opts.on("--hide-types", "Hide attributes type") do |h|
70
- self.hide_types = h
71
- end
72
- opts.on("-j", "--join", "Concentrate edges") do |j|
73
- self.join = j
74
- end
75
- opts.on("-m", "--modules", "Include modules") do |m|
76
- self.modules = m
77
- end
78
- opts.separator ""
79
- opts.separator "Controllers diagram options:"
80
- opts.on("--hide-public", "Hide public methods") do |h|
81
- self.hide_public = h
82
- end
83
- opts.on("--hide-protected", "Hide protected methods") do |h|
84
- @options.hide_protected = h
85
- end
86
- opts.on("--hide-private", "Hide private methods") do |h|
87
- @options.hide_private = h
88
- end
89
- opts.separator ""
90
- opts.separator "Other options:"
91
- opts.on("-h", "--help", "Show this message") do
92
- print "#{opts}\n"
93
- exit
94
- end
95
- opts.on("--version", "Show version and copyright") do
96
- print "#{APP_HUMAN_NAME} version #{APP_VERSION.join('.')}\n\n" +
97
- "#{COPYRIGHT}\n" +
98
- "This is free software; see the source for copying conditions.\n\n"
99
- exit
100
- end
101
- opts.separator ""
102
- opts.separator "Commands (you must supply one of these):"
103
- opts.on("-M", "--models", "Generate models diagram") do |c|
104
- if self.command == 'controllers'
105
- STDERR.print "Error: Can't generate models AND " +
106
- "controllers diagram\n\n"
107
- exit 1
108
- else
109
- self.command = 'models'
110
- end
111
- end
112
- opts.on("-C", "--controllers", "Generate controllers diagram") do |c|
113
- if self.command == 'models'
114
- STDERR.print "Error: Can't generate models AND " +
115
- "controllers diagram\n\n"
116
- exit 1
117
- else
118
- self.command = 'controllers'
119
- end
120
- end
121
- opts.separator ""
122
- opts.separator "For bug reporting and additional information, please see:"
123
- opts.separator "http://railroad.rubyforge.org"
124
- end
125
-
126
- begin
127
- @opt_parser.parse!(args)
128
- rescue OptionParser::AmbiguousOption
129
- option_error "Ambiguous option"
130
- rescue OptionParser::InvalidOption
131
- option_error "Invalid option"
132
- rescue OptionParser::InvalidArgument
133
- option_error "Invalid argument"
134
- rescue OptionParser::MissingArgument
135
- option_error "Missing argument"
136
- rescue
137
- option_error "Unknown error"
138
- end
139
- end # initialize
140
-
141
- private
142
-
143
- def option_error(msg)
144
- STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
145
- exit 1
146
- end
147
-
148
- end # class OptionsStruct
149
-
150
-
151
- # Root class for RailRoad diagrams
152
- class AppDiagram
153
-
154
- def initialize(options)
155
- @options = options
156
- load_environment
157
- load_classes
158
- end
159
-
160
- private
161
-
162
- # Quotes a class name
163
- def node(name)
164
- '"' + name + '"'
165
- end
166
-
167
- # Prevents Rails application from writing to STDOUT
168
- def disable_stdout
169
- @old_stdout = STDOUT.dup
170
- STDOUT.reopen( PLATFORM =~ /mswin/ ? "NUL" : "/dev/null" )
171
- end
172
-
173
- # Restore STDOUT
174
- def enable_stdout
175
- STDOUT.reopen(@old_stdout)
176
- end
177
-
178
- # Print diagram header
179
- def print_header(type)
180
- print "digraph #{type.downcase}_diagram {\n" +
181
- "\tgraph[overlap=false, splines=true]\n"
182
- print_info(type) if @options.label
183
- end
184
-
185
-
186
- # Print diagram label
187
- def print_info(type)
188
- print "\t_diagram_info [shape=\"plaintext\", label=\"Diagram: #{type}\\l" +
189
- "Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l" +
190
- "Migration version: #{ActiveRecord::Migrator.current_version}\\l" +
191
- "Generated by #{APP_HUMAN_NAME} #{APP_VERSION.join('.')}\\l\""+
192
- ", fontsize=14]\n"
193
- end
194
-
195
- # Print an edge
196
- def print_edge(from, to, attributes)
197
- print "\t#{node(from)} -> #{node(to)} [#{attributes}]\n"
198
- end
199
-
200
- # Print a node
201
- def print_node(name, attributes=nil)
202
- print "\t#{node(name)}"
203
- print " [#{attributes}]" if attributes && attributes != ''
204
- print "\n"
205
- end
206
-
207
- # Print error when loading Rails application
208
- def print_error(type)
209
- STDERR.print "Error loading #{type}.\n (Are you running " +
210
- "#{APP_NAME} on the aplication's root directory?)\n\n"
211
- end
212
-
213
- # Load Rails application's environment
214
- def load_environment
215
- begin
216
- disable_stdout
217
- require "config/environment"
218
- enable_stdout
219
- rescue LoadError
220
- enable_stdout
221
- print_error "application environment"
222
- raise
223
- end
224
- end
225
-
226
- end # class AppDiagram
227
-
228
-
229
- # RailRoad models diagram
230
- class ModelsDiagram < AppDiagram
231
-
232
- def initialize(options)
233
- super options
234
- # Processed habtm associations
235
- @habtm = []
236
- end
237
-
238
- # Generate models diagram
239
- def generate
240
- print_header 'Models'
241
- models_files = Dir.glob("app/models/**/*.rb")
242
- models_files.each do |m|
243
- # Extract the class name from the file name
244
- class_name = m.split('/')[2..-1].join('/').split('.').first.camelize
245
- process_class class_name.constantize
246
- end
247
- print "}\n"
248
- end # generate
249
-
250
- private
251
-
252
- # Load model classes
253
- def load_classes
254
- begin
255
- disable_stdout
256
- Dir.glob("app/models/**/*.rb") {|m| require m }
257
- enable_stdout
258
- rescue LoadError
259
- enable_stdout
260
- print_error "model classes"
261
- raise
262
- end
263
- end # load_classes
264
-
265
- # Process model class
266
- def process_class(current_class)
267
-
268
- class_printed = false
269
-
270
- # Is current_clas derived from ActiveRecord::Base?
271
- if current_class.respond_to?'reflect_on_all_associations'
272
-
273
- # Print the node
274
- if @options.brief
275
- node_attrib = ''
276
- else
277
- node_attrib = 'shape=Mrecord, label="{' + current_class.name + '|'
278
- current_class.content_columns.each do |a|
279
- node_attrib += a.name
280
- node_attrib += ' :' + a.type.to_s unless @options.hide_types
281
- node_attrib += '\l'
282
- end
283
- node_attrib += '}"'
284
- end
285
- print_node current_class.name, node_attrib
286
- class_printed = true
287
- # Iterate over the class associations
288
- current_class.reflect_on_all_associations.each do |a|
289
- process_association(current_class, a)
290
- end
291
- elsif @options.all && (current_class.is_a? Class)
292
- # Not ActiveRecord::Base model
293
- if @options.brief
294
- node_attrib = 'shape=box'
295
- else
296
- node_attrib = 'shape=record, label="{' + current_class.name + '|}"'
297
- end
298
- print_node current_class.name, node_attrib
299
- class_printed = true
300
- elsif @options.modules && (current_class.is_a? Module)
301
- print_node current_class.name,
302
- 'shape=box, style=dotted, label="' + current_class.name + '"'
303
- end
304
-
305
- # Only consider meaningful inheritance relations for printed classes
306
- if @options.inheritance && class_printed &&
307
- (current_class.superclass != ActiveRecord::Base) &&
308
- (current_class.superclass != Object)
309
- print_edge(current_class.name, current_class.superclass.name,
310
- 'arrowhead="onormal"')
311
- end
312
-
313
- end # process_class
314
-
315
- # Process model association
316
- def process_association(current_class, association)
317
-
318
- # Skip "belongs_to" associations
319
- return if association.macro.to_s == 'belongs_to'
320
-
321
- assoc_attrib = ""
322
- assoc_name = ""
323
- # Only non standard association names needs a label
324
- if association.class_name != association.name.to_s.singularize.camelize
325
- association_name = association.name.to_s
326
- assoc_attrib += 'label="' + association_name + '", '
327
- end
328
-
329
- # Tail label with the association arity
330
- assoc_attrib += association.macro.to_s == 'has_one' ? 'taillabel="1"' : 'taillabel="n"'
331
-
332
- # Use double-headed arrows for habtm and has_many, :through
333
- if association.macro.to_s == 'has_and_belongs_to_many' ||
334
- (association.macro.to_s == 'has_many' && association.options[:through])
335
-
336
- # Skip habtm associations already considered
337
- return if @habtm.include? [association.class_name, current_class.name,
338
- association_name]
339
- @habtm << [current_class.name, association.class_name,association_name]
340
- assoc_attrib += ', headlabel="n", arrowtail="normal"'
341
- end
342
- print_edge(current_class.name, association.class_name, assoc_attrib)
343
- end # process_association
344
-
345
- end # class ModelsDiagram
346
-
347
-
348
- # RailRoad controllers diagram
349
- class ControllersDiagram < AppDiagram
350
-
351
- def initialize(options)
352
- super options
353
- @app_controller = ApplicationController
354
- end
355
-
356
- # Generate controllers diagram
357
- def generate
358
-
359
- print_header 'Controllers'
360
-
361
- controllers_files = Dir.glob("app/controllers/**/*_controller.rb")
362
- controllers_files << 'app/controllers/application.rb'
363
- controllers_files.each do |c|
364
- # Extract the class name from the file name
365
- class_name = c.split('/')[2..-1].join('/').split('.').first.camelize
366
- # ApplicationController's file is 'application.rb'
367
- class_name += 'Controller' if class_name == 'Application'
368
- process_class class_name.constantize
369
- end
370
- print "}\n"
371
- end # generate
372
-
373
- private
374
-
375
- # Load controller classes
376
- def load_classes
377
- begin
378
- disable_stdout
379
- # ApplicationController must be loaded first
380
- require "app/controllers/application.rb"
381
- Dir.glob("app/controllers/**/*_controller.rb") {|c| require c }
382
- enable_stdout
383
- rescue LoadError
384
- enable_stdout
385
- print_error "controller classes"
386
- raise
387
- end
388
- end # load_classes
389
-
390
- # Proccess controller class
391
- def process_class(current_class)
392
-
393
- if @options.brief
394
- print_node current_class.name
395
-
396
- elsif current_class.is_a? Class
397
- node_attrib = 'shape=Mrecord, label="{' + current_class.name + '|'
398
- current_class.public_instance_methods(false).sort.each { |m|
399
- node_attrib += m + '\l'
400
- } unless @options.hide_public
401
- node_attrib += '|'
402
- current_class.protected_instance_methods(false).sort.each { |m|
403
- node_attrib += m + '\l'
404
- } unless @options.hide_protected
405
- node_attrib += '|'
406
- current_class.private_instance_methods(false).sort.each { |m|
407
- node_attrib += m + '\l'
408
- } unless @options.hide_private
409
- node_attrib += '}"'
410
- print_node current_class.name, node_attrib
411
-
412
- elsif @options.modules && current_class.is_a?(Module)
413
- print_node current_class.name,
414
- 'shape=box, style=dotted, label="' + current_class.name + '"'
415
- end
416
-
417
- # Print the inheritance edge (only for ApplicationControllers)
418
- if @options.inheritance &&
419
- (@app_controller.subclasses.include? current_class.name)
420
- print_edge(current_class.name, current_class.superclass.name,
421
- 'arrowhead="onormal"')
422
- end
423
-
424
- end # process_class
425
-
426
- end # class ControllersDiagram
427
-
428
-
429
- # Main program
430
-
431
- options = OptionsStruct.new
432
-
433
- options.parse ARGV
434
-
435
- if options.command == 'models'
436
- diagram = ModelsDiagram.new options
437
- elsif options.command == 'controllers'
438
- diagram = ControllersDiagram.new options
439
- else
440
- STDERR.print "Error: You must supply a command\n" +
441
- " (try #{APP_NAME} -h)\n\n"
442
- exit 1
443
- end
444
-
445
- if options.output
446
- old_stdout = STDOUT.dup
447
- begin
448
- STDOUT.reopen(options.output)
449
- rescue
450
- STDERR.print "Error: Cannot write diagram to #{options.output}\n\n"
451
- exit 2
452
- end
453
- end
454
-
455
- diagram.generate
456
-
457
- if options.output
458
- STDOUT.reopen(old_stdout)
459
- end