railroad 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
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