2007-01-22 14:43:50 +01:00
require 'delegate'
require 'optparse'
require 'fileutils'
2007-02-09 09:04:31 +01:00
require 'tempfile'
2007-01-22 14:43:50 +01:00
require 'erb'
module Rails
module Generator
module Commands
# Here's a convenient way to get a handle on generator commands.
# Command.instance('destroy', my_generator) instantiates a Destroy
# delegate of my_generator ready to do your dirty work.
def self . instance ( command , generator )
const_get ( command . to_s . camelize ) . new ( generator )
end
# Even more convenient access to commands. Include Commands in
# the generator Base class to get a nice #command instance method
# which returns a delegate for the requested command.
2007-02-09 09:04:31 +01:00
def self . included ( base )
2007-01-22 14:43:50 +01:00
base . send ( :define_method , :command ) do | command |
Commands . instance ( command , self )
end
end
# Generator commands delegate Rails::Generator::Base and implement
# a standard set of actions. Their behavior is defined by the way
# they respond to these actions: Create brings life; Destroy brings
# death; List passively observes.
#
# Commands are invoked by replaying (or rewinding) the generator's
# manifest of actions. See Rails::Generator::Manifest and
# Rails::Generator::Base#manifest method that generator subclasses
# are required to override.
#
# Commands allows generators to "plug in" invocation behavior, which
# corresponds to the GoF Strategy pattern.
class Base < DelegateClass ( Rails :: Generator :: Base )
# Replay action manifest. RewindBase subclass rewinds manifest.
def invoke!
manifest . replay ( self )
end
def dependency ( generator_name , args , runtime_options = { } )
logger . dependency ( generator_name ) do
self . class . new ( instance ( generator_name , args , full_options ( runtime_options ) ) ) . invoke!
end
end
# Does nothing for all commands except Create.
def class_collisions ( * class_names )
end
# Does nothing for all commands except Create.
def readme ( * args )
end
protected
def migration_directory ( relative_path )
directory ( @migration_directory = relative_path )
end
def existing_migrations ( file_name )
Dir . glob ( " #{ @migration_directory } /[0-9]*_*.rb " ) . grep ( / [0-9]+_ #{ file_name } .rb$ / )
end
def migration_exists? ( file_name )
not existing_migrations ( file_name ) . empty?
end
def next_migration_string ( padding = 3 )
2008-05-18 06:22:34 +02:00
Time . now . utc . strftime ( " %Y%m%d%H%M%S " )
2007-01-22 14:43:50 +01:00
end
2007-02-09 09:04:31 +01:00
def gsub_file ( relative_destination , regexp , * args , & block )
path = destination_path ( relative_destination )
content = File . read ( path ) . gsub ( regexp , * args , & block )
File . open ( path , 'wb' ) { | file | file . write ( content ) }
end
2007-01-22 14:43:50 +01:00
private
# Ask the user interactively whether to force collision.
2007-02-09 09:04:31 +01:00
def force_file_collision? ( destination , src , dst , file_options = { } , & block )
2007-12-21 08:48:59 +01:00
$stdout . print " overwrite #{ destination } ? (enter \" h \" for help) [Ynaqdh] "
case $stdin . gets . chomp
when / \ Ad \ z /i
2007-02-09 09:04:31 +01:00
Tempfile . open ( File . basename ( destination ) , File . dirname ( dst ) ) do | temp |
temp . write render_file ( src , file_options , & block )
temp . rewind
$stdout . puts ` #{ diff_cmd } #{ dst } #{ temp . path } `
end
puts " retrying "
raise 'retry diff'
2007-12-21 08:48:59 +01:00
when / \ Aa \ z /i
2007-01-22 14:43:50 +01:00
$stdout . puts " forcing #{ spec . name } "
options [ :collision ] = :force
2007-12-21 08:48:59 +01:00
when / \ Aq \ z /i
2007-01-22 14:43:50 +01:00
$stdout . puts " aborting #{ spec . name } "
raise SystemExit
2007-12-21 08:48:59 +01:00
when / \ An \ z /i then :skip
when / \ Ay \ z /i then :force
else
$stdout . puts <<-HELP
Y - yes , overwrite
n - no , do not overwrite
a - all , overwrite this and all others
q - quit , abort
d - diff , show the differences between the old and the new
h - help , show this help
HELP
raise 'retry'
2007-01-22 14:43:50 +01:00
end
rescue
retry
end
2007-02-09 09:04:31 +01:00
def diff_cmd
ENV [ 'RAILS_DIFF' ] || 'diff -u'
end
2007-01-22 14:43:50 +01:00
def render_template_part ( template_options )
# Getting Sandbox to evaluate part template in it
part_binding = template_options [ :sandbox ] . call . sandbox_binding
part_rel_path = template_options [ :insert ]
part_path = source_path ( part_rel_path )
# Render inner template within Sandbox binding
rendered_part = ERB . new ( File . readlines ( part_path ) . join , nil , '-' ) . result ( part_binding )
begin_mark = template_part_mark ( template_options [ :begin_mark ] , template_options [ :mark_id ] )
end_mark = template_part_mark ( template_options [ :end_mark ] , template_options [ :mark_id ] )
begin_mark + rendered_part + end_mark
end
def template_part_mark ( name , id )
" <!--[ #{ name } : #{ id } ]--> \n "
end
end
# Base class for commands which handle generator actions in reverse, such as Destroy.
class RewindBase < Base
# Rewind action manifest.
def invoke!
manifest . rewind ( self )
end
end
# Create is the premier generator command. It copies files, creates
# directories, renders templates, and more.
class Create < Base
# Check whether the given class names are already taken by
# Ruby or Rails. In the future, expand to check other namespaces
# such as the rest of the user's app.
def class_collisions ( * class_names )
2008-06-02 08:35:38 +02:00
# Initialize some check varibles
last_class = Object
current_class = nil
name = nil
2007-01-22 14:43:50 +01:00
class_names . flatten . each do | class_name |
# Convert to string to allow symbol arguments.
class_name = class_name . to_s
# Skip empty strings.
2008-06-02 08:35:38 +02:00
class_name . strip . empty? ? next : current_class = class_name
2007-01-22 14:43:50 +01:00
# Split the class from its module nesting.
nesting = class_name . split ( '::' )
name = nesting . pop
# Extract the last Module in the nesting.
2008-06-02 08:35:38 +02:00
last = nesting . inject ( last_class ) { | last , nest |
break unless last_class . const_defined? ( nest )
last_class = last_class . const_get ( nest )
2007-01-22 14:43:50 +01:00
}
2008-06-02 08:35:38 +02:00
end
# If the last Module exists, check whether the given
# class exists and raise a collision if so.
if last_class and last_class . const_defined? ( name . camelize )
raise_class_collision ( current_class )
2007-01-22 14:43:50 +01:00
end
end
# Copy a file from source to destination with collision checking.
#
# The file_options hash accepts :chmod and :shebang and :collision options.
# :chmod sets the permissions of the destination file:
# file 'config/empty.log', 'log/test.log', :chmod => 0664
# :shebang sets the #!/usr/bin/ruby line for scripts
# file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
2008-05-18 06:22:34 +02:00
# :collision sets the collision option only for the destination file:
2007-01-22 14:43:50 +01:00
# file 'settings/server.yml', 'config/server.yml', :collision => :skip
#
# Collisions are handled by checking whether the destination file
# exists and either skipping the file, forcing overwrite, or asking
# the user what to do.
def file ( relative_source , relative_destination , file_options = { } , & block )
# Determine full paths for source and destination files.
source = source_path ( relative_source )
destination = destination_path ( relative_destination )
2007-12-21 08:48:59 +01:00
destination_exists = File . exist? ( destination )
2007-01-22 14:43:50 +01:00
# If source and destination are identical then we're done.
if destination_exists and identical? ( source , destination , & block )
2008-05-18 06:22:34 +02:00
return logger . identical ( relative_destination )
2007-01-22 14:43:50 +01:00
end
# Check for and resolve file collisions.
if destination_exists
# Make a choice whether to overwrite the file. :force and
# :skip already have their mind made up, but give :ask a shot.
choice = case ( file_options [ :collision ] || options [ :collision ] ) . to_sym #|| :ask
2007-02-09 09:04:31 +01:00
when :ask then force_file_collision? ( relative_destination , source , destination , file_options , & block )
2007-01-22 14:43:50 +01:00
when :force then :force
when :skip then :skip
else raise " Invalid collision option: #{ options [ :collision ] . inspect } "
end
# Take action based on our choice. Bail out if we chose to
# skip the file; otherwise, log our transgression and continue.
case choice
when :force then logger . force ( relative_destination )
when :skip then return ( logger . skip ( relative_destination ) )
else raise " Invalid collision choice: #{ choice } .inspect "
end
# File doesn't exist so log its unbesmirched creation.
else
logger . create relative_destination
end
# If we're pretending, back off now.
return if options [ :pretend ]
# Write destination file with optional shebang. Yield for content
# if block given so templaters may render the source file. If a
# shebang is requested, replace the existing shebang or insert a
# new one.
2007-02-09 09:04:31 +01:00
File . open ( destination , 'wb' ) do | dest |
dest . write render_file ( source , file_options , & block )
2007-01-22 14:43:50 +01:00
end
# Optionally change permissions.
if file_options [ :chmod ]
FileUtils . chmod ( file_options [ :chmod ] , destination )
end
2007-02-09 09:04:31 +01:00
2008-05-18 06:22:34 +02:00
# Optionally add file to subversion or git
2007-01-22 14:43:50 +01:00
system ( " svn add #{ destination } " ) if options [ :svn ]
2008-05-18 06:22:34 +02:00
system ( " git add -v #{ relative_destination } " ) if options [ :git ]
2007-01-22 14:43:50 +01:00
end
# Checks if the source and the destination file are identical. If
# passed a block then the source file is a template that needs to first
# be evaluated before being compared to the destination.
def identical? ( source , destination , & block )
return false if File . directory? destination
source = block_given? ? File . open ( source ) { | sf | yield ( sf ) } : IO . read ( source )
destination = IO . read ( destination )
source == destination
end
# Generate a file for a Rails application using an ERuby template.
2007-12-21 08:48:59 +01:00
# Looks up and evaluates a template by name and writes the result.
2007-01-22 14:43:50 +01:00
#
# The ERB template uses explicit trim mode to best control the
# proliferation of whitespace in generated code. <%- trims leading
# whitespace; -%> trims trailing whitespace including one newline.
#
# A hash of template options may be passed as the last argument.
# The options accepted by the file are accepted as well as :assigns,
# a hash of variable bindings. Example:
# template 'foo', 'bar', :assigns => { :action => 'view' }
#
# Template is implemented in terms of file. It calls file with a
# block which takes a file handle and returns its rendered contents.
def template ( relative_source , relative_destination , template_options = { } )
file ( relative_source , relative_destination , template_options ) do | file |
# Evaluate any assignments in a temporary, throwaway binding.
vars = template_options [ :assigns ] || { }
b = binding
vars . each { | k , v | eval " #{ k } = vars[: #{ k } ] || vars[' #{ k } '] " , b }
# Render the source file with the temporary binding.
ERB . new ( file . read , nil , '-' ) . result ( b )
end
end
def complex_template ( relative_source , relative_destination , template_options = { } )
options = template_options . dup
options [ :assigns ] || = { }
options [ :assigns ] [ 'template_for_inclusion' ] = render_template_part ( template_options )
template ( relative_source , relative_destination , options )
end
# Create a directory including any missing parent directories.
2008-05-18 06:22:34 +02:00
# Always skips directories which exist.
2007-01-22 14:43:50 +01:00
def directory ( relative_path )
path = destination_path ( relative_path )
2007-12-21 08:48:59 +01:00
if File . exist? ( path )
2007-01-22 14:43:50 +01:00
logger . exists relative_path
else
logger . create relative_path
2008-05-18 06:22:34 +02:00
unless options [ :pretend ]
FileUtils . mkdir_p ( path )
# git doesn't require adding the paths, adding the files later will
# automatically do a path add.
# Subversion doesn't do path adds, so we need to add
# each directory individually.
# So stack up the directory tree and add the paths to
# subversion in order without recursion.
if options [ :svn ]
stack = [ relative_path ]
until File . dirname ( stack . last ) == stack . last # dirname('.') == '.'
stack . push File . dirname ( stack . last )
end
stack . reverse_each do | rel_path |
svn_path = destination_path ( rel_path )
system ( " svn add -N #{ svn_path } " ) unless File . directory? ( File . join ( svn_path , '.svn' ) )
end
end
end
end
end
2007-01-22 14:43:50 +01:00
# Display a README.
def readme ( * relative_sources )
relative_sources . flatten . each do | relative_source |
logger . readme relative_source
puts File . read ( source_path ( relative_source ) ) unless options [ :pretend ]
end
end
# When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
def migration_template ( relative_source , relative_destination , template_options = { } )
migration_directory relative_destination
migration_file_name = template_options [ :migration_file_name ] || file_name
raise " Another migration is already named #{ migration_file_name } : #{ existing_migrations ( migration_file_name ) . first } " if migration_exists? ( migration_file_name )
template ( relative_source , " #{ relative_destination } / #{ next_migration_string } _ #{ migration_file_name } .rb " , template_options )
end
2007-02-09 09:04:31 +01:00
def route_resources ( * resources )
resource_list = resources . map { | r | r . to_sym . inspect } . join ( ', ' )
sentinel = 'ActionController::Routing::Routes.draw do |map|'
logger . route " map.resources #{ resource_list } "
unless options [ :pretend ]
gsub_file 'config/routes.rb' , / ( #{ Regexp . escape ( sentinel ) } ) /mi do | match |
" #{ match } \n map.resources #{ resource_list } \n "
end
end
end
2007-01-22 14:43:50 +01:00
private
2007-02-09 09:04:31 +01:00
def render_file ( path , options = { } )
File . open ( path , 'rb' ) do | file |
if block_given?
yield file
else
content = ''
if shebang = options [ :shebang ]
content << " # ! #{ shebang } \n "
if line = file . gets
content << " line \n " if line !~ / ^ # ! /
end
end
content << file . read
end
end
end
2007-01-22 14:43:50 +01:00
# Raise a usage error with an informative WordNet suggestion.
# Thanks to Florian Gross (flgr).
def raise_class_collision ( class_name )
message = <<end_message
2008-06-02 08:35:38 +02:00
The name '#{class_name}' is either already used in your application or reserved by Ruby on Rails .
2007-01-22 14:43:50 +01:00
Please choose an alternative and run this generator again .
end_message
if suggest = find_synonyms ( class_name )
2008-06-02 08:35:38 +02:00
if suggest . any?
message << " \n Suggestions: \n \n "
message << suggest . join ( " \n " )
end
2007-01-22 14:43:50 +01:00
end
raise UsageError , message
end
2008-05-18 06:22:34 +02:00
SYNONYM_LOOKUP_URI = " http://wordnet.princeton.edu/perl/webwn?s=%s "
2007-01-22 14:43:50 +01:00
# Look up synonyms on WordNet. Thanks to Florian Gross (flgr).
def find_synonyms ( word )
require 'open-uri'
require 'timeout'
timeout ( 5 ) do
open ( SYNONYM_LOOKUP_URI % word ) do | stream |
2008-05-18 06:22:34 +02:00
# Grab words linked to dictionary entries as possible synonyms
data = stream . read . gsub ( " " , " " ) . scan ( / <a href="webwn.*?">([ \ w ]*?)< \/ a> /s ) . uniq
2007-01-22 14:43:50 +01:00
end
end
rescue Exception
return nil
end
end
# Undo the actions performed by a generator. Rewind the action
# manifest and attempt to completely erase the results of each action.
class Destroy < RewindBase
# Remove a file if it exists and is a file.
def file ( relative_source , relative_destination , file_options = { } )
destination = destination_path ( relative_destination )
2007-12-21 08:48:59 +01:00
if File . exist? ( destination )
2007-01-22 14:43:50 +01:00
logger . rm relative_destination
unless options [ :pretend ]
if options [ :svn ]
# If the file has been marked to be added
# but has not yet been checked in, revert and delete
if options [ :svn ] [ relative_destination ]
system ( " svn revert #{ destination } " )
FileUtils . rm ( destination )
else
# If the directory is not in the status list, it
# has no modifications so we can simply remove it
system ( " svn rm #{ destination } " )
2008-05-18 06:22:34 +02:00
end
elsif options [ :git ]
if options [ :git ] [ :new ] [ relative_destination ]
# file has been added, but not committed
system ( " git reset HEAD #{ relative_destination } " )
FileUtils . rm ( destination )
elsif options [ :git ] [ :modified ] [ relative_destination ]
# file is committed and modified
system ( " git rm -f #{ relative_destination } " )
else
# If the directory is not in the status list, it
# has no modifications so we can simply remove it
system ( " git rm #{ relative_destination } " )
end
2007-01-22 14:43:50 +01:00
else
FileUtils . rm ( destination )
end
end
else
logger . missing relative_destination
return
end
end
# Templates are deleted just like files and the actions take the
# same parameters, so simply alias the file method.
alias_method :template , :file
# Remove each directory in the given path from right to left.
# Remove each subdirectory if it exists and is a directory.
def directory ( relative_path )
parts = relative_path . split ( '/' )
until parts . empty?
partial = File . join ( parts )
path = destination_path ( partial )
2007-12-21 08:48:59 +01:00
if File . exist? ( path )
2007-01-22 14:43:50 +01:00
if Dir [ File . join ( path , '*' ) ] . empty?
logger . rmdir partial
unless options [ :pretend ]
if options [ :svn ]
# If the directory has been marked to be added
# but has not yet been checked in, revert and delete
if options [ :svn ] [ relative_path ]
system ( " svn revert #{ path } " )
FileUtils . rmdir ( path )
else
# If the directory is not in the status list, it
# has no modifications so we can simply remove it
system ( " svn rm #{ path } " )
end
2008-05-18 06:22:34 +02:00
# I don't think git needs to remove directories?..
# or maybe they have special consideration...
2007-01-22 14:43:50 +01:00
else
FileUtils . rmdir ( path )
end
end
else
logger . notempty partial
end
else
logger . missing partial
end
parts . pop
end
end
def complex_template ( * args )
# nothing should be done here
end
# When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
def migration_template ( relative_source , relative_destination , template_options = { } )
migration_directory relative_destination
migration_file_name = template_options [ :migration_file_name ] || file_name
unless migration_exists? ( migration_file_name )
puts " There is no migration named #{ migration_file_name } "
return
end
existing_migrations ( migration_file_name ) . each do | file_path |
file ( relative_source , file_path , template_options )
end
end
2007-02-09 09:04:31 +01:00
def route_resources ( * resources )
resource_list = resources . map { | r | r . to_sym . inspect } . join ( ', ' )
look_for = " \n map.resources #{ resource_list } \n "
logger . route " map.resources #{ resource_list } "
gsub_file 'config/routes.rb' , / ( #{ look_for } ) /mi , ''
end
2007-01-22 14:43:50 +01:00
end
# List a generator's action manifest.
class List < Base
def dependency ( generator_name , args , options = { } )
logger . dependency " #{ generator_name } ( #{ args . join ( ', ' ) } , #{ options . inspect } ) "
end
def class_collisions ( * class_names )
logger . class_collisions class_names . join ( ', ' )
end
def file ( relative_source , relative_destination , options = { } )
logger . file relative_destination
end
def template ( relative_source , relative_destination , options = { } )
logger . template relative_destination
end
def complex_template ( relative_source , relative_destination , options = { } )
logger . template " #{ options [ :insert ] } inside #{ relative_destination } "
end
def directory ( relative_path )
logger . directory " #{ destination_path ( relative_path ) } / "
end
def readme ( * args )
logger . readme args . join ( ', ' )
end
2008-05-18 06:22:34 +02:00
2007-01-22 14:43:50 +01:00
def migration_template ( relative_source , relative_destination , options = { } )
migration_directory relative_destination
logger . migration_template file_name
end
2007-02-09 09:04:31 +01:00
def route_resources ( * resources )
resource_list = resources . map { | r | r . to_sym . inspect } . join ( ', ' )
logger . route " map.resources #{ resource_list } "
end
2007-01-22 14:43:50 +01:00
end
# Update generator's action manifest.
class Update < Create
def file ( relative_source , relative_destination , options = { } )
# logger.file relative_destination
end
def template ( relative_source , relative_destination , options = { } )
# logger.template relative_destination
end
def complex_template ( relative_source , relative_destination , template_options = { } )
begin
dest_file = destination_path ( relative_destination )
source_to_update = File . readlines ( dest_file ) . join
rescue Errno :: ENOENT
logger . missing relative_destination
return
end
2007-12-21 08:48:59 +01:00
logger . refreshing " #{ template_options [ :insert ] . gsub ( / \ .erb / , '' ) } inside #{ relative_destination } "
2007-01-22 14:43:50 +01:00
begin_mark = Regexp . quote ( template_part_mark ( template_options [ :begin_mark ] , template_options [ :mark_id ] ) )
end_mark = Regexp . quote ( template_part_mark ( template_options [ :end_mark ] , template_options [ :mark_id ] ) )
# Refreshing inner part of the template with freshly rendered part.
rendered_part = render_template_part ( template_options )
source_to_update . gsub! ( / #{ begin_mark } .*? #{ end_mark } /m , rendered_part )
File . open ( dest_file , 'w' ) { | file | file . write ( source_to_update ) }
end
def directory ( relative_path )
# logger.directory "#{destination_path(relative_path)}/"
end
end
end
end
end