Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/railties/lib/rails_generator/commands.rb

Revision 9176, 23.4 kB (checked in by bitsweat, 2 years ago)

Update generator tests. Closes #11487 [thechrisoshow]

Line 
1 require 'delegate'
2 require 'optparse'
3 require 'fileutils'
4 require 'tempfile'
5 require 'erb'
6
7 module Rails
8   module Generator
9     module Commands
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)
15       end
16
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)
23         end
24       end
25
26
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.
31       #
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.
36       #
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.
41         def invoke!
42           manifest.replay(self)
43         end
44
45         def dependency(generator_name, args, runtime_options = {})
46           logger.dependency(generator_name) do
47             self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke!
48           end
49         end
50
51         # Does nothing for all commands except Create.
52         def class_collisions(*class_names)
53         end
54
55         # Does nothing for all commands except Create.
56         def readme(*args)
57         end
58
59         protected
60           def migration_directory(relative_path)
61             directory(@migration_directory = relative_path)
62           end
63
64           def existing_migrations(file_name)
65             Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/)
66           end
67
68           def migration_exists?(file_name)
69             not existing_migrations(file_name).empty?
70           end
71
72           def next_migration_string(padding = 3)
73             Time.now.utc.strftime("%Y%m%d%H%M%S")
74           end
75
76           def gsub_file(relative_destination, regexp, *args, &block)
77             path = destination_path(relative_destination)
78             content = File.read(path).gsub(regexp, *args, &block)
79             File.open(path, 'wb') { |file| file.write(content) }
80           end
81
82         private
83           # Ask the user interactively whether to force collision.
84           def force_file_collision?(destination, src, dst, file_options = {}, &block)
85             $stdout.print "overwrite #{destination}? (enter \"h\" for help) [Ynaqdh] "
86             case $stdin.gets.chomp
87               when /\Ad\z/i
88                 Tempfile.open(File.basename(destination), File.dirname(dst)) do |temp|
89                   temp.write render_file(src, file_options, &block)
90                   temp.rewind
91                   $stdout.puts `#{diff_cmd} #{dst} #{temp.path}`
92                 end
93                 puts "retrying"
94                 raise 'retry diff'
95               when /\Aa\z/i
96                 $stdout.puts "forcing #{spec.name}"
97                 options[:collision] = :force
98               when /\Aq\z/i
99                 $stdout.puts "aborting #{spec.name}"
100                 raise SystemExit
101               when /\An\z/i then :skip
102               when /\Ay\z/i then :force
103               else
104                 $stdout.puts <<-HELP
105 Y - yes, overwrite
106 n - no, do not overwrite
107 a - all, overwrite this and all others
108 q - quit, abort
109 d - diff, show the differences between the old and the new
110 h - help, show this help
111 HELP
112                 raise 'retry'
113             end
114           rescue
115             retry
116           end
117
118           def diff_cmd
119             ENV['RAILS_DIFF'] || 'diff -u'
120           end
121
122           def render_template_part(template_options)
123             # Getting Sandbox to evaluate part template in it
124             part_binding = template_options[:sandbox].call.sandbox_binding
125             part_rel_path = template_options[:insert]
126             part_path = source_path(part_rel_path)
127
128             # Render inner template within Sandbox binding
129             rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding)
130             begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id])
131             end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id])
132             begin_mark + rendered_part + end_mark
133           end
134
135           def template_part_mark(name, id)
136             "<!--[#{name}:#{id}]-->\n"
137           end
138       end
139
140       # Base class for commands which handle generator actions in reverse, such as Destroy.
141       class RewindBase < Base
142         # Rewind action manifest.
143         def invoke!
144           manifest.rewind(self)
145         end
146       end
147
148
149       # Create is the premier generator command.  It copies files, creates
150       # directories, renders templates, and more.
151       class Create < Base
152
153         # Check whether the given class names are already taken by
154         # Ruby or Rails.  In the future, expand to check other namespaces
155         # such as the rest of the user's app.
156         def class_collisions(*class_names)
157           class_names.flatten.each do |class_name|
158             # Convert to string to allow symbol arguments.
159             class_name = class_name.to_s
160
161             # Skip empty strings.
162             next if class_name.strip.empty?
163
164             # Split the class from its module nesting.
165             nesting = class_name.split('::')
166             name = nesting.pop
167
168             # Extract the last Module in the nesting.
169             last = nesting.inject(Object) { |last, nest|
170               break unless last.const_defined?(nest)
171               last.const_get(nest)
172             }
173
174             # If the last Module exists, check whether the given
175             # class exists and raise a collision if so.
176             if last and last.const_defined?(name.camelize)
177               raise_class_collision(class_name)
178             end
179           end
180         end
181
182         # Copy a file from source to destination with collision checking.
183         #
184         # The file_options hash accepts :chmod and :shebang and :collision options.
185         # :chmod sets the permissions of the destination file:
186         #   file 'config/empty.log', 'log/test.log', :chmod => 0664
187         # :shebang sets the #!/usr/bin/ruby line for scripts
188         #   file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
189         # :collision sets the collision option only for the destination file:
190         #   file 'settings/server.yml', 'config/server.yml', :collision => :skip
191         #
192         # Collisions are handled by checking whether the destination file
193         # exists and either skipping the file, forcing overwrite, or asking
194         # the user what to do.
195         def file(relative_source, relative_destination, file_options = {}, &block)
196           # Determine full paths for source and destination files.
197           source              = source_path(relative_source)
198           destination         = destination_path(relative_destination)
199           destination_exists  = File.exist?(destination)
200
201           # If source and destination are identical then we're done.
202           if destination_exists and identical?(source, destination, &block)
203             return logger.identical(relative_destination)
204           end
205
206           # Check for and resolve file collisions.
207           if destination_exists
208
209             # Make a choice whether to overwrite the file.  :force and
210             # :skip already have their mind made up, but give :ask a shot.
211             choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask
212               when :ask   then force_file_collision?(relative_destination, source, destination, file_options, &block)
213               when :force then :force
214               when :skip  then :skip
215               else raise "Invalid collision option: #{options[:collision].inspect}"
216             end
217
218             # Take action based on our choice.  Bail out if we chose to
219             # skip the file; otherwise, log our transgression and continue.
220             case choice
221               when :force then logger.force(relative_destination)
222               when :skip  then return(logger.skip(relative_destination))
223               else raise "Invalid collision choice: #{choice}.inspect"
224             end
225
226           # File doesn't exist so log its unbesmirched creation.
227           else
228             logger.create relative_destination
229           end
230
231           # If we're pretending, back off now.
232           return if options[:pretend]
233
234           # Write destination file with optional shebang.  Yield for content
235           # if block given so templaters may render the source file.  If a
236           # shebang is requested, replace the existing shebang or insert a
237           # new one.
238           File.open(destination, 'wb') do |dest|
239             dest.write render_file(source, file_options, &block)
240           end
241
242           # Optionally change permissions.
243           if file_options[:chmod]
244             FileUtils.chmod(file_options[:chmod], destination)
245           end
246
247           # Optionally add file to subversion or git
248           system("svn add #{destination}") if options[:svn]
249           system("git add -v #{relative_destination}") if options[:git]
250         end
251
252         # Checks if the source and the destination file are identical. If
253         # passed a block then the source file is a template that needs to first
254         # be evaluated before being compared to the destination.
255         def identical?(source, destination, &block)
256           return false if File.directory? destination
257           source      = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source)
258           destination = IO.read(destination)
259           source == destination
260         end
261
262         # Generate a file for a Rails application using an ERuby template.
263         # Looks up and evaluates a template by name and writes the result.
264         #
265         # The ERB template uses explicit trim mode to best control the
266         # proliferation of whitespace in generated code.  <%- trims leading
267         # whitespace; -%> trims trailing whitespace including one newline.
268         #
269         # A hash of template options may be passed as the last argument.
270         # The options accepted by the file are accepted as well as :assigns,
271         # a hash of variable bindings.  Example:
272         #   template 'foo', 'bar', :assigns => { :action => 'view' }
273         #
274         # Template is implemented in terms of file.  It calls file with a
275         # block which takes a file handle and returns its rendered contents.
276         def template(relative_source, relative_destination, template_options = {})
277           file(relative_source, relative_destination, template_options) do |file|
278             # Evaluate any assignments in a temporary, throwaway binding.
279             vars = template_options[:assigns] || {}
280             b = binding
281             vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }
282
283             # Render the source file with the temporary binding.
284             ERB.new(file.read, nil, '-').result(b)
285           end
286         end
287
288         def complex_template(relative_source, relative_destination, template_options = {})
289           options = template_options.dup
290           options[:assigns] ||= {}
291           options[:assigns]['template_for_inclusion'] = render_template_part(template_options)
292           template(relative_source, relative_destination, options)
293         end
294
295         # Create a directory including any missing parent directories.
296         # Always skips directories which exist.
297         def directory(relative_path)
298           path = destination_path(relative_path)
299           if File.exist?(path)
300             logger.exists relative_path
301           else
302             logger.create relative_path
303             unless options[:pretend]
304               FileUtils.mkdir_p(path)
305               # git doesn't require adding the paths, adding the files later will
306               # automatically do a path add.
307
308               # Subversion doesn't do path adds, so we need to add
309               # each directory individually.
310               # So stack up the directory tree and add the paths to
311               # subversion in order without recursion.
312               if options[:svn]
313                 stack = [relative_path]
314                 until File.dirname(stack.last) == stack.last # dirname('.') == '.'
315                   stack.push File.dirname(stack.last)
316                 end
317                 stack.reverse_each do |rel_path|
318                   svn_path = destination_path(rel_path)
319                   system("svn add -N #{svn_path}") unless File.directory?(File.join(svn_path, '.svn'))
320                 end
321               end
322             end
323           end
324         end
325
326         # Display a README.
327         def readme(*relative_sources)
328           relative_sources.flatten.each do |relative_source|
329             logger.readme relative_source
330             puts File.read(source_path(relative_source)) unless options[:pretend]
331           end
332         end
333
334         # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
335         def migration_template(relative_source, relative_destination, template_options = {})
336           migration_directory relative_destination
337           migration_file_name = template_options[:migration_file_name] || file_name
338           raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name)
339           template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options)
340         end
341
342         def route_resources(*resources)
343           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
344           sentinel = 'ActionController::Routing::Routes.draw do |map|'
345
346           logger.route "map.resources #{resource_list}"
347           unless options[:pretend]
348             gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
349               "#{match}\n  map.resources #{resource_list}\n"
350             end
351           end
352         end
353
354         private
355           def render_file(path, options = {})
356             File.open(path, 'rb') do |file|
357               if block_given?
358                 yield file
359               else
360                 content = ''
361                 if shebang = options[:shebang]
362                   content << "#!#{shebang}\n"
363                   if line = file.gets
364                     content << "line\n" if line !~ /^#!/
365                   end
366                 end
367                 content << file.read
368               end
369             end
370           end
371
372           # Raise a usage error with an informative WordNet suggestion.
373           # Thanks to Florian Gross (flgr).
374           def raise_class_collision(class_name)
375             message = <<end_message
376   The name '#{class_name}' is reserved by Ruby on Rails.
377   Please choose an alternative and run this generator again.
378 end_message
379             if suggest = find_synonyms(class_name)
380               message << "\n  Suggestions:  \n\n"
381               message << suggest.join("\n")
382             end
383             raise UsageError, message
384           end
385
386           SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/perl/webwn?s=%s"
387
388           # Look up synonyms on WordNet.  Thanks to Florian Gross (flgr).
389           def find_synonyms(word)
390             require 'open-uri'
391             require 'timeout'
392             timeout(5) do
393               open(SYNONYM_LOOKUP_URI % word) do |stream|
394                 # Grab words linked to dictionary entries as possible synonyms
395                 data = stream.read.gsub("&nbsp;", " ").scan(/<a href="webwn.*?">([\w ]*?)<\/a>/s).uniq
396               end
397             end
398           rescue Exception
399             return nil
400           end
401       end
402
403
404       # Undo the actions performed by a generator.  Rewind the action
405       # manifest and attempt to completely erase the results of each action.
406       class Destroy < RewindBase
407         # Remove a file if it exists and is a file.
408         def file(relative_source, relative_destination, file_options = {})
409           destination = destination_path(relative_destination)
410           if File.exist?(destination)
411             logger.rm relative_destination
412             unless options[:pretend]
413               if options[:svn]
414                 # If the file has been marked to be added
415                 # but has not yet been checked in, revert and delete
416                 if options[:svn][relative_destination]
417                   system("svn revert #{destination}")
418                   FileUtils.rm(destination)
419                 else
420                 # If the directory is not in the status list, it
421                 # has no modifications so we can simply remove it
422                   system("svn rm #{destination}")
423                 end
424               elsif options[:git]
425                 if options[:git][:new][relative_destination]
426                   # file has been added, but not committed
427                   system("git reset HEAD #{relative_destination}")
428                   FileUtils.rm(destination)
429                 elsif options[:git][:modified][relative_destination]
430                   # file is committed and modified
431                   system("git rm -f #{relative_destination}")
432                 else
433                   # If the directory is not in the status list, it
434                   # has no modifications so we can simply remove it
435                   system("git rm #{relative_destination}")
436                 end
437               else
438                 FileUtils.rm(destination)
439               end
440             end
441           else
442             logger.missing relative_destination
443             return
444           end
445         end
446
447         # Templates are deleted just like files and the actions take the
448         # same parameters, so simply alias the file method.
449         alias_method :template, :file
450
451         # Remove each directory in the given path from right to left.
452         # Remove each subdirectory if it exists and is a directory.
453         def directory(relative_path)
454           parts = relative_path.split('/')
455           until parts.empty?
456             partial = File.join(parts)
457             path = destination_path(partial)
458             if File.exist?(path)
459               if Dir[File.join(path, '*')].empty?
460                 logger.rmdir partial
461                 unless options[:pretend]
462                   if options[:svn]
463                     # If the directory has been marked to be added
464                     # but has not yet been checked in, revert and delete
465                     if options[:svn][relative_path]
466                       system("svn revert #{path}")
467                       FileUtils.rmdir(path)
468                     else
469                     # If the directory is not in the status list, it
470                     # has no modifications so we can simply remove it
471                       system("svn rm #{path}")
472                     end
473                   # I don't think git needs to remove directories?..
474                   # or maybe they have special consideration...
475                   else
476                     FileUtils.rmdir(path)
477                   end
478                 end
479               else
480                 logger.notempty partial
481               end
482             else
483               logger.missing partial
484             end
485             parts.pop
486           end
487         end
488
489         def complex_template(*args)
490           # nothing should be done here
491         end
492
493         # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
494         def migration_template(relative_source, relative_destination, template_options = {})
495           migration_directory relative_destination
496
497           migration_file_name = template_options[:migration_file_name] || file_name
498           unless migration_exists?(migration_file_name)
499             puts "There is no migration named #{migration_file_name}"
500             return
501           end
502
503
504           existing_migrations(migration_file_name).each do |file_path|
505             file(relative_source, file_path, template_options)
506           end
507         end
508
509         def route_resources(*resources)
510           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
511           look_for = "\n  map.resources #{resource_list}\n"
512           logger.route "map.resources #{resource_list}"
513           gsub_file 'config/routes.rb', /(#{look_for})/mi, ''
514         end
515       end
516
517
518       # List a generator's action manifest.
519       class List < Base
520         def dependency(generator_name, args, options = {})
521           logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})"
522         end
523
524         def class_collisions(*class_names)
525           logger.class_collisions class_names.join(', ')
526         end
527
528         def file(relative_source, relative_destination, options = {})
529           logger.file relative_destination
530         end
531
532         def template(relative_source, relative_destination, options = {})
533           logger.template relative_destination
534         end
535
536         def complex_template(relative_source, relative_destination, options = {})
537           logger.template "#{options[:insert]} inside #{relative_destination}"
538         end
539
540         def directory(relative_path)
541           logger.directory "#{destination_path(relative_path)}/"
542         end
543
544         def readme(*args)
545           logger.readme args.join(', ')
546         end
547
548         def migration_template(relative_source, relative_destination, options = {})
549           migration_directory relative_destination
550           logger.migration_template file_name
551         end
552
553         def route_resources(*resources)
554           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
555           logger.route "map.resources #{resource_list}"
556         end
557       end
558
559       # Update generator's action manifest.
560       class Update < Create
561         def file(relative_source, relative_destination, options = {})
562           # logger.file relative_destination
563         end
564
565         def template(relative_source, relative_destination, options = {})
566           # logger.template relative_destination
567         end
568
569         def complex_template(relative_source, relative_destination, template_options = {})
570
571            begin
572              dest_file = destination_path(relative_destination)
573              source_to_update = File.readlines(dest_file).join
574            rescue Errno::ENOENT
575              logger.missing relative_destination
576              return
577            end
578
579            logger.refreshing "#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"
580
581            begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id]))
582            end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id]))
583
584            # Refreshing inner part of the template with freshly rendered part.
585            rendered_part = render_template_part(template_options)
586            source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part)
587
588            File.open(dest_file, 'w') { |file| file.write(source_to_update) }
589         end
590
591         def directory(relative_path)
592           # logger.directory "#{destination_path(relative_path)}/"
593         end
594       end
595
596     end
597   end
598 end
Note: See TracBrowser for help on using the browser.