10 # Here's a convenient way to get a handle on generator commands.
11 # Command.instance('destroy', my_generator) instantiates a Destroy
12 # delegate of my_generator ready to do your dirty work.
13 def self.instance(command
, generator
)
14 const_get(command
.to_s
.camelize
).new(generator
)
17 # Even more convenient access to commands. Include Commands in
18 # the generator Base class to get a nice #command instance method
19 # which returns a delegate for the requested command.
20 def self.included(base
)
21 base
.send(:define_method, :command) do |command
|
22 Commands
.instance(command
, self)
27 # Generator commands delegate Rails::Generator::Base and implement
28 # a standard set of actions. Their behavior is defined by the way
29 # they respond to these actions: Create brings life; Destroy brings
30 # death; List passively observes.
32 # Commands are invoked by replaying (or rewinding) the generator's
33 # manifest of actions. See Rails::Generator::Manifest and
34 # Rails::Generator::Base#manifest method that generator subclasses
35 # are required to override.
37 # Commands allows generators to "plug in" invocation behavior, which
38 # corresponds to the GoF Strategy pattern.
39 class Base
< DelegateClass(Rails
::Generator::Base)
40 # Replay action manifest. RewindBase subclass rewinds manifest.
46 def dependency(generator_name
, args
, runtime_options
= {})
47 logger
.dependency(generator_name
) do
48 self.class.new(instance(generator_name
, args
, full_options(runtime_options
))).invoke
!
52 # Does nothing for all commands except Create.
53 def class_collisions(*class_names
)
56 # Does nothing for all commands except Create.
61 def current_migration_number
62 Dir
.glob("#{RAILS_ROOT}/#{@migration_directory}/[0-9]*_*.rb").inject(0) do |max
, file_path
|
63 n
= File
.basename(file_path
).split('_', 2).first
.to_i
64 if n
> max
then n
else max
end
68 def next_migration_number
69 current_migration_number
+ 1
72 def migration_directory(relative_path
)
73 directory(@migration_directory = relative_path
)
76 def existing_migrations(file_name
)
77 Dir
.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_
#{file_name}.rb$/)
80 def migration_exists
?(file_name
)
81 not existing_migrations(file_name
).empty
?
84 def next_migration_string(padding
= 3)
85 if ActiveRecord
::Base.timestamped_migrations
86 Time
.now
.utc
.strftime("%Y%m%d%H%M%S")
88 "%.#{padding}d" % next_migration_number
92 def gsub_file(relative_destination
, regexp
, *args
, &block
)
93 path
= destination_path(relative_destination
)
94 content
= File
.read(path
).gsub(regexp
, *args
, &block
)
95 File
.open(path
, 'wb') { |file
| file
.write(content
) }
99 # Ask the user interactively whether to force collision.
100 def force_file_collision
?(destination
, src
, dst
, file_options
= {}, &block
)
101 $stdout.print
"overwrite #{destination}? (enter \"h\" for help) [Ynaqdh] "
102 case $stdin.gets
.chomp
104 Tempfile
.open(File
.basename(destination
), File
.dirname(dst
)) do |temp
|
105 temp
.write
render_file(src
, file_options
, &block
)
107 $stdout.puts
`#{diff_cmd} "#{dst}" "#{temp.path}"`
112 $stdout.puts
"forcing #{spec.name}"
113 options
[:collision] = :force
115 $stdout.puts
"aborting #{spec.name}"
117 when /\An\z/i
then :skip
118 when /\Ay\z/i
then :force
122 n - no, do not overwrite
123 a - all, overwrite this and all others
125 d - diff, show the differences between the old and the new
126 h - help, show this help
135 ENV['RAILS_DIFF'] || 'diff -u'
138 def render_template_part(template_options
)
139 # Getting Sandbox to evaluate part template in it
140 part_binding
= template_options
[:sandbox].call
.sandbox_binding
141 part_rel_path
= template_options
[:insert]
142 part_path
= source_path(part_rel_path
)
144 # Render inner template within Sandbox binding
145 rendered_part
= ERB
.new(File
.readlines(part_path
).join
, nil, '-').result(part_binding
)
146 begin_mark
= template_part_mark(template_options
[:begin_mark], template_options
[:mark_id])
147 end_mark
= template_part_mark(template_options
[:end_mark], template_options
[:mark_id])
148 begin_mark
+ rendered_part
+ end_mark
151 def template_part_mark(name
, id
)
152 "<!--[#{name}:#{id}]-->\n"
156 # Base class for commands which handle generator actions in reverse, such as Destroy.
157 class RewindBase
< Base
158 # Rewind action manifest.
160 manifest
.rewind(self)
165 # Create is the premier generator command. It copies files, creates
166 # directories, renders templates, and more.
169 # Check whether the given class names are already taken by
170 # Ruby or Rails. In the future, expand to check other namespaces
171 # such as the rest of the user's app.
172 def class_collisions(*class_names
)
173 path
= class_names
.shift
174 class_names
.flatten
.each
do |class_name
|
175 # Convert to string to allow symbol arguments.
176 class_name
= class_name
.to_s
178 # Skip empty strings.
179 next if class_name
.strip
.empty
?
181 # Split the class from its module nesting.
182 nesting
= class_name
.split('::')
185 # Hack to limit const_defined? to non-inherited on 1.9.
187 extra
<< false unless Object
.method(:const_defined?).arity
== 1
189 # Extract the last Module in the nesting.
190 last
= nesting
.inject(Object
) { |last
, nest
|
191 break unless last
.const_defined
?(nest
, *extra
)
195 # If the last Module exists, check whether the given
196 # class exists and raise a collision if so.
197 if last
and last
.const_defined
?(name
.camelize
, *extra
)
198 raise_class_collision(class_name
)
203 # Copy a file from source to destination with collision checking.
205 # The file_options hash accepts :chmod and :shebang and :collision options.
206 # :chmod sets the permissions of the destination file:
207 # file 'config/empty.log', 'log/test.log', :chmod => 0664
208 # :shebang sets the #!/usr/bin/ruby line for scripts
209 # file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
210 # :collision sets the collision option only for the destination file:
211 # file 'settings/server.yml', 'config/server.yml', :collision => :skip
213 # Collisions are handled by checking whether the destination file
214 # exists and either skipping the file, forcing overwrite, or asking
215 # the user what to do.
216 def file(relative_source
, relative_destination
, file_options
= {}, &block
)
217 # Determine full paths for source and destination files.
218 source
= source_path(relative_source
)
219 destination
= destination_path(relative_destination
)
220 destination_exists
= File
.exist
?(destination
)
222 # If source and destination are identical then we're done.
223 if destination_exists
and identical
?(source
, destination
, &block
)
224 return logger
.identical(relative_destination
)
227 # Check for and resolve file collisions.
228 if destination_exists
230 # Make a choice whether to overwrite the file. :force and
231 # :skip already have their mind made up, but give :ask a shot.
232 choice
= case (file_options
[:collision] || options
[:collision]).to_sym
#|| :ask
233 when :ask then force_file_collision
?(relative_destination
, source
, destination
, file_options
, &block
)
234 when :force then :force
235 when :skip then :skip
236 else raise "Invalid collision option: #{options[:collision].inspect}"
239 # Take action based on our choice. Bail out if we chose to
240 # skip the file; otherwise, log our transgression and continue.
242 when :force then logger
.force(relative_destination
)
243 when :skip then return(logger
.skip(relative_destination
))
244 else raise "Invalid collision choice: #{choice}.inspect"
247 # File doesn't exist so log its unbesmirched creation.
249 logger
.create relative_destination
252 # If we're pretending, back off now.
253 return if options
[:pretend]
255 # Write destination file with optional shebang. Yield for content
256 # if block given so templaters may render the source file. If a
257 # shebang is requested, replace the existing shebang or insert a
259 File
.open(destination
, 'wb') do |dest
|
260 dest
.write
render_file(source
, file_options
, &block
)
263 # Optionally change permissions.
264 if file_options
[:chmod]
265 FileUtils
.chmod(file_options
[:chmod], destination
)
268 # Optionally add file to subversion or git
269 system("svn add #{destination}") if options
[:svn]
270 system("git add -v #{relative_destination}") if options
[:git]
273 # Checks if the source and the destination file are identical. If
274 # passed a block then the source file is a template that needs to first
275 # be evaluated before being compared to the destination.
276 def identical
?(source
, destination
, &block
)
277 return false if File
.directory
? destination
278 source
= block_given
? ? File
.open(source
) {|sf
| yield(sf
)} : IO
.read(source
)
279 destination
= IO
.read(destination
)
280 source
== destination
283 # Generate a file for a Rails application using an ERuby template.
284 # Looks up and evaluates a template by name and writes the result.
286 # The ERB template uses explicit trim mode to best control the
287 # proliferation of whitespace in generated code. <%- trims leading
288 # whitespace; -%> trims trailing whitespace including one newline.
290 # A hash of template options may be passed as the last argument.
291 # The options accepted by the file are accepted as well as :assigns,
292 # a hash of variable bindings. Example:
293 # template 'foo', 'bar', :assigns => { :action => 'view' }
295 # Template is implemented in terms of file. It calls file with a
296 # block which takes a file handle and returns its rendered contents.
297 def template(relative_source
, relative_destination
, template_options
= {})
298 file(relative_source
, relative_destination
, template_options
) do |file
|
299 # Evaluate any assignments in a temporary, throwaway binding.
300 vars
= template_options
[:assigns] || {}
301 b
= template_options
[:binding] || binding
302 vars
.each
{ |k
,v
| eval "#{k} = vars[:#{k}] || vars['#{k}']", b
}
304 # Render the source file with the temporary binding.
305 ERB
.new(file
.read
, nil, '-').result(b
)
309 def complex_template(relative_source
, relative_destination
, template_options
= {})
310 options
= template_options
.dup
311 options
[:assigns] ||= {}
312 options
[:assigns]['template_for_inclusion'] = render_template_part(template_options
)
313 template(relative_source
, relative_destination
, options
)
316 # Create a directory including any missing parent directories.
317 # Always skips directories which exist.
318 def directory(relative_path
)
319 path
= destination_path(relative_path
)
321 logger
.exists relative_path
323 logger
.create relative_path
324 unless options
[:pretend]
325 FileUtils
.mkdir_p(path
)
326 # git doesn't require adding the paths, adding the files later will
327 # automatically do a path add.
329 # Subversion doesn't do path adds, so we need to add
330 # each directory individually.
331 # So stack up the directory tree and add the paths to
332 # subversion in order without recursion.
334 stack
= [relative_path
]
335 until File
.dirname(stack
.last
) == stack
.last
# dirname('.') == '.'
336 stack
.push File
.dirname(stack
.last
)
338 stack
.reverse_each
do |rel_path
|
339 svn_path
= destination_path(rel_path
)
340 system("svn add -N #{svn_path}") unless File
.directory
?(File
.join(svn_path
, '.svn'))
348 def readme(*relative_sources
)
349 relative_sources
.flatten
.each
do |relative_source
|
350 logger
.readme relative_source
351 puts File
.read(source_path(relative_source
)) unless options
[:pretend]
355 # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
356 def migration_template(relative_source
, relative_destination
, template_options
= {})
357 migration_directory relative_destination
358 migration_file_name
= template_options
[:migration_file_name] || file_name
359 raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists
?(migration_file_name
)
360 template(relative_source
, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options
)
363 def route_resources(*resources
)
364 resource_list
= resources
.map
{ |r
| r
.to_sym
.inspect
}.join(', ')
365 sentinel
= 'ActionController::Routing::Routes.draw do |map|'
367 logger
.route
"map.resources #{resource_list}"
368 unless options
[:pretend]
369 gsub_file
'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
370 "#{match}\n map.resources #{resource_list}\n"
376 def render_file(path
, options
= {})
377 File
.open(path
, 'rb') do |file
|
382 if shebang
= options
[:shebang]
383 content
<< "#!#{shebang}\n"
385 content
<< "line\n" if line
!~
/^#!/
393 # Raise a usage error with an informative WordNet suggestion.
394 # Thanks to Florian Gross (flgr).
395 def raise_class_collision(class_name
)
396 message
= <<end_message
397 The name '#{class_name}' is either already used in your application or reserved by Ruby on Rails.
398 Please choose an alternative and run this generator again.
400 if suggest
= find_synonyms(class_name
)
402 message
<< "\n Suggestions: \n\n"
403 message
<< suggest
.join("\n")
406 raise UsageError
, message
409 SYNONYM_LOOKUP_URI
= "http://wordnet.princeton.edu/perl/webwn?s=%s"
411 # Look up synonyms on WordNet. Thanks to Florian Gross (flgr).
412 def find_synonyms(word
)
416 open(SYNONYM_LOOKUP_URI
% word
) do |stream
|
417 # Grab words linked to dictionary entries as possible synonyms
418 data = stream
.read
.gsub(" ", " ").scan(/<a href="webwn.*?">([\w ]*?)<\/a
>/s
).uniq
427 # Undo the actions performed by a generator. Rewind the action
428 # manifest and attempt to completely erase the results of each action.
429 class Destroy
< RewindBase
430 # Remove a file if it exists and is a file.
431 def file(relative_source
, relative_destination
, file_options
= {})
432 destination
= destination_path(relative_destination
)
433 if File
.exist
?(destination
)
434 logger
.rm relative_destination
435 unless options
[:pretend]
437 # If the file has been marked to be added
438 # but has not yet been checked in, revert and delete
439 if options
[:svn][relative_destination
]
440 system("svn revert #{destination}")
441 FileUtils
.rm(destination
)
443 # If the directory is not in the status list, it
444 # has no modifications so we can simply remove it
445 system("svn rm #{destination}")
448 if options
[:git][:new][relative_destination
]
449 # file has been added, but not committed
450 system("git reset HEAD #{relative_destination}")
451 FileUtils
.rm(destination
)
452 elsif options
[:git][:modified][relative_destination
]
453 # file is committed and modified
454 system("git rm -f #{relative_destination}")
456 # If the directory is not in the status list, it
457 # has no modifications so we can simply remove it
458 system("git rm #{relative_destination}")
461 FileUtils
.rm(destination
)
465 logger
.missing relative_destination
470 # Templates are deleted just like files and the actions take the
471 # same parameters, so simply alias the file method.
472 alias_method
:template, :file
474 # Remove each directory in the given path from right to left.
475 # Remove each subdirectory if it exists and is a directory.
476 def directory(relative_path
)
477 parts
= relative_path
.split('/')
479 partial
= File
.join(parts
)
480 path
= destination_path(partial
)
482 if Dir
[File
.join(path
, '*')].empty
?
484 unless options
[:pretend]
486 # If the directory has been marked to be added
487 # but has not yet been checked in, revert and delete
488 if options
[:svn][relative_path
]
489 system("svn revert #{path}")
490 FileUtils
.rmdir(path
)
492 # If the directory is not in the status list, it
493 # has no modifications so we can simply remove it
494 system("svn rm #{path}")
496 # I don't think git needs to remove directories?..
497 # or maybe they have special consideration...
499 FileUtils
.rmdir(path
)
503 logger
.notempty partial
506 logger
.missing partial
512 def complex_template(*args
)
513 # nothing should be done here
516 # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
517 def migration_template(relative_source
, relative_destination
, template_options
= {})
518 migration_directory relative_destination
520 migration_file_name
= template_options
[:migration_file_name] || file_name
521 unless migration_exists
?(migration_file_name
)
522 puts
"There is no migration named #{migration_file_name}"
527 existing_migrations(migration_file_name
).each
do |file_path
|
528 file(relative_source
, file_path
, template_options
)
532 def route_resources(*resources
)
533 resource_list
= resources
.map
{ |r
| r
.to_sym
.inspect
}.join(', ')
534 look_for
= "\n map.resources #{resource_list}\n"
535 logger
.route
"map.resources #{resource_list}"
536 gsub_file
'config/routes.rb', /(#{look_for})/mi, ''
541 # List a generator's action manifest.
543 def dependency(generator_name
, args
, options
= {})
544 logger
.dependency
"#{generator_name}(#{args.join(', ')}, #{options.inspect})"
547 def class_collisions(*class_names
)
548 logger
.class_collisions class_names
.join(', ')
551 def file(relative_source
, relative_destination
, options
= {})
552 logger
.file relative_destination
555 def template(relative_source
, relative_destination
, options
= {})
556 logger
.template relative_destination
559 def complex_template(relative_source
, relative_destination
, options
= {})
560 logger
.template
"#{options[:insert]} inside #{relative_destination}"
563 def directory(relative_path
)
564 logger
.directory
"#{destination_path(relative_path)}/"
568 logger
.readme args
.join(', ')
571 def migration_template(relative_source
, relative_destination
, options
= {})
572 migration_directory relative_destination
573 logger
.migration_template file_name
576 def route_resources(*resources
)
577 resource_list
= resources
.map
{ |r
| r
.to_sym
.inspect
}.join(', ')
578 logger
.route
"map.resources #{resource_list}"
582 # Update generator's action manifest.
583 class Update
< Create
584 def file(relative_source
, relative_destination
, options
= {})
585 # logger.file relative_destination
588 def template(relative_source
, relative_destination
, options
= {})
589 # logger.template relative_destination
592 def complex_template(relative_source
, relative_destination
, template_options
= {})
595 dest_file
= destination_path(relative_destination
)
596 source_to_update
= File
.readlines(dest_file
).join
598 logger
.missing relative_destination
602 logger
.refreshing
"#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"
604 begin_mark
= Regexp
.quote(template_part_mark(template_options
[:begin_mark], template_options
[:mark_id]))
605 end_mark
= Regexp
.quote(template_part_mark(template_options
[:end_mark], template_options
[:mark_id]))
607 # Refreshing inner part of the template with freshly rendered part.
608 rendered_part
= render_template_part(template_options
)
609 source_to_update
.gsub
!(/#{begin_mark}.*?#{end_mark}/m
, rendered_part
)
611 File
.open(dest_file
, 'w') { |file
| file
.write(source_to_update
) }
614 def directory(relative_path
)
615 # logger.directory "#{destination_path(relative_path)}/"