lib/railroad in railroad-0.3.2 vs lib/railroad in railroad-0.3.3
- old
+ new
@@ -14,80 +14,79 @@
# (at your option) any later version.
#
APP_NAME = "railroad"
APP_HUMAN_NAME = "RailRoad"
-APP_VERSION = [0,3,2]
+APP_VERSION = [0,3,3]
COPYRIGHT = "Copyright (C) 2007 Javier Smaldone"
+require 'ostruct'
-# Command line options parser
-class OptionsParser
+# Command line options structure
+class OptionsStruct < OpenStruct
require 'optparse'
- require 'ostruct'
- # Parse arguments from command line
- def self.parse(args)
+ def initialize
+ init_options = { :all => false,
+ :brief => false,
+ :inheritance => false,
+ :join => false,
+ :label => false,
+ :modules => false,
+ :hide_types => false,
+ :hide_public => false,
+ :hide_protected => false,
+ :hide_private => false,
+ :command => '' }
+ super(init_options)
+ end # initialize
- options = OpenStruct.new
-
- options.all = false
- options.brief = false
- options.inheritance = false
- options.join = false
- options.label = false
- options.modules = false
- options.hide_types = false
- options.hide_public = false
- options.hide_protected = false
- options.hide_private = false
- options.command = ''
-
- opts = OptionParser.new do |opts|
+ def parse(args)
+ @opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: #{APP_NAME} [options] command"
opts.separator ""
opts.separator "Common options:"
opts.on("-b", "--brief", "Generate compact diagram",
" (no attributes nor methods)") do |b|
- options.brief = b
+ self.brief = b
end
opts.on("-i", "--inheritance", "Include inheritance relations") do |i|
- options.inheritance = i
+ self.inheritance = i
end
opts.on("-l", "--label", "Add a label with diagram information",
" (type, date, migration, version)") do |l|
- options.label = l
+ self.label = l
end
opts.on("-o", "--output FILE", "Write diagram to file FILE") do |f|
- options.output = f
+ self.output = f
end
opts.separator ""
opts.separator "Models diagram options:"
opts.on("-a", "--all", "Include all models",
" (not only ActiveRecord::Base derived)") do |a|
- options.all = a
+ self.all = a
end
opts.on("--hide-types", "Hide attributes type") do |h|
- options.hide_types = h
+ self.hide_types = h
end
opts.on("-j", "--join", "Concentrate edges") do |j|
- options.join = j
+ self.join = j
end
opts.on("-m", "--modules", "Include modules") do |m|
- options.modules = m
+ self.modules = m
end
opts.separator ""
opts.separator "Controllers diagram options:"
opts.on("--hide-public", "Hide public methods") do |h|
- options.hide_public = h
+ self.hide_public = h
end
opts.on("--hide-protected", "Hide protected methods") do |h|
- options.hide_protected = h
+ @options.hide_protected = h
end
opts.on("--hide-private", "Hide private methods") do |h|
- options.hide_private = h
+ @options.hide_private = h
end
opts.separator ""
opts.separator "Other options:"
opts.on("-h", "--help", "Show this message") do
print "#{opts}\n"
@@ -100,62 +99,64 @@
exit
end
opts.separator ""
opts.separator "Commands (you must supply one of these):"
opts.on("-M", "--models", "Generate models diagram") do |c|
- if options.command == 'controllers'
- STDERR.print "Error: Can't generate models AND controllers diagram\n\n"
+ if self.command == 'controllers'
+ STDERR.print "Error: Can't generate models AND " +
+ "controllers diagram\n\n"
exit 1
else
- options.command = 'models'
+ self.command = 'models'
end
end
opts.on("-C", "--controllers", "Generate controllers diagram") do |c|
- if options.command == 'models'
- STDERR.print "Error: Can't generate models AND controllers diagram\n\n"
+ if self.command == 'models'
+ STDERR.print "Error: Can't generate models AND " +
+ "controllers diagram\n\n"
exit 1
else
- options.command = 'controllers'
+ self.command = 'controllers'
end
end
opts.separator ""
- opts.separator "For bug reporting instructions and additional information, please see:"
+ opts.separator "For bug reporting and additional information, please see:"
opts.separator "http://railroad.rubyforge.org"
end
begin
- opts.parse!(args)
+ @opt_parser.parse!(args)
rescue OptionParser::AmbiguousOption
- option_error "Ambiguous option", opts
+ option_error "Ambiguous option"
rescue OptionParser::InvalidOption
- option_error "Invalid option", opts
+ option_error "Invalid option"
rescue OptionParser::InvalidArgument
- option_error "Invalid argument", opts
+ option_error "Invalid argument"
rescue OptionParser::MissingArgument
- option_error "Missing argument", opts
+ option_error "Missing argument"
rescue
- option_error "Unknown error", opts
+ option_error "Unknown error"
end
- options
- end # self.parse
-
+ end # initialize
+
private
- def self.option_error(msg, opts)
- STDERR.print "Error: #{msg}\n\n #{opts}\n"
+ def option_error(msg)
+ STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
exit 1
end
-end # class OptionsParser
+end # class OptionsStruct
# Root class for RailRoad diagrams
class AppDiagram
def initialize(options)
@options = options
load_environment
+ load_classes
end
private
# Quotes a class name
@@ -172,21 +173,40 @@
# Restore STDOUT
def enable_stdout
STDOUT.reopen(@old_stdout)
end
+ # Print diagram header
+ def print_header(type)
+ print "digraph #{type.downcase}_diagram {\n" +
+ "\tgraph[overlap=false, splines=true]\n"
+ print_info(type) if @options.label
+ end
+
# Print diagram label
def print_info(type)
print "\t_diagram_info [shape=\"plaintext\", label=\"Diagram: #{type}\\l" +
"Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l" +
"Migration version: #{ActiveRecord::Migrator.current_version}\\l" +
"Generated by #{APP_HUMAN_NAME} #{APP_VERSION.join('.')}\\l\""+
", fontsize=14]\n"
end
- # Print error when loading application classes
+ # Print an edge
+ def print_edge(from, to, attributes)
+ print "\t#{node(from)} -> #{node(to)} [#{attributes}]\n"
+ end
+
+ # Print a node
+ def print_node(name, attributes=nil)
+ print "\t#{node(name)}"
+ print " [#{attributes}]" if attributes && attributes != ''
+ print "\n"
+ end
+
+ # Print error when loading Rails application
def print_error(type)
STDERR.print "Error loading #{type}.\n (Are you running " +
"#{APP_NAME} on the aplication's root directory?)\n\n"
end
@@ -196,43 +216,37 @@
disable_stdout
require "config/environment"
enable_stdout
rescue LoadError
enable_stdout
- print_error "app environment"
+ print_error "application environment"
raise
end
end
end # class AppDiagram
# RailRoad models diagram
class ModelsDiagram < AppDiagram
- # Generate models diagram
- def generate
-
- load_classes
-
- # DOT diagram header
- print "digraph models_diagram {\n"
- print "\tgraph[concentrate=true]\n" if @options.join
- print_info "Models" if @options.label
-
+ def initialize(options)
+ super options
# Processed habtm associations
@habtm = []
+ end
+ # Generate models diagram
+ def generate
+ print_header 'Models'
models_files = Dir.glob("app/models/**/*.rb")
models_files.each do |m|
# Extract the class name from the file name
- current_class = m.split('/')[2..-1].join('/').split('.').first.camelize.constantize
- process_class current_class
+ class_name = m.split('/')[2..-1].join('/').split('.').first.camelize
+ process_class class_name.constantize
end
print "}\n"
-
-
end # generate
private
# Load model classes
@@ -249,127 +263,111 @@
end # load_classes
# Process model class
def process_class(current_class)
- printed = false
+ class_printed = false
# Is current_clas derived from ActiveRecord::Base?
if current_class.respond_to?'reflect_on_all_associations'
# Print the node
if @options.brief
- print "\t#{node(current_class.name)}\n"
+ node_attrib = ''
else
- print "\t#{node(current_class.name)} " +
- "[shape=Mrecord, label=\"{#{current_class.name}|"
+ node_attrib = 'shape=Mrecord, label="{' + current_class.name + '|'
current_class.content_columns.each do |a|
- print "#{a.name}"
- print " :#{a.type.to_s}" unless @options.hide_types
- print "\\l"
+ node_attrib += a.name
+ node_attrib += ' :' + a.type.to_s unless @options.hide_types
+ node_attrib += '\l'
end
- print "}\"]\n"
+ node_attrib += '}"'
end
- printed = true
+ print_node current_class.name, node_attrib
+ class_printed = true
# Iterate over the class associations
- current_class.reflect_on_all_associations.each { |a|
+ current_class.reflect_on_all_associations.each do |a|
process_association(current_class, a)
- }
+ end
elsif @options.all && (current_class.is_a? Class)
# Not ActiveRecord::Base model
if @options.brief
- print "\t#{node(current_class.name)} [shape=box]\n"
+ node_attrib = 'shape=box'
else
- print "\t#{node(current_class.name)}" +
- "[shape=record, label=\"{#{current_class.name}|}\"]\n"
+ node_attrib = 'shape=record, label="{' + current_class.name + '|}"'
end
- printed = true
+ print_node current_class.name, node_attrib
+ class_printed = true
elsif @options.modules && (current_class.is_a? Module)
- print "\t#{node(current_class.name)}" +
- "[shape=box, style=dotted, label=\"#{current_class.name}\"]\n"
+ print_node current_class.name,
+ 'shape=box, style=dotted, label="' + current_class.name + '"'
end
# Only consider meaningful inheritance relations for printed classes
- if @options.inheritance && printed &&
+ if @options.inheritance && class_printed &&
(current_class.superclass != ActiveRecord::Base) &&
(current_class.superclass != Object)
- print "\t\t#{node(current_class.name)} -> " +
- "#{node(current_class.superclass.name)}" +
- " [arrowhead=\"onormal\"]\n"
+ print_edge(current_class.name, current_class.superclass.name,
+ 'arrowhead="onormal"')
end
end # process_class
# Process model association
def process_association(current_class, association)
+ # Skip "belongs_to" associations
+ return if association.macro.to_s == 'belongs_to'
+
+ assoc_attrib = ""
+ assoc_name = ""
# Only non standard association names needs a label
if association.class_name != association.name.to_s.singularize.camelize
- params = "label=\"#{association.name.to_s}\", "
- else
- params = ""
+ association_name = association.name.to_s
+ assoc_attrib += 'label="' + association_name + '", '
end
- # Skip "belongs_to" associations
- if association.macro.to_s != 'belongs_to'
- # Put a tail label with the association arity
- if association.macro.to_s == 'has_one'
- params += 'taillabel="1"'
- else
- params += 'taillabel="n"'
- end
+ # Tail label with the association arity
+ assoc_attrib += association.macro.to_s == 'has_one' ? 'taillabel="1"' : 'taillabel="n"'
- # Use double-headed arrows for habtm and has_many, :through
- if association.macro.to_s == 'has_and_belongs_to_many' ||
- (association.macro.to_s == 'has_many' && association.options[:through])
- if @habtm.include? "#{association.class_name} -> #{current_class.name}"
- # habtm association already considered
- return
- else
- params += ', headlabel="n", arrowtail="normal"'
- @habtm << "#{current_class.name} -> #{association.class_name}"
- end
- end
- # Print the edge
- print "\t\t#{node(current_class.name)} -> " +
- "#{node(association.class_name)} " +
- "[ #{params} ]\n"
- end
+ # Use double-headed arrows for habtm and has_many, :through
+ if association.macro.to_s == 'has_and_belongs_to_many' ||
+ (association.macro.to_s == 'has_many' && association.options[:through])
+
+ # Skip habtm associations already considered
+ return if @habtm.include? [association.class_name, current_class.name,
+ association_name]
+ @habtm << [current_class.name, association.class_name,association_name]
+ assoc_attrib += ', headlabel="n", arrowtail="normal"'
+ end
+ print_edge(current_class.name, association.class_name, assoc_attrib)
end # process_association
end # class ModelsDiagram
# RailRoad controllers diagram
class ControllersDiagram < AppDiagram
+
+ def initialize(options)
+ super options
+ @app_controller = ApplicationController
+ end
# Generate controllers diagram
def generate
- load_classes
+ print_header 'Controllers'
- # DOT diagram header
- print "digraph controllers_diagram {\n" +
- "\tgraph[overlap=false, splines=true]\n"
- print_info "Controllers" if @options.label
-
- @app_controller = ApplicationController
-
controllers_files = Dir.glob("app/controllers/**/*_controller.rb")
controllers_files << 'app/controllers/application.rb'
controllers_files.each do |c|
-
# Extract the class name from the file name
class_name = c.split('/')[2..-1].join('/').split('.').first.camelize
-
# ApplicationController's file is 'application.rb'
- if class_name == 'Application'
- class_name = 'ApplicationController'
- end
-
+ class_name += 'Controller' if class_name == 'Application'
process_class class_name.constantize
-
end
print "}\n"
end # generate
private
@@ -378,13 +376,11 @@
def load_classes
begin
disable_stdout
# ApplicationController must be loaded first
require "app/controllers/application.rb"
- Dir.glob("app/controllers/**/*_controller.rb") do |c|
- require c
- end
+ Dir.glob("app/controllers/**/*_controller.rb") {|c| require c }
enable_stdout
rescue LoadError
enable_stdout
print_error "controller classes"
raise
@@ -392,50 +388,52 @@
end # load_classes
# Proccess controller class
def process_class(current_class)
- # Print the node
if @options.brief
- print "\t#{node(current_class.name)}\n"
+ print_node current_class.name
+
elsif current_class.is_a? Class
- print "\t#{node(current_class.name)} " +
- "[shape=Mrecord, label=\"{#{current_class.name}|"
+ node_attrib = 'shape=Mrecord, label="{' + current_class.name + '|'
current_class.public_instance_methods(false).sort.each { |m|
- print "#{m}\\l"
+ node_attrib += m + '\l'
} unless @options.hide_public
- print "|"
+ node_attrib += '|'
current_class.protected_instance_methods(false).sort.each { |m|
- print "#{m}\\l"
+ node_attrib += m + '\l'
} unless @options.hide_protected
- print "|"
+ node_attrib += '|'
current_class.private_instance_methods(false).sort.each { |m|
- print "#{m}\\l"
+ node_attrib += m + '\l'
} unless @options.hide_private
- print "}\"]\n"
- elsif @options.modules && (current_class.is_a? Module)
- print "\t#{node(current_class.name)}" +
- "[shape=box, style=dotted, label=\"#{current_class.name}\"]\n"
+ node_attrib += '}"'
+ print_node current_class.name, node_attrib
+
+ elsif @options.modules && current_class.is_a?(Module)
+ print_node current_class.name,
+ 'shape=box, style=dotted, label="' + current_class.name + '"'
end
- # Print the inheritance edge
+ # Print the inheritance edge (only for ApplicationControllers)
if @options.inheritance &&
(@app_controller.subclasses.include? current_class.name)
- print "\t#{node(current_class.name)} -> " +
- "#{node(current_class.superclass.name)} " +
- "[arrowhead=\"onormal\"]\n"
+ print_edge(current_class.name, current_class.superclass.name,
+ 'arrowhead="onormal"')
end
end # process_class
end # class ControllersDiagram
# Main program
-options = OptionsParser.parse ARGV
+options = OptionsStruct.new
+options.parse ARGV
+
if options.command == 'models'
diagram = ModelsDiagram.new options
elsif options.command == 'controllers'
diagram = ControllersDiagram.new options
else
@@ -447,10 +445,10 @@
if options.output
old_stdout = STDOUT.dup
begin
STDOUT.reopen(options.output)
rescue
- STDERR.print "Error: Cannot write to #{options.output}\n\n"
+ STDERR.print "Error: Cannot write diagram to #{options.output}\n\n"
exit 2
end
end
diagram.generate