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.
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
!
51 # Does nothing for all commands except Create.
52 def class_collisions(*class_names
)
55 # Does nothing for all commands except Create.
60 def current_migration_number
61 Dir
.glob("#{RAILS_ROOT}/#{@migration_directory}/[0-9]*_*.rb").inject(0) do |max
, file_path
|
62 n
= File
.basename(file_path
).split('_', 2).first
.to_i
63 if n
> max
then n
else max
end
67 def next_migration_number
68 current_migration_number
+ 1
71 def migration_directory(relative_path
)
72 directory(@migration_directory = relative_path
)
75 def existing_migrations(file_name
)
76 Dir
.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_
#{file_name}.rb$/)
79 def migration_exists
?(file_name
)
80 not existing_migrations(file_name
).empty
?
83 def next_migration_string(padding
= 3)
84 if ActiveRecord
::Base.timestamped_migrations
85 Time
.now
.utc
.strftime("%Y%m%d%H%M%S")
87 "%.#{padding}d" % next_migration_number
91 def gsub_file(relative_destination
, regexp
, *args
, &block
)
92 path
= destination_path(relative_destination
)
93 content
= File
.read(path
).gsub(regexp
, *args
, &block
)
94 File
.open(path
, 'wb') { |file
| file
.write(content
) }
98 # Ask the user interactively whether to force collision.
99 def force_file_collision
?(destination
, src
, dst
, file_options
= {}, &block
)
100 $stdout.print
"overwrite #{destination}? (enter \"h\" for help) [Ynaqdh] "
101 case $stdin.gets
.chomp
103 Tempfile
.open(File
.basename(destination
), File
.dirname(dst
)) do |temp
|
104 temp
.write
render_file(src
, file_options
, &block
)
106 $stdout.puts
`#{diff_cmd} "#{dst}" "#{temp.path}"`
111 $stdout.puts
"forcing #{spec.name}"
112 options
[:collision] = :force
114 $stdout.puts
"aborting #{spec.name}"
116 when /\An\z/i
then :skip
117 when /\Ay\z/i
then :force
121 n - no, do not overwrite
122 a - all, overwrite this and all others
124 d - diff, show the differences between the old and the new
125 h - help, show this help
134 ENV['RAILS_DIFF'] || 'diff -u'
137 def render_template_part(template_options
)
138 # Getting Sandbox to evaluate part template in it
139 part_binding
= template_options
[:sandbox].call
.sandbox_binding
140 part_rel_path
= template_options
[:insert]
141 part_path
= source_path(part_rel_path
)
143 # Render inner template within Sandbox binding
144 rendered_part
= ERB
.new(File
.readlines(part_path
).join
, nil, '-').result(part_binding
)
145 begin_mark
= template_part_mark(template_options
[:begin_mark], template_options
[:mark_id])
146 end_mark
= template_part_mark(template_options
[:end_mark], template_options
[:mark_id])
147 begin_mark
+ rendered_part
+ end_mark
150 def template_part_mark(name
, id
)
151 "<!--[#{name}:#{id}]-->\n"
155 # Base class for commands which handle generator actions in reverse, such as Destroy.
156 class RewindBase
< Base
157 # Rewind action manifest.
159 manifest
.rewind(self)
164 # Create is the premier generator command. It copies files, creates
165 # directories, renders templates, and more.
168 # Check whether the given class names are already taken by
169 # Ruby or Rails. In the future, expand to check other namespaces
170 # such as the rest of the user's app.
171 def class_collisions(*class_names
)
172 path
= class_names
.shift
173 class_names
.flatten
.each
do |class_name
|
174 # Convert to string to allow symbol arguments.
175 class_name
= class_name
.to_s
177 # Skip empty strings.
178 next if class_name
.strip
.empty
?
180 # Split the class from its module nesting.
181 nesting
= class_name
.split('::')
184 # Extract the last Module in the nesting.
185 last
= nesting
.inject(Object
) { |last
, nest
|
186 break unless last
.const_defined
?(nest
)
190 # If the last Module exists, check whether the given
191 # class exists and raise a collision if so.
192 if last
and last
.const_defined
?(name
.camelize
)
193 raise_class_collision(class_name
)
198 # Copy a file from source to destination with collision checking.
200 # The file_options hash accepts :chmod and :shebang and :collision options.
201 # :chmod sets the permissions of the destination file:
202 # file 'config/empty.log', 'log/test.log', :chmod => 0664
203 # :shebang sets the #!/usr/bin/ruby line for scripts
204 # file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
205 # :collision sets the collision option only for the destination file:
206 # file 'settings/server.yml', 'config/server.yml', :collision => :skip
208 # Collisions are handled by checking whether the destination file
209 # exists and either skipping the file, forcing overwrite, or asking
210 # the user what to do.
211 def file(relative_source
, relative_destination
, file_options
= {}, &block
)
212 # Determine full paths for source and destination files.
213 source
= source_path(relative_source
)
214 destination
= destination_path(relative_destination
)
215 destination_exists
= File
.exist
?(destination
)
217 # If source and destination are identical then we're done.
218 if destination_exists
and identical
?(source
, destination
, &block
)
219 return logger
.identical(relative_destination
)
222 # Check for and resolve file collisions.
223 if destination_exists
225 # Make a choice whether to overwrite the file. :force and
226 # :skip already have their mind made up, but give :ask a shot.
227 choice
= case (file_options
[:collision] || options
[:collision]).to_sym
#|| :ask
228 when :ask then force_file_collision
?(relative_destination
, source
, destination
, file_options
, &block
)
229 when :force then :force
230 when :skip then :skip
231 else raise "Invalid collision option: #{options[:collision].inspect}"
234 # Take action based on our choice. Bail out if we chose to
235 # skip the file; otherwise, log our transgression and continue.
237 when :force then logger
.force(relative_destination
)
238 when :skip then return(logger
.skip(relative_destination
))
239 else raise "Invalid collision choice: #{choice}.inspect"
242 # File doesn't exist so log its unbesmirched creation.
244 logger
.create relative_destination
247 # If we're pretending, back off now.
248 return if options
[:pretend]
250 # Write destination file with optional shebang. Yield for content
251 # if block given so templaters may render the source file. If a
252 # shebang is requested, replace the existing shebang or insert a
254 File
.open(destination
, 'wb') do |dest
|
255 dest
.write
render_file(source
, file_options
, &block
)
258 # Optionally change permissions.
259 if file_options
[:chmod]
260 FileUtils
.chmod(file_options
[:chmod], destination
)
263 # Optionally add file to subversion or git
264 system("svn add #{destination}") if options
[:svn]
265 system("git add -v #{relative_destination}") if options
[:git]
268 # Checks if the source and the destination file are identical. If
269 # passed a block then the source file is a template that needs to first
270 # be evaluated before being compared to the destination.
271 def identical
?(source
, destination
, &block
)
272 return false if File
.directory
? destination
273 source
= block_given
? ? File
.open(source
) {|sf
| yield(sf
)} : IO
.read(source
)
274 destination
= IO
.read(destination
)
275 source
== destination
278 # Generate a file for a Rails application using an ERuby template.
279 # Looks up and evaluates a template by name and writes the result.
281 # The ERB template uses explicit trim mode to best control the
282 # proliferation of whitespace in generated code. <%- trims leading
283 # whitespace; -%> trims trailing whitespace including one newline.
285 # A hash of template options may be passed as the last argument.
286 # The options accepted by the file are accepted as well as :assigns,
287 # a hash of variable bindings. Example:
288 # template 'foo', 'bar', :assigns => { :action => 'view' }
290 # Template is implemented in terms of file. It calls file with a
291 # block which takes a file handle and returns its rendered contents.
292 def template(relative_source
, relative_destination
, template_options
= {})
293 file(relative_source
, relative_destination
, template_options
) do |file
|
294 # Evaluate any assignments in a temporary, throwaway binding.
295 vars
= template_options
[:assigns] || {}
297 vars
.each
{ |k
,v
| eval "#{k} = vars[:#{k}] || vars['#{k}']", b
}
299 # Render the source file with the temporary binding.
300 ERB
.new(file
.read
, nil, '-').result(b
)
304 def complex_template(relative_source
, relative_destination
, template_options
= {})
305 options
= template_options
.dup
306 options
[:assigns] ||= {}
307 options
[:assigns]['template_for_inclusion'] = render_template_part(template_options
)
308 template(relative_source
, relative_destination
, options
)
311 # Create a directory including any missing parent directories.
312 # Always skips directories which exist.
313 def directory(relative_path
)
314 path
= destination_path(relative_path
)
316 logger
.exists relative_path
318 logger
.create relative_path
319 unless options
[:pretend]
320 FileUtils
.mkdir_p(path
)
321 # git doesn't require adding the paths, adding the files later will
322 # automatically do a path add.
324 # Subversion doesn't do path adds, so we need to add
325 # each directory individually.
326 # So stack up the directory tree and add the paths to
327 # subversion in order without recursion.
329 stack
= [relative_path
]
330 until File
.dirname(stack
.last
) == stack
.last
# dirname('.') == '.'
331 stack
.push File
.dirname(stack
.last
)
333 stack
.reverse_each
do |rel_path
|
334 svn_path
= destination_path(rel_path
)
335 system("svn add -N #{svn_path}") unless File
.directory
?(File
.join(svn_path
, '.svn'))
343 def readme(*relative_sources
)
344 relative_sources
.flatten
.each
do |relative_source
|
345 logger
.readme relative_source
346 puts File
.read(source_path(relative_source
)) unless options
[:pretend]
350 # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
351 def migration_template(relative_source
, relative_destination
, template_options
= {})
352 migration_directory relative_destination
353 migration_file_name
= template_options
[:migration_file_name] || file_name
354 raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists
?(migration_file_name
)
355 template(relative_source
, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options
)
358 def route_resources(*resources
)
359 resource_list
= resources
.map
{ |r
| r
.to_sym
.inspect
}.join(', ')
360 sentinel
= 'ActionController::Routing::Routes.draw do |map|'
362 logger
.route
"map.resources #{resource_list}"
363 unless options
[:pretend]
364 gsub_file
'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
365 "#{match}\n map.resources #{resource_list}\n"
371 def render_file(path
, options
= {})
372 File
.open(path
, 'rb') do |file
|
377 if shebang
= options
[:shebang]
378 content
<< "#!#{shebang}\n"
380 content
<< "line\n" if line
!~
/^#!/
388 # Raise a usage error with an informative WordNet suggestion.
389 # Thanks to Florian Gross (flgr).
390 def raise_class_collision(class_name
)
391 message
= <<end_message
392 The name '#{class_name}' is either already used in your application or reserved by Ruby on Rails.
393 Please choose an alternative and run this generator again.
395 if suggest
= find_synonyms(class_name
)
397 message
<< "\n Suggestions: \n\n"
398 message
<< suggest
.join("\n")
401 raise UsageError
, message
404 SYNONYM_LOOKUP_URI
= "http://wordnet.princeton.edu/perl/webwn?s=%s"
406 # Look up synonyms on WordNet. Thanks to Florian Gross (flgr).
407 def find_synonyms(word
)
411 open(SYNONYM_LOOKUP_URI
% word
) do |stream
|
412 # Grab words linked to dictionary entries as possible synonyms
413 data = stream
.read
.gsub(" ", " ").scan(/<a href="webwn.*?">([\w ]*?)<\/a
>/s
).uniq
422 # Undo the actions performed by a generator. Rewind the action
423 # manifest and attempt to completely erase the results of each action.
424 class Destroy
< RewindBase
425 # Remove a file if it exists and is a file.
426 def file(relative_source
, relative_destination
, file_options
= {})
427 destination
= destination_path(relative_destination
)
428 if File
.exist
?(destination
)
429 logger
.rm relative_destination
430 unless options
[:pretend]
432 # If the file has been marked to be added
433 # but has not yet been checked in, revert and delete
434 if options
[:svn][relative_destination
]
435 system("svn revert #{destination}")
436 FileUtils
.rm(destination
)
438 # If the directory is not in the status list, it
439 # has no modifications so we can simply remove it
440 system("svn rm #{destination}")
443 if options
[:git][:new][relative_destination
]
444 # file has been added, but not committed
445 system("git reset HEAD #{relative_destination}")
446 FileUtils
.rm(destination
)
447 elsif options
[:git][:modified][relative_destination
]
448 # file is committed and modified
449 system("git rm -f #{relative_destination}")
451 # If the directory is not in the status list, it
452 # has no modifications so we can simply remove it
453 system("git rm #{relative_destination}")
456 FileUtils
.rm(destination
)
460 logger
.missing relative_destination
465 # Templates are deleted just like files and the actions take the
466 # same parameters, so simply alias the file method.
467 alias_method
:template, :file
469 # Remove each directory in the given path from right to left.
470 # Remove each subdirectory if it exists and is a directory.
471 def directory(relative_path
)
472 parts
= relative_path
.split('/')
474 partial
= File
.join(parts
)
475 path
= destination_path(partial
)
477 if Dir
[File
.join(path
, '*')].empty
?
479 unless options
[:pretend]
481 # If the directory has been marked to be added
482 # but has not yet been checked in, revert and delete
483 if options
[:svn][relative_path
]
484 system("svn revert #{path}")
485 FileUtils
.rmdir(path
)
487 # If the directory is not in the status list, it
488 # has no modifications so we can simply remove it
489 system("svn rm #{path}")
491 # I don't think git needs to remove directories?..
492 # or maybe they have special consideration...
494 FileUtils
.rmdir(path
)
498 logger
.notempty partial
501 logger
.missing partial
507 def complex_template(*args
)
508 # nothing should be done here
511 # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
512 def migration_template(relative_source
, relative_destination
, template_options
= {})
513 migration_directory relative_destination
515 migration_file_name
= template_options
[:migration_file_name] || file_name
516 unless migration_exists
?(migration_file_name
)
517 puts
"There is no migration named #{migration_file_name}"
522 existing_migrations(migration_file_name
).each
do |file_path
|
523 file(relative_source
, file_path
, template_options
)
527 def route_resources(*resources
)
528 resource_list
= resources
.map
{ |r
| r
.to_sym
.inspect
}.join(', ')
529 look_for
= "\n map.resources #{resource_list}\n"
530 logger
.route
"map.resources #{resource_list}"
531 gsub_file
'config/routes.rb', /(#{look_for})/mi, ''
536 # List a generator's action manifest.
538 def dependency(generator_name
, args
, options
= {})
539 logger
.dependency
"#{generator_name}(#{args.join(', ')}, #{options.inspect})"
542 def class_collisions(*class_names
)
543 logger
.class_collisions class_names
.join(', ')
546 def file(relative_source
, relative_destination
, options
= {})
547 logger
.file relative_destination
550 def template(relative_source
, relative_destination
, options
= {})
551 logger
.template relative_destination
554 def complex_template(relative_source
, relative_destination
, options
= {})
555 logger
.template
"#{options[:insert]} inside #{relative_destination}"
558 def directory(relative_path
)
559 logger
.directory
"#{destination_path(relative_path)}/"
563 logger
.readme args
.join(', ')
566 def migration_template(relative_source
, relative_destination
, options
= {})
567 migration_directory relative_destination
568 logger
.migration_template file_name
571 def route_resources(*resources
)
572 resource_list
= resources
.map
{ |r
| r
.to_sym
.inspect
}.join(', ')
573 logger
.route
"map.resources #{resource_list}"
577 # Update generator's action manifest.
578 class Update
< Create
579 def file(relative_source
, relative_destination
, options
= {})
580 # logger.file relative_destination
583 def template(relative_source
, relative_destination
, options
= {})
584 # logger.template relative_destination
587 def complex_template(relative_source
, relative_destination
, template_options
= {})
590 dest_file
= destination_path(relative_destination
)
591 source_to_update
= File
.readlines(dest_file
).join
593 logger
.missing relative_destination
597 logger
.refreshing
"#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"
599 begin_mark
= Regexp
.quote(template_part_mark(template_options
[:begin_mark], template_options
[:mark_id]))
600 end_mark
= Regexp
.quote(template_part_mark(template_options
[:end_mark], template_options
[:mark_id]))
602 # Refreshing inner part of the template with freshly rendered part.
603 rendered_part
= render_template_part(template_options
)
604 source_to_update
.gsub
!(/#{begin_mark}.*?#{end_mark}/m
, rendered_part
)
606 File
.open(dest_file
, 'w') { |file
| file
.write(source_to_update
) }
609 def directory(relative_path
)
610 # logger.directory "#{destination_path(relative_path)}/"