lib/railroad in railroad-0.2.0 vs lib/railroad in railroad-0.3.0
- old
+ new
@@ -1,241 +1,433 @@
#!/usr/bin/env ruby
# RailRoad - RoR diagrams generator
-# http://railroad.rubyforge.org/
+# http://railroad.rubyforge.org
#
+# RailRoad generates models and controllers diagrams in DOT language
+# for a Rails application.
+#
# Copyright 2007 - Javier Smaldone (http://www.smaldone.com.ar)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
-# Purpose:
-# RailRoad generates models and controllers diagrams in DOT language
-# for a Rails application.
-@app_name = "railroad"
-@app_human_name = "RailRoad"
-@version = "0.1.2"
-@copyright = "Copyright (C) 2007 Javier Smaldone"
+APP_NAME = "railroad"
+APP_HUMAN_NAME = "RailRoad"
+APP_VERSION = [0,3,0]
+COPYRIGHT = "Copyright (C) 2007 Javier Smaldone"
-def print_help
- print "Usage: #{@app_name} [options] [command]\n\n"
- print "Options:\n"
- print " -a, --all\tInclude all models (not ActiveRecord::Base derived)\n"
- print " -b, --brief\tGenerate compact diagram (no attributes)\n"
- print " -c, --compact\tConcentrate edges (only for modules diagram)\n"
- print " -l, --label\tAdd a label with diagram info (type, date, version)\n"
- print " -i, --inherit\tInclude inheritance relations\n"
- print " -h, --help\tDisplay this help message and exit\n"
- print " -v, --version\tDisplay version information and exit\n\n"
- print "Commands:\n"
- print " -M\tGenerate models diagram\n"
- print " -C\tGenerate controllers diagram\n"
- print "\n"
-end
-def node(name)
+class OptionsParser
+
+ require 'optparse'
+ require 'ostruct'
+
+ def self.parse(args)
+
+ 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|
+ 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
+ end
+ opts.on("-i", "--inheritance", "Include inheritance relations") do |i|
+ options.inheritance = i
+ end
+ opts.on("-l", "--label", "Add a label with diagram information",
+ " (type, date, migration, version)") do |l|
+ options.label = l
+ end
+ opts.on("-o", "--output FILE", "Write diagram to file FILE") do |f|
+ options.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
+ end
+ opts.on("--hide-types", "Hide attributes type") do |h|
+ options.hide_types = h
+ end
+ opts.on("-j", "--join", "Concentrate edges") do |j|
+ options.join = j
+ end
+ opts.on("-m", "--modules", "Include modules") do |m|
+ options.modules = m
+ end
+ opts.separator ""
+ opts.separator "Controllers diagram options:"
+ opts.on("--hide-public", "Hide public methods") do |h|
+ options.hide_public = h
+ end
+ opts.on("--hide-protected", "Hide protected methods") do |h|
+ options.hide_protected = h
+ end
+ opts.on("--hide-private", "Hide private methods") do |h|
+ options.hide_private = h
+ end
+ opts.separator ""
+ opts.separator "Other options:"
+ opts.on("-h", "--help", "Show this message") do
+ print "#{opts}\n"
+ exit
+ end
+ opts.on("--version", "Show version and copyright") do
+ print "#{APP_HUMAN_NAME} version #{APP_VERSION.join('.')}\n\n" +
+ "#{COPYRIGHT}\n" +
+ "This is free software; see the source for copying conditions.\n\n"
+ 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"
+ exit 1
+ else
+ options.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"
+ exit 1
+ else
+ options.command = 'controllers'
+ end
+ end
+ opts.separator ""
+ opts.separator "For bug reporting instructions and additional information, please see:"
+ opts.separator "http://railroad.rubyforge.org"
+ end
+
+ begin
+ opts.parse!(args)
+ rescue OptionParser::AmbiguousOption
+ option_error "Ambiguous option", opts
+ rescue OptionParser::InvalidOption
+ option_error "Invalid option", opts
+ rescue OptionParser::InvalidArgument
+ option_error "Invalid argument", opts
+ rescue OptionParser::MissingArgument
+ option_error "Missing argument", opts
+ rescue
+ option_error "Unknown error", opts
+ end
+ options
+ end # self.parse
+
+ private
+
+ def self.option_error(msg, opts)
+ $stderr.print "Error: #{msg}\n\n #{opts}\n"
+ exit 1
+ end
+
+end # class OptionsParser
+
+
+class AppDiagram
+
+ def initialize(options)
+ @options = options
+ load_environment
+ end
+
+ private
+
+ # Quotes a class name
+ def node(name)
'"' + name + '"'
-end
+ end
-def print_info(type)
- print "\t_diagram_info [shape=\"plaintext\", label=\"Diagram: #{type}\\l"
- print "Date: #{Time.now.strftime "%b %d %Y - %H:%M"}\\l"
- print "Migration version: #{ActiveRecord::Migrator.current_version}\\l"
- print "Generated by #{@app_human_name} #{@version}\\l\""
- print ", fontsize=14]\n"
-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
-def print_error(type)
- STDERR.print "Error loading #{type}. (Are you running "
- STDERR.print "#{@app_name} on the aplication's root directory?)\n\n"
-end
+ def print_error(type)
+ STDERR.print "Error loading #{type}.\n (Are you running " +
+ "#{APP_NAME} on the aplication's root directory?)\n\n"
+ end
-def generate_models
- # Load model classes
- begin
- Dir.glob("app/models/**/*.rb") {|m| require m }
- rescue LoadError
- print_error "model classes"
- raise
+ # Load Rails application's environment
+ def load_environment
+ begin
+ require "config/environment"
+ rescue LoadError
+ print_error "app environment"
+ raise
+ end
end
- # DOT diagram header
- print "digraph models_diagram {\n"
- print "\tgraph[concentrate=true]\n" if @compact
- print_info "Models" if @label
+end # class AppDiagram
- 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
+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
+
+ # Processed habtm associations
+ @habtm = []
+
+ 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
+ end
+ print "}\n"
+
+
+ end # generate
+
+ private
+
+ # Load model classes
+ def load_classes
+ begin
+ Dir.glob("app/models/**/*.rb") {|m| require m }
+ rescue LoadError
+ print_error "model classes"
+ raise
+ end
+ end # load_classes
+
+ # Process model class
+ def process_class(current_class)
+
+ printed = false
+
# Is current_clas derived from ActiveRecord::Base?
if current_class.respond_to?'reflect_on_all_associations'
# Print the node
- if @brief
+ if @options.brief
print "\t#{node(current_class.name)}\n"
else
- print "\t#{node(current_class.name)} "
- print "[shape=Mrecord, label=\"{#{current_class.name}|"
+ print "\t#{node(current_class.name)} " +
+ "[shape=Mrecord, label=\"{#{current_class.name}|"
current_class.content_columns.each do |a|
- print "#{a.name} :#{a.type.to_s}\\l"
+ print "#{a.name}"
+ print " :#{a.type.to_s}" unless @options.hide_types
+ print "\\l"
end
print "}\"]\n"
end
-
+ printed = true
# Iterate over the class associations
- current_class.reflect_on_all_associations.each do |association|
+ current_class.reflect_on_all_associations.each { |a|
+ process_association(current_class, a)
+ }
+ elsif @options.all && (current_class.is_a? Class)
+ # Not ActiveRecord::Base model
+ if @options.brief
+ print "\t#{node(current_class.name)} [shape=box]\n"
+ else
+ print "\t#{node(current_class.name)}" +
+ "[shape=record, label=\"{#{current_class.name}|}\"]\n"
+ end
+ 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"
+ end
- # 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 = ""
- end
+ # Only consider meaningful inheritance relations for printed classes
+ if @options.inheritance && 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"
+ 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
+ end # process_class
- # Print the edge
- print "\t\t#{node(current_class.name)} -> "
- print "#{node(association.class_name)} "
- print "[ #{params} ]\n"
- end
- end # Associations
+ # Process model association
+ def process_association(current_class, association)
- if @inherit && (current_class.superclass != ActiveRecord::Base)
- # Print the inheritance edge
- print "\t\t#{node(current_class.name)} -> "
- print "#{node(current_class.superclass.name)}"
- print " [arrowhead=\"onormal\"]\n"
- end
+ # 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 = ""
+ end
- elsif @all_classes
- # Not ActiveRecord::Base model
- if @brief
- print "\t#{node(current_class.name)} [shape=box]\n"
- else
- print "\t#{node(current_class.name)}"
- print "[shape=record, label=\"{#{current_class.name}|}\"]\n"
+ # 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
- end
- end # Dir
- print "}\n"
-end # generate_models
-def generate_controllers
- # Load controller classes
- begin
- # ApplicationController must be loaded first
- require "app/controllers/application.rb"
- Dir.glob("app/controllers/**/*_controller.rb") do |c|
- require c
+ # 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
- rescue LoadError
- print_error "controller classes"
- raise
- end
+ end # process_association
- # DOT diagram header
- print "digraph controllers_diagram {\n"
- print "\tgraph[overlap=false, splines=true]\n"
- print_info "Controllers" if @label
+end # class ModelsDiagram
- 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
+class ControllersDiagram < AppDiagram
- # ApplicationController's file is 'application.rb'
- if class_name == 'Application'
- class_name = 'ApplicationController'
+ # Generate controllers diagram
+ def generate
+
+ load_classes
+
+ # 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
+
+ process_class class_name.constantize
+
end
- current_class = class_name.constantize
+ print "}\n"
+ end # generate
+ private
+
+ # Load controller classes
+ def load_classes
+ begin
+ # ApplicationController must be loaded first
+ require "app/controllers/application.rb"
+ Dir.glob("app/controllers/**/*_controller.rb") do |c|
+ require c
+ end
+ rescue LoadError
+ print_error "controller classes"
+ raise
+ end
+ end # load_classes
+
+ # Proccess controller class
+ def process_class(current_class)
# Print the node
- if @brief
+ if @options.brief
print "\t#{node(current_class.name)}\n"
- else
- print "\t#{node(current_class.name)} "
- print "[shape=Mrecord, label=\"{#{current_class.name}|"
- current_class.public_instance_methods(false).sort.each do |m|
+ elsif current_class.is_a? Class
+ print "\t#{node(current_class.name)} " +
+ "[shape=Mrecord, label=\"{#{current_class.name}|"
+ current_class.public_instance_methods(false).sort.each { |m|
print "#{m}\\l"
- end
- print "|"
- current_class.protected_instance_methods(false).sort.each do |m|
+ } unless @options.hide_public
+ print "|"
+ current_class.protected_instance_methods(false).sort.each { |m|
print "#{m}\\l"
- end
- print "|"
- current_class.private_instance_methods(false).sort.each do |m|
+ } unless @options.hide_protected
+ print "|"
+ current_class.private_instance_methods(false).sort.each { |m|
print "#{m}\\l"
- end
- print "}\"]\n"
+ } 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"
end
- if @inherit && (app_controller.subclasses.include? current_class.name)
- # Print the inheritance edge
- print "\t#{node(current_class.name)} -> "
- print "#{node(current_class.superclass.name)} "
- print "[arrowhead=\"onormal\"]\n"
+
+ # Print the inheritance edge
+ if @options.inheritance &&
+ (@app_controller.subclasses.include? current_class.name)
+ print "\t#{node(current_class.name)} -> " +
+ "#{node(current_class.superclass.name)} " +
+ "[arrowhead=\"onormal\"]\n"
end
- end # Dir
- print "}\n"
-end # generate_controllers
-# Main program
-if ARGV.include?('--help') || ARGV.include?('-h')
- print_help
- exit
-elsif ARGV.include?('--version') || ARGV.include?('-v')
- print "#{@app_name} #{@version}\n#{@copyright}\n\n"
- exit
-end
+ end # process_class
-@all_classes = ARGV.include?('--all') || ARGV.include?('-a')
-@brief = ARGV.include?('--brief') || ARGV.include?('-b')
-@compact = ARGV.include?('--compact') || ARGV.include?('-c')
-@label = ARGV.include?('--label') || ARGV.include?('-l')
-@inherit = ARGV.include?('--inherit') || ARGV.include?('-i')
+end # class ControllersDiagram
-params = %w(-a --all -b --brief -c --compact -l --label -i --inherit -M -C)
-error_params = ARGV - params
-if ! error_params.empty?
- # Unknown parameter
- STDERR.print "Error: wrong parameter(s) #{error_params}\n"
- STDERR.print "(try '#{@app_name} --help')\n\n"
+
+# Main program
+
+options = OptionsParser.parse ARGV
+
+if options.command == 'models'
+ diagram = ModelsDiagram.new options
+elsif options.command == 'controllers'
+ diagram = ControllersDiagram.new options
+else
+ $stderr.print "Error: You must supply a command\n" +
+ " (try #{APP_NAME} -h)\n\n"
exit 1
end
-if ARGV.include?('-M') && ARGV.include?('-C')
- STDERR.print "Error: you can only specify one command\n"
- STDERR.print "(try '#{@app_name} --help')\n\n"
- exit 1
-elsif ! (ARGV.include?('-M') || ARGV.include?('-C'))
- STDERR.print "Error: you must specify a command\n"
- STDERR.print "(try '#{@app_name} --help')\n\n"
- exit 1
-end
-# Load Rails app environment
-begin
- require "config/environment"
-rescue LoadError
- print_error "app environment"
- raise
+if options.output
+ old_stdout = $stdout
+ begin
+ $stdout = File.open(options.output, "w")
+ rescue
+ $stderr.print "Error: Cannot write to #{options.output}\n\n"
+ exit 2
+ end
end
-if ARGV.include?('-M')
- generate_models
-elsif ARGV.include?('-C')
- generate_controllers
+diagram.generate
+
+if options.out
+ $stdout = old_stdout
end